server.py
444 lines
| 13.8 KiB
| text/x-python
|
PythonLexer
|
r2391 | # hgweb/server.py - The standalone hg web server. | ||
|
r2355 | # | ||
# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net> | ||||
|
r47575 | # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com> | ||
|
r2355 | # | ||
|
r8225 | # This software may be used and distributed according to the terms of the | ||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r2355 | |||
|
r27046 | from __future__ import absolute_import | ||
import errno | ||||
|
r43339 | import importlib | ||
|
r27046 | import os | ||
import socket | ||||
import sys | ||||
import traceback | ||||
|
r36821 | import wsgiref.validate | ||
|
r27046 | |||
from ..i18n import _ | ||||
|
r43359 | from ..pycompat import ( | ||
getattr, | ||||
open, | ||||
) | ||||
|
r27046 | |||
from .. import ( | ||||
|
r34707 | encoding, | ||
|
r27046 | error, | ||
|
r30639 | pycompat, | ||
|
r27046 | util, | ||
) | ||||
r47669 | from ..utils import ( | |||
urlutil, | ||||
) | ||||
|
r27046 | |||
|
r29566 | httpservermod = util.httpserver | ||
|
r29433 | socketserver = util.socketserver | ||
|
r28883 | urlerr = util.urlerr | ||
urlreq = util.urlreq | ||||
|
r43346 | from . import common | ||
|
r2355 | |||
def _splitURI(uri): | ||||
|
r17427 | """Return path and query that has been split from uri | ||
|
r2355 | |||
Just like CGI environment, the path is unquoted, the query is | ||||
not. | ||||
""" | ||||
|
r43906 | if '?' in uri: | ||
path, query = uri.split('?', 1) | ||||
|
r2355 | else: | ||
|
r34701 | path, query = uri, r'' | ||
|
r28883 | return urlreq.unquote(path), query | ||
|
r2355 | |||
|
r43346 | |||
|
r2506 | class _error_logger(object): | ||
def __init__(self, handler): | ||||
self.handler = handler | ||||
|
r43346 | |||
|
r2506 | def flush(self): | ||
pass | ||||
|
r43346 | |||
|
r3130 | def write(self, str): | ||
|
r43347 | self.writelines(str.split(b'\n')) | ||
|
r43346 | |||
|
r3130 | def writelines(self, seq): | ||
|
r2506 | for msg in seq: | ||
|
r43809 | self.handler.log_error("HG error: %s", encoding.strfromlocal(msg)) | ||
|
r2506 | |||
|
r43346 | |||
|
r29566 | class _httprequesthandler(httpservermod.basehttprequesthandler): | ||
|
r4870 | |||
|
r43347 | url_scheme = b'http' | ||
|
r4957 | |||
|
r12783 | @staticmethod | ||
|
r29553 | def preparehttpserver(httpserver, ui): | ||
|
r12783 | """Prepare .socket of new HTTPServer instance""" | ||
|
r2355 | def __init__(self, *args, **kargs): | ||
|
r34513 | self.protocol_version = r'HTTP/1.1' | ||
|
r29566 | httpservermod.basehttprequesthandler.__init__(self, *args, **kargs) | ||
|
r2355 | |||
|
r5549 | def _log_any(self, fp, format, *args): | ||
|
r43346 | fp.write( | ||
pycompat.sysbytes( | ||||
r"%s - - [%s] %s" | ||||
% ( | ||||
self.client_address[0], | ||||
self.log_date_time_string(), | ||||
format % args, | ||||
) | ||||
) | ||||
|
r43347 | + b'\n' | ||
|
r43346 | ) | ||
|
r5549 | fp.flush() | ||
|
r2355 | def log_error(self, format, *args): | ||
|
r5549 | self._log_any(self.server.errorlog, format, *args) | ||
|
r2355 | |||
def log_message(self, format, *args): | ||||
|
r5549 | self._log_any(self.server.accesslog, format, *args) | ||
|
r2355 | |||
|
r43906 | def log_request(self, code='-', size='-'): | ||
|
r19877 | xheaders = [] | ||
|
r43347 | if util.safehasattr(self, b'headers'): | ||
|
r43346 | xheaders = [ | ||
|
r43906 | h for h in self.headers.items() if h[0].startswith('x-') | ||
|
r43346 | ] | ||
self.log_message( | ||||
|
r43906 | '"%s" %s %s%s', | ||
|
r43346 | self.requestline, | ||
str(code), | ||||
str(size), | ||||
|
r43906 | ''.join([' %s:%s' % h for h in sorted(xheaders)]), | ||
|
r43346 | ) | ||
|
r14093 | |||
|
r4860 | def do_write(self): | ||
try: | ||||
self.do_hgweb() | ||||
|
r25660 | except socket.error as inst: | ||
|
r40909 | if inst.errno != errno.EPIPE: | ||
|
r4860 | raise | ||
|
r2355 | def do_POST(self): | ||
try: | ||||
|
r4860 | self.do_write() | ||
|
r41620 | except Exception as e: | ||
|
r41603 | # I/O below could raise another exception. So log the original | ||
# exception first to ensure it is recorded. | ||||
|
r43346 | if not ( | ||
isinstance(e, (OSError, socket.error)) | ||||
and e.errno == errno.ECONNRESET | ||||
): | ||||
|
r43809 | tb = "".join(traceback.format_exception(*sys.exc_info())) | ||
|
r41620 | # We need a native-string newline to poke in the log | ||
# message, because we won't get a newline when using an | ||||
# r-string. This is the easy way out. | ||||
newline = chr(10) | ||||
|
r43346 | self.log_error( | ||
r"Exception happened during processing " | ||||
|
r43809 | "request '%s':%s%s", | ||
|
r43346 | self.path, | ||
newline, | ||||
tb, | ||||
) | ||||
|
r2355 | |||
|
r43809 | self._start_response("500 Internal Server Error", []) | ||
|
r41603 | self._write(b"Internal Server Error") | ||
self._done() | ||||
|
r37165 | def do_PUT(self): | ||
self.do_POST() | ||||
|
r2355 | def do_GET(self): | ||
self.do_POST() | ||||
def do_hgweb(self): | ||||
|
r34721 | self.sent_headers = False | ||
|
r5835 | path, query = _splitURI(self.path) | ||
|
r2355 | |||
|
r37288 | # Ensure the slicing of path below is valid | ||
|
r43346 | if path != self.server.prefix and not path.startswith( | ||
self.server.prefix + b'/' | ||||
): | ||||
self._start_response(pycompat.strurl(common.statusmessage(404)), []) | ||||
|
r43906 | if self.command == 'POST': | ||
|
r41161 | # Paranoia: tell the client we're going to close the | ||
# socket so they don't try and reuse a socket that | ||||
# might have a POST body waiting to confuse us. We do | ||||
# this by directly munging self.saved_headers because | ||||
# self._start_response ignores Connection headers. | ||||
|
r43906 | self.saved_headers = [('Connection', 'Close')] | ||
|
r38316 | self._write(b"Not Found") | ||
|
r37288 | self._done() | ||
return | ||||
|
r2355 | env = {} | ||
|
r43906 | env['GATEWAY_INTERFACE'] = 'CGI/1.1' | ||
env['REQUEST_METHOD'] = self.command | ||||
env['SERVER_NAME'] = self.server.server_name | ||||
env['SERVER_PORT'] = str(self.server.server_port) | ||||
env['REQUEST_URI'] = self.path | ||||
env['SCRIPT_NAME'] = pycompat.sysstr(self.server.prefix) | ||||
env['PATH_INFO'] = pycompat.sysstr(path[len(self.server.prefix) :]) | ||||
env['REMOTE_HOST'] = self.client_address[0] | ||||
env['REMOTE_ADDR'] = self.client_address[0] | ||||
env['QUERY_STRING'] = query or '' | ||||
|
r2355 | |||
|
r34720 | if pycompat.ispy3: | ||
if self.headers.get_content_type() is None: | ||||
|
r43906 | env['CONTENT_TYPE'] = self.headers.get_default_type() | ||
|
r34720 | else: | ||
|
r43906 | env['CONTENT_TYPE'] = self.headers.get_content_type() | ||
length = self.headers.get('content-length') | ||||
|
r34720 | else: | ||
|
r34719 | if self.headers.typeheader is None: | ||
|
r43906 | env['CONTENT_TYPE'] = self.headers.type | ||
|
r34719 | else: | ||
|
r43906 | env['CONTENT_TYPE'] = self.headers.typeheader | ||
length = self.headers.getheader('content-length') | ||||
|
r2355 | if length: | ||
|
r43906 | env['CONTENT_LENGTH'] = length | ||
|
r43346 | for header in [ | ||
h | ||||
for h in self.headers.keys() | ||||
|
r43906 | if h.lower() not in ('content-type', 'content-length') | ||
|
r43346 | ]: | ||
|
r43906 | hkey = 'HTTP_' + header.replace('-', '_').upper() | ||
|
r34513 | hval = self.headers.get(header) | ||
|
r43906 | hval = hval.replace('\n', '').strip() | ||
|
r2505 | if hval: | ||
env[hkey] = hval | ||||
|
r43906 | env['SERVER_PROTOCOL'] = self.request_version | ||
env['wsgi.version'] = (1, 0) | ||||
env['wsgi.url_scheme'] = pycompat.sysstr(self.url_scheme) | ||||
if env.get('HTTP_EXPECT', b'').lower() == b'100-continue': | ||||
|
r13570 | self.rfile = common.continuereader(self.rfile, self.wfile.write) | ||
|
r43906 | env['wsgi.input'] = self.rfile | ||
env['wsgi.errors'] = _error_logger(self) | ||||
env['wsgi.multithread'] = isinstance( | ||||
|
r43346 | self.server, socketserver.ThreadingMixIn | ||
) | ||||
|
r43347 | if util.safehasattr(socketserver, b'ForkingMixIn'): | ||
|
r43906 | env['wsgi.multiprocess'] = isinstance( | ||
|
r43346 | self.server, socketserver.ForkingMixIn | ||
) | ||||
|
r39866 | else: | ||
|
r43906 | env['wsgi.multiprocess'] = False | ||
|
r39866 | |||
|
r43906 | env['wsgi.run_once'] = 0 | ||
|
r2355 | |||
|
r36821 | wsgiref.validate.check_environ(env) | ||
|
r2506 | self.saved_status = None | ||
self.saved_headers = [] | ||||
|
r2508 | self.length = None | ||
|
r18354 | self._chunked = None | ||
|
r6784 | for chunk in self.server.application(env, self._start_response): | ||
self._write(chunk) | ||||
|
r18349 | if not self.sent_headers: | ||
self.send_headers() | ||||
|
r18354 | self._done() | ||
|
r2506 | |||
def send_headers(self): | ||||
if not self.saved_status: | ||||
|
r43346 | raise AssertionError( | ||
|
r43387 | b"Sending headers before start_response() called" | ||
|
r43346 | ) | ||
|
r2506 | saved_status = self.saved_status.split(None, 1) | ||
saved_status[0] = int(saved_status[0]) | ||||
self.send_response(*saved_status) | ||||
|
r18354 | self.length = None | ||
self._chunked = False | ||||
|
r2506 | for h in self.saved_headers: | ||
self.send_header(*h) | ||||
|
r43906 | if h[0].lower() == 'content-length': | ||
|
r2508 | self.length = int(h[1]) | ||
|
r43346 | if self.length is None and saved_status[0] != common.HTTP_NOT_MODIFIED: | ||
self._chunked = ( | ||||
|
r43906 | not self.close_connection and self.request_version == 'HTTP/1.1' | ||
|
r43346 | ) | ||
|
r18354 | if self._chunked: | ||
|
r43906 | self.send_header('Transfer-Encoding', 'chunked') | ||
|
r18354 | else: | ||
|
r43906 | self.send_header('Connection', 'close') | ||
|
r2506 | self.end_headers() | ||
self.sent_headers = True | ||||
def _start_response(self, http_status, headers, exc_info=None): | ||||
|
r38317 | assert isinstance(http_status, str) | ||
|
r2506 | code, msg = http_status.split(None, 1) | ||
code = int(code) | ||||
self.saved_status = http_status | ||||
|
r43906 | bad_headers = ('connection', 'transfer-encoding') | ||
|
r43346 | self.saved_headers = [ | ||
h for h in headers if h[0].lower() not in bad_headers | ||||
] | ||||
|
r2506 | return self._write | ||
def _write(self, data): | ||||
if not self.saved_status: | ||||
|
r43347 | raise AssertionError(b"data written before start_response() called") | ||
|
r2506 | elif not self.sent_headers: | ||
self.send_headers() | ||||
|
r2508 | if self.length is not None: | ||
if len(data) > self.length: | ||||
|
r43346 | raise AssertionError( | ||
|
r43347 | b"Content-length header sent, but more " | ||
b"bytes than specified are being written." | ||||
|
r43346 | ) | ||
|
r2508 | self.length = self.length - len(data) | ||
|
r18354 | elif self._chunked and data: | ||
|
r43347 | data = b'%x\r\n%s\r\n' % (len(data), data) | ||
|
r2506 | self.wfile.write(data) | ||
self.wfile.flush() | ||||
|
r2355 | |||
|
r18354 | def _done(self): | ||
if self._chunked: | ||||
|
r43347 | self.wfile.write(b'0\r\n\r\n') | ||
|
r18354 | self.wfile.flush() | ||
|
r37027 | def version_string(self): | ||
if self.server.serverheader: | ||||
|
r38613 | return encoding.strfromlocal(self.server.serverheader) | ||
|
r37027 | return httpservermod.basehttprequesthandler.version_string(self) | ||
|
r43346 | |||
|
r12784 | class _httprequesthandlerssl(_httprequesthandler): | ||
|
r26202 | """HTTPS handler based on Python's ssl module""" | ||
|
r12784 | |||
|
r43347 | url_scheme = b'https' | ||
|
r12784 | |||
@staticmethod | ||||
|
r29553 | def preparehttpserver(httpserver, ui): | ||
|
r12784 | try: | ||
|
r29555 | from .. import sslutil | ||
|
r43346 | |||
|
r45413 | sslutil.wrapserversocket | ||
|
r12784 | except ImportError: | ||
|
r43347 | raise error.Abort(_(b"SSL support is unavailable")) | ||
|
r29553 | |||
|
r43347 | certfile = ui.config(b'web', b'certificate') | ||
|
r29555 | |||
# These config options are currently only meant for testing. Use | ||||
# at your own risk. | ||||
|
r43347 | cafile = ui.config(b'devel', b'servercafile') | ||
reqcert = ui.configbool(b'devel', b'serverrequirecert') | ||||
|
r29555 | |||
|
r43346 | httpserver.socket = sslutil.wrapserversocket( | ||
httpserver.socket, | ||||
ui, | ||||
certfile=certfile, | ||||
cafile=cafile, | ||||
requireclientcert=reqcert, | ||||
) | ||||
|
r12784 | |||
def setup(self): | ||||
self.connection = self.request | ||||
|
r43809 | self.rfile = self.request.makefile("rb", self.rbufsize) | ||
self.wfile = self.request.makefile("wb", self.wbufsize) | ||||
|
r12784 | |||
|
r43346 | |||
|
r10639 | try: | ||
|
r27046 | import threading | ||
|
r43346 | |||
|
r47965 | threading.active_count() # silence pyflakes and bypass demandimport | ||
|
r29433 | _mixin = socketserver.ThreadingMixIn | ||
|
r10639 | except ImportError: | ||
|
r43347 | if util.safehasattr(os, b"fork"): | ||
|
r29433 | _mixin = socketserver.ForkingMixIn | ||
|
r10639 | else: | ||
|
r43346 | |||
|
r14764 | class _mixin(object): | ||
|
r10639 | pass | ||
|
r43346 | |||
|
r10643 | def openlog(opt, default): | ||
|
r43347 | if opt and opt != b'-': | ||
return open(opt, b'ab') | ||||
|
r10643 | return default | ||
|
r43346 | |||
|
r30082 | class MercurialHTTPServer(_mixin, httpservermod.httpserver, object): | ||
|
r10643 | |||
# SO_REUSEADDR has broken semantics on windows | ||||
|
r34646 | if pycompat.iswindows: | ||
|
r10643 | allow_reuse_address = 0 | ||
def __init__(self, ui, app, addr, handler, **kwargs): | ||||
|
r29566 | httpservermod.httpserver.__init__(self, addr, handler, **kwargs) | ||
|
r10643 | self.daemon_threads = True | ||
self.application = app | ||||
|
r2355 | |||
|
r29553 | handler.preparehttpserver(self, ui) | ||
|
r10643 | |||
|
r43347 | prefix = ui.config(b'web', b'prefix') | ||
|
r10643 | if prefix: | ||
|
r43347 | prefix = b'/' + prefix.strip(b'/') | ||
|
r10643 | self.prefix = prefix | ||
|
r43347 | alog = openlog(ui.config(b'web', b'accesslog'), ui.fout) | ||
elog = openlog(ui.config(b'web', b'errorlog'), ui.ferr) | ||||
|
r10643 | self.accesslog = alog | ||
self.errorlog = elog | ||||
self.addr, self.port = self.socket.getsockname()[0:2] | ||||
|
r45578 | self.fqaddr = self.server_name | ||
|
r10643 | |||
|
r43347 | self.serverheader = ui.config(b'web', b'server-header') | ||
|
r37027 | |||
|
r43346 | |||
|
r10643 | class IPv6HTTPServer(MercurialHTTPServer): | ||
address_family = getattr(socket, 'AF_INET6', None) | ||||
|
r43346 | |||
|
r10643 | def __init__(self, *args, **kwargs): | ||
if self.address_family is None: | ||||
|
r43347 | raise error.RepoError(_(b'IPv6 is not available on this system')) | ||
|
r10643 | super(IPv6HTTPServer, self).__init__(*args, **kwargs) | ||
|
r43346 | |||
|
r10644 | def create_server(ui, app): | ||
|
r2355 | |||
|
r43347 | if ui.config(b'web', b'certificate'): | ||
|
r26848 | handler = _httprequesthandlerssl | ||
|
r4860 | else: | ||
|
r12783 | handler = _httprequesthandler | ||
|
r4860 | |||
|
r43347 | if ui.configbool(b'web', b'ipv6'): | ||
|
r10641 | cls = IPv6HTTPServer | ||
else: | ||||
cls = MercurialHTTPServer | ||||
|
r8224 | # ugly hack due to python issue5853 (for threaded use) | ||
|
r20357 | try: | ||
import mimetypes | ||||
|
r43346 | |||
|
r20357 | mimetypes.init() | ||
except UnicodeDecodeError: | ||||
# Python 2.x's mimetypes module attempts to decode strings | ||||
# from Windows' ANSI APIs as ascii (fail), then re-encode them | ||||
# as ascii (clown fail), because the default Python Unicode | ||||
# codec is hardcoded as ascii. | ||||
|
r43346 | sys.argv # unwrap demand-loader so that reload() works | ||
|
r43339 | # resurrect sys.setdefaultencoding() | ||
try: | ||||
importlib.reload(sys) | ||||
except AttributeError: | ||||
reload(sys) | ||||
|
r20357 | oldenc = sys.getdefaultencoding() | ||
|
r43347 | sys.setdefaultencoding(b"latin1") # or any full 8-bit encoding | ||
|
r20357 | mimetypes.init() | ||
sys.setdefaultencoding(oldenc) | ||||
|
r8224 | |||
|
r43347 | address = ui.config(b'web', b'address') | ||
r47669 | port = urlutil.getport(ui.config(b'web', b'port')) | |||
|
r3628 | try: | ||
|
r10644 | return cls(ui, app, (address, port), handler) | ||
|
r25660 | except socket.error as inst: | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b"cannot start server at '%s:%d': %s") | ||
|
r43346 | % (address, port, encoding.strtolocal(inst.args[1])) | ||
) | ||||