mercurial/hgweb/server.py
changeset 43076 2372284d9457
parent 43069 e554cfd93975
child 43077 687b865b95ad
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
    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         )