##// END OF EJS Templates
hgweb: remove Python 2 support code...
Gregory Szorc -
r49761:e453c698 default
parent child Browse files
Show More
@@ -1,443 +1,436 b''
1 1 # hgweb/server.py - The standalone hg web server.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9
10 10 import errno
11 11 import importlib
12 12 import os
13 13 import socket
14 14 import sys
15 15 import traceback
16 16 import wsgiref.validate
17 17
18 18 from ..i18n import _
19 19 from ..pycompat import (
20 20 getattr,
21 21 open,
22 22 )
23 23
24 24 from .. import (
25 25 encoding,
26 26 error,
27 27 pycompat,
28 28 util,
29 29 )
30 30 from ..utils import (
31 31 urlutil,
32 32 )
33 33
34 34 httpservermod = util.httpserver
35 35 socketserver = util.socketserver
36 36 urlerr = util.urlerr
37 37 urlreq = util.urlreq
38 38
39 39 from . import common
40 40
41 41
42 42 def _splitURI(uri):
43 43 """Return path and query that has been split from uri
44 44
45 45 Just like CGI environment, the path is unquoted, the query is
46 46 not.
47 47 """
48 48 if '?' in uri:
49 49 path, query = uri.split('?', 1)
50 50 else:
51 51 path, query = uri, r''
52 52 return urlreq.unquote(path), query
53 53
54 54
55 55 class _error_logger(object):
56 56 def __init__(self, handler):
57 57 self.handler = handler
58 58
59 59 def flush(self):
60 60 pass
61 61
62 62 def write(self, str):
63 63 self.writelines(str.split(b'\n'))
64 64
65 65 def writelines(self, seq):
66 66 for msg in seq:
67 67 self.handler.log_error("HG error: %s", encoding.strfromlocal(msg))
68 68
69 69
70 70 class _httprequesthandler(httpservermod.basehttprequesthandler):
71 71
72 72 url_scheme = b'http'
73 73
74 74 @staticmethod
75 75 def preparehttpserver(httpserver, ui):
76 76 """Prepare .socket of new HTTPServer instance"""
77 77
78 78 def __init__(self, *args, **kargs):
79 79 self.protocol_version = r'HTTP/1.1'
80 80 httpservermod.basehttprequesthandler.__init__(self, *args, **kargs)
81 81
82 82 def _log_any(self, fp, format, *args):
83 83 fp.write(
84 84 pycompat.sysbytes(
85 85 r"%s - - [%s] %s"
86 86 % (
87 87 self.client_address[0],
88 88 self.log_date_time_string(),
89 89 format % args,
90 90 )
91 91 )
92 92 + b'\n'
93 93 )
94 94 fp.flush()
95 95
96 96 def log_error(self, format, *args):
97 97 self._log_any(self.server.errorlog, format, *args)
98 98
99 99 def log_message(self, format, *args):
100 100 self._log_any(self.server.accesslog, format, *args)
101 101
102 102 def log_request(self, code='-', size='-'):
103 103 xheaders = []
104 104 if util.safehasattr(self, b'headers'):
105 105 xheaders = [
106 106 h for h in self.headers.items() if h[0].startswith('x-')
107 107 ]
108 108 self.log_message(
109 109 '"%s" %s %s%s',
110 110 self.requestline,
111 111 str(code),
112 112 str(size),
113 113 ''.join([' %s:%s' % h for h in sorted(xheaders)]),
114 114 )
115 115
116 116 def do_write(self):
117 117 try:
118 118 self.do_hgweb()
119 119 except socket.error as inst:
120 120 if inst.errno != errno.EPIPE:
121 121 raise
122 122
123 123 def do_POST(self):
124 124 try:
125 125 self.do_write()
126 126 except Exception as e:
127 127 # I/O below could raise another exception. So log the original
128 128 # exception first to ensure it is recorded.
129 129 if not (
130 130 isinstance(e, (OSError, socket.error))
131 131 and e.errno == errno.ECONNRESET
132 132 ):
133 133 tb = "".join(traceback.format_exception(*sys.exc_info()))
134 134 # We need a native-string newline to poke in the log
135 135 # message, because we won't get a newline when using an
136 136 # r-string. This is the easy way out.
137 137 newline = chr(10)
138 138 self.log_error(
139 139 r"Exception happened during processing "
140 140 "request '%s':%s%s",
141 141 self.path,
142 142 newline,
143 143 tb,
144 144 )
145 145
146 146 self._start_response("500 Internal Server Error", [])
147 147 self._write(b"Internal Server Error")
148 148 self._done()
149 149
150 150 def do_PUT(self):
151 151 self.do_POST()
152 152
153 153 def do_GET(self):
154 154 self.do_POST()
155 155
156 156 def do_hgweb(self):
157 157 self.sent_headers = False
158 158 path, query = _splitURI(self.path)
159 159
160 160 # Ensure the slicing of path below is valid
161 161 if path != self.server.prefix and not path.startswith(
162 162 self.server.prefix + b'/'
163 163 ):
164 164 self._start_response(pycompat.strurl(common.statusmessage(404)), [])
165 165 if self.command == 'POST':
166 166 # Paranoia: tell the client we're going to close the
167 167 # socket so they don't try and reuse a socket that
168 168 # might have a POST body waiting to confuse us. We do
169 169 # this by directly munging self.saved_headers because
170 170 # self._start_response ignores Connection headers.
171 171 self.saved_headers = [('Connection', 'Close')]
172 172 self._write(b"Not Found")
173 173 self._done()
174 174 return
175 175
176 176 env = {}
177 177 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
178 178 env['REQUEST_METHOD'] = self.command
179 179 env['SERVER_NAME'] = self.server.server_name
180 180 env['SERVER_PORT'] = str(self.server.server_port)
181 181 env['REQUEST_URI'] = self.path
182 182 env['SCRIPT_NAME'] = pycompat.sysstr(self.server.prefix)
183 183 env['PATH_INFO'] = pycompat.sysstr(path[len(self.server.prefix) :])
184 184 env['REMOTE_HOST'] = self.client_address[0]
185 185 env['REMOTE_ADDR'] = self.client_address[0]
186 186 env['QUERY_STRING'] = query or ''
187 187
188 if pycompat.ispy3:
189 if self.headers.get_content_type() is None:
190 env['CONTENT_TYPE'] = self.headers.get_default_type()
191 else:
192 env['CONTENT_TYPE'] = self.headers.get_content_type()
193 length = self.headers.get('content-length')
188 if self.headers.get_content_type() is None:
189 env['CONTENT_TYPE'] = self.headers.get_default_type()
194 190 else:
195 if self.headers.typeheader is None:
196 env['CONTENT_TYPE'] = self.headers.type
197 else:
198 env['CONTENT_TYPE'] = self.headers.typeheader
199 length = self.headers.getheader('content-length')
191 env['CONTENT_TYPE'] = self.headers.get_content_type()
192 length = self.headers.get('content-length')
200 193 if length:
201 194 env['CONTENT_LENGTH'] = length
202 195 for header in [
203 196 h
204 197 for h in self.headers.keys()
205 198 if h.lower() not in ('content-type', 'content-length')
206 199 ]:
207 200 hkey = 'HTTP_' + header.replace('-', '_').upper()
208 201 hval = self.headers.get(header)
209 202 hval = hval.replace('\n', '').strip()
210 203 if hval:
211 204 env[hkey] = hval
212 205 env['SERVER_PROTOCOL'] = self.request_version
213 206 env['wsgi.version'] = (1, 0)
214 207 env['wsgi.url_scheme'] = pycompat.sysstr(self.url_scheme)
215 208 if env.get('HTTP_EXPECT', b'').lower() == b'100-continue':
216 209 self.rfile = common.continuereader(self.rfile, self.wfile.write)
217 210
218 211 env['wsgi.input'] = self.rfile
219 212 env['wsgi.errors'] = _error_logger(self)
220 213 env['wsgi.multithread'] = isinstance(
221 214 self.server, socketserver.ThreadingMixIn
222 215 )
223 216 if util.safehasattr(socketserver, b'ForkingMixIn'):
224 217 env['wsgi.multiprocess'] = isinstance(
225 218 self.server, socketserver.ForkingMixIn
226 219 )
227 220 else:
228 221 env['wsgi.multiprocess'] = False
229 222
230 223 env['wsgi.run_once'] = 0
231 224
232 225 wsgiref.validate.check_environ(env)
233 226
234 227 self.saved_status = None
235 228 self.saved_headers = []
236 229 self.length = None
237 230 self._chunked = None
238 231 for chunk in self.server.application(env, self._start_response):
239 232 self._write(chunk)
240 233 if not self.sent_headers:
241 234 self.send_headers()
242 235 self._done()
243 236
244 237 def send_headers(self):
245 238 if not self.saved_status:
246 239 raise AssertionError(
247 240 b"Sending headers before start_response() called"
248 241 )
249 242 saved_status = self.saved_status.split(None, 1)
250 243 saved_status[0] = int(saved_status[0])
251 244 self.send_response(*saved_status)
252 245 self.length = None
253 246 self._chunked = False
254 247 for h in self.saved_headers:
255 248 self.send_header(*h)
256 249 if h[0].lower() == 'content-length':
257 250 self.length = int(h[1])
258 251 if self.length is None and saved_status[0] != common.HTTP_NOT_MODIFIED:
259 252 self._chunked = (
260 253 not self.close_connection and self.request_version == 'HTTP/1.1'
261 254 )
262 255 if self._chunked:
263 256 self.send_header('Transfer-Encoding', 'chunked')
264 257 else:
265 258 self.send_header('Connection', 'close')
266 259 self.end_headers()
267 260 self.sent_headers = True
268 261
269 262 def _start_response(self, http_status, headers, exc_info=None):
270 263 assert isinstance(http_status, str)
271 264 code, msg = http_status.split(None, 1)
272 265 code = int(code)
273 266 self.saved_status = http_status
274 267 bad_headers = ('connection', 'transfer-encoding')
275 268 self.saved_headers = [
276 269 h for h in headers if h[0].lower() not in bad_headers
277 270 ]
278 271 return self._write
279 272
280 273 def _write(self, data):
281 274 if not self.saved_status:
282 275 raise AssertionError(b"data written before start_response() called")
283 276 elif not self.sent_headers:
284 277 self.send_headers()
285 278 if self.length is not None:
286 279 if len(data) > self.length:
287 280 raise AssertionError(
288 281 b"Content-length header sent, but more "
289 282 b"bytes than specified are being written."
290 283 )
291 284 self.length = self.length - len(data)
292 285 elif self._chunked and data:
293 286 data = b'%x\r\n%s\r\n' % (len(data), data)
294 287 self.wfile.write(data)
295 288 self.wfile.flush()
296 289
297 290 def _done(self):
298 291 if self._chunked:
299 292 self.wfile.write(b'0\r\n\r\n')
300 293 self.wfile.flush()
301 294
302 295 def version_string(self):
303 296 if self.server.serverheader:
304 297 return encoding.strfromlocal(self.server.serverheader)
305 298 return httpservermod.basehttprequesthandler.version_string(self)
306 299
307 300
308 301 class _httprequesthandlerssl(_httprequesthandler):
309 302 """HTTPS handler based on Python's ssl module"""
310 303
311 304 url_scheme = b'https'
312 305
313 306 @staticmethod
314 307 def preparehttpserver(httpserver, ui):
315 308 try:
316 309 from .. import sslutil
317 310
318 311 sslutil.wrapserversocket
319 312 except ImportError:
320 313 raise error.Abort(_(b"SSL support is unavailable"))
321 314
322 315 certfile = ui.config(b'web', b'certificate')
323 316
324 317 # These config options are currently only meant for testing. Use
325 318 # at your own risk.
326 319 cafile = ui.config(b'devel', b'servercafile')
327 320 reqcert = ui.configbool(b'devel', b'serverrequirecert')
328 321
329 322 httpserver.socket = sslutil.wrapserversocket(
330 323 httpserver.socket,
331 324 ui,
332 325 certfile=certfile,
333 326 cafile=cafile,
334 327 requireclientcert=reqcert,
335 328 )
336 329
337 330 def setup(self):
338 331 self.connection = self.request
339 332 self.rfile = self.request.makefile("rb", self.rbufsize)
340 333 self.wfile = self.request.makefile("wb", self.wbufsize)
341 334
342 335
343 336 try:
344 337 import threading
345 338
346 339 threading.active_count() # silence pyflakes and bypass demandimport
347 340 _mixin = socketserver.ThreadingMixIn
348 341 except ImportError:
349 342 if util.safehasattr(os, b"fork"):
350 343 _mixin = socketserver.ForkingMixIn
351 344 else:
352 345
353 346 class _mixin(object):
354 347 pass
355 348
356 349
357 350 def openlog(opt, default):
358 351 if opt and opt != b'-':
359 352 return open(opt, b'ab')
360 353 return default
361 354
362 355
363 356 class MercurialHTTPServer(_mixin, httpservermod.httpserver, object):
364 357
365 358 # SO_REUSEADDR has broken semantics on windows
366 359 if pycompat.iswindows:
367 360 allow_reuse_address = 0
368 361
369 362 def __init__(self, ui, app, addr, handler, **kwargs):
370 363 httpservermod.httpserver.__init__(self, addr, handler, **kwargs)
371 364 self.daemon_threads = True
372 365 self.application = app
373 366
374 367 handler.preparehttpserver(self, ui)
375 368
376 369 prefix = ui.config(b'web', b'prefix')
377 370 if prefix:
378 371 prefix = b'/' + prefix.strip(b'/')
379 372 self.prefix = prefix
380 373
381 374 alog = openlog(ui.config(b'web', b'accesslog'), ui.fout)
382 375 elog = openlog(ui.config(b'web', b'errorlog'), ui.ferr)
383 376 self.accesslog = alog
384 377 self.errorlog = elog
385 378
386 379 self.addr, self.port = self.socket.getsockname()[0:2]
387 380 self.fqaddr = self.server_name
388 381
389 382 self.serverheader = ui.config(b'web', b'server-header')
390 383
391 384
392 385 class IPv6HTTPServer(MercurialHTTPServer):
393 386 address_family = getattr(socket, 'AF_INET6', None)
394 387
395 388 def __init__(self, *args, **kwargs):
396 389 if self.address_family is None:
397 390 raise error.RepoError(_(b'IPv6 is not available on this system'))
398 391 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
399 392
400 393
401 394 def create_server(ui, app):
402 395
403 396 if ui.config(b'web', b'certificate'):
404 397 handler = _httprequesthandlerssl
405 398 else:
406 399 handler = _httprequesthandler
407 400
408 401 if ui.configbool(b'web', b'ipv6'):
409 402 cls = IPv6HTTPServer
410 403 else:
411 404 cls = MercurialHTTPServer
412 405
413 406 # ugly hack due to python issue5853 (for threaded use)
414 407 try:
415 408 import mimetypes
416 409
417 410 mimetypes.init()
418 411 except UnicodeDecodeError:
419 412 # Python 2.x's mimetypes module attempts to decode strings
420 413 # from Windows' ANSI APIs as ascii (fail), then re-encode them
421 414 # as ascii (clown fail), because the default Python Unicode
422 415 # codec is hardcoded as ascii.
423 416
424 417 sys.argv # unwrap demand-loader so that reload() works
425 418 # resurrect sys.setdefaultencoding()
426 419 try:
427 420 importlib.reload(sys)
428 421 except AttributeError:
429 422 reload(sys)
430 423 oldenc = sys.getdefaultencoding()
431 424 sys.setdefaultencoding(b"latin1") # or any full 8-bit encoding
432 425 mimetypes.init()
433 426 sys.setdefaultencoding(oldenc)
434 427
435 428 address = ui.config(b'web', b'address')
436 429 port = urlutil.getport(ui.config(b'web', b'port'))
437 430 try:
438 431 return cls(ui, app, (address, port), handler)
439 432 except socket.error as inst:
440 433 raise error.Abort(
441 434 _(b"cannot start server at '%s:%d': %s")
442 435 % (address, port, encoding.strtolocal(inst.args[1]))
443 436 )
General Comments 0
You need to be logged in to leave comments. Login now