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