mercurial/hgweb/hgweb_mod.py
changeset 36804 b9b968e21f78
parent 36802 7fc80c982656
child 36805 ec46415ed826
equal deleted inserted replaced
36803:8e1556ac01bb 36804:b9b968e21f78
    20     HTTP_SERVER_ERROR,
    20     HTTP_SERVER_ERROR,
    21     caching,
    21     caching,
    22     cspvalues,
    22     cspvalues,
    23     permhooks,
    23     permhooks,
    24 )
    24 )
    25 from .request import wsgirequest
       
    26 
    25 
    27 from .. import (
    26 from .. import (
    28     encoding,
    27     encoding,
    29     error,
    28     error,
    30     formatter,
    29     formatter,
    39     util,
    38     util,
    40     wireprotoserver,
    39     wireprotoserver,
    41 )
    40 )
    42 
    41 
    43 from . import (
    42 from . import (
       
    43     request as requestmod,
    44     webcommands,
    44     webcommands,
    45     webutil,
    45     webutil,
    46     wsgicgi,
    46     wsgicgi,
    47 )
    47 )
    48 
    48 
   140         allowed = self.configlist('web', 'allow_archive')
   140         allowed = self.configlist('web', 'allow_archive')
   141         for typ, spec in self.archivespecs.iteritems():
   141         for typ, spec in self.archivespecs.iteritems():
   142             if typ in allowed or self.configbool('web', 'allow%s' % typ):
   142             if typ in allowed or self.configbool('web', 'allow%s' % typ):
   143                 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
   143                 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
   144 
   144 
   145     def templater(self, req):
   145     def templater(self, wsgireq):
   146         # determine scheme, port and server name
   146         # determine scheme, port and server name
   147         # this is needed to create absolute urls
   147         # this is needed to create absolute urls
   148 
   148 
   149         proto = req.env.get('wsgi.url_scheme')
   149         proto = wsgireq.env.get('wsgi.url_scheme')
   150         if proto == 'https':
   150         if proto == 'https':
   151             proto = 'https'
   151             proto = 'https'
   152             default_port = '443'
   152             default_port = '443'
   153         else:
   153         else:
   154             proto = 'http'
   154             proto = 'http'
   155             default_port = '80'
   155             default_port = '80'
   156 
   156 
   157         port = req.env[r'SERVER_PORT']
   157         port = wsgireq.env[r'SERVER_PORT']
   158         port = port != default_port and (r':' + port) or r''
   158         port = port != default_port and (r':' + port) or r''
   159         urlbase = r'%s://%s%s' % (proto, req.env[r'SERVER_NAME'], port)
   159         urlbase = r'%s://%s%s' % (proto, wsgireq.env[r'SERVER_NAME'], port)
   160         logourl = self.config('web', 'logourl')
   160         logourl = self.config('web', 'logourl')
   161         logoimg = self.config('web', 'logoimg')
   161         logoimg = self.config('web', 'logoimg')
   162         staticurl = (self.config('web', 'staticurl')
   162         staticurl = (self.config('web', 'staticurl')
   163                      or pycompat.sysbytes(req.url) + 'static/')
   163                      or pycompat.sysbytes(wsgireq.url) + 'static/')
   164         if not staticurl.endswith('/'):
   164         if not staticurl.endswith('/'):
   165             staticurl += '/'
   165             staticurl += '/'
   166 
   166 
   167         # some functions for the templater
   167         # some functions for the templater
   168 
   168 
   170             yield self.config('web', 'motd')
   170             yield self.config('web', 'motd')
   171 
   171 
   172         # figure out which style to use
   172         # figure out which style to use
   173 
   173 
   174         vars = {}
   174         vars = {}
   175         styles, (style, mapfile) = getstyle(req, self.config,
   175         styles, (style, mapfile) = getstyle(wsgireq, self.config,
   176                                             self.templatepath)
   176                                             self.templatepath)
   177         if style == styles[0]:
   177         if style == styles[0]:
   178             vars['style'] = style
   178             vars['style'] = style
   179 
   179 
   180         start = '&' if req.url[-1] == r'?' else '?'
   180         start = '&' if wsgireq.url[-1] == r'?' else '?'
   181         sessionvars = webutil.sessionvars(vars, start)
   181         sessionvars = webutil.sessionvars(vars, start)
   182 
   182 
   183         if not self.reponame:
   183         if not self.reponame:
   184             self.reponame = (self.config('web', 'name', '')
   184             self.reponame = (self.config('web', 'name', '')
   185                              or req.env.get('REPO_NAME')
   185                              or wsgireq.env.get('REPO_NAME')
   186                              or req.url.strip(r'/') or self.repo.root)
   186                              or wsgireq.url.strip(r'/') or self.repo.root)
   187 
   187 
   188         def websubfilter(text):
   188         def websubfilter(text):
   189             return templatefilters.websub(text, self.websubtable)
   189             return templatefilters.websub(text, self.websubtable)
   190 
   190 
   191         # create the templater
   191         # create the templater
   192         # TODO: export all keywords: defaults = templatekw.keywords.copy()
   192         # TODO: export all keywords: defaults = templatekw.keywords.copy()
   193         defaults = {
   193         defaults = {
   194             'url': pycompat.sysbytes(req.url),
   194             'url': pycompat.sysbytes(wsgireq.url),
   195             'logourl': logourl,
   195             'logourl': logourl,
   196             'logoimg': logoimg,
   196             'logoimg': logoimg,
   197             'staticurl': staticurl,
   197             'staticurl': staticurl,
   198             'urlbase': urlbase,
   198             'urlbase': urlbase,
   199             'repo': self.reponame,
   199             'repo': self.reponame,
   200             'encoding': encoding.encoding,
   200             'encoding': encoding.encoding,
   201             'motd': motd,
   201             'motd': motd,
   202             'sessionvars': sessionvars,
   202             'sessionvars': sessionvars,
   203             'pathdef': makebreadcrumb(pycompat.sysbytes(req.url)),
   203             'pathdef': makebreadcrumb(pycompat.sysbytes(wsgireq.url)),
   204             'style': style,
   204             'style': style,
   205             'nonce': self.nonce,
   205             'nonce': self.nonce,
   206         }
   206         }
   207         tres = formatter.templateresources(self.repo.ui, self.repo)
   207         tres = formatter.templateresources(self.repo.ui, self.repo)
   208         tmpl = templater.templater.frommapfile(mapfile,
   208         tmpl = templater.templater.frommapfile(mapfile,
   299     def __call__(self, env, respond):
   299     def __call__(self, env, respond):
   300         """Run the WSGI application.
   300         """Run the WSGI application.
   301 
   301 
   302         This may be called by multiple threads.
   302         This may be called by multiple threads.
   303         """
   303         """
   304         req = wsgirequest(env, respond)
   304         req = requestmod.wsgirequest(env, respond)
   305         return self.run_wsgi(req)
   305         return self.run_wsgi(req)
   306 
   306 
   307     def run_wsgi(self, req):
   307     def run_wsgi(self, wsgireq):
   308         """Internal method to run the WSGI application.
   308         """Internal method to run the WSGI application.
   309 
   309 
   310         This is typically only called by Mercurial. External consumers
   310         This is typically only called by Mercurial. External consumers
   311         should be using instances of this class as the WSGI application.
   311         should be using instances of this class as the WSGI application.
   312         """
   312         """
   313         with self._obtainrepo() as repo:
   313         with self._obtainrepo() as repo:
   314             profile = repo.ui.configbool('profiling', 'enabled')
   314             profile = repo.ui.configbool('profiling', 'enabled')
   315             with profiling.profile(repo.ui, enabled=profile):
   315             with profiling.profile(repo.ui, enabled=profile):
   316                 for r in self._runwsgi(req, repo):
   316                 for r in self._runwsgi(wsgireq, repo):
   317                     yield r
   317                     yield r
   318 
   318 
   319     def _runwsgi(self, req, repo):
   319     def _runwsgi(self, wsgireq, repo):
   320         rctx = requestcontext(self, repo)
   320         rctx = requestcontext(self, repo)
   321 
   321 
   322         # This state is global across all threads.
   322         # This state is global across all threads.
   323         encoding.encoding = rctx.config('web', 'encoding')
   323         encoding.encoding = rctx.config('web', 'encoding')
   324         rctx.repo.ui.environ = req.env
   324         rctx.repo.ui.environ = wsgireq.env
   325 
   325 
   326         if rctx.csp:
   326         if rctx.csp:
   327             # hgwebdir may have added CSP header. Since we generate our own,
   327             # hgwebdir may have added CSP header. Since we generate our own,
   328             # replace it.
   328             # replace it.
   329             req.headers = [h for h in req.headers
   329             wsgireq.headers = [h for h in wsgireq.headers
   330                            if h[0] != 'Content-Security-Policy']
   330                                if h[0] != 'Content-Security-Policy']
   331             req.headers.append(('Content-Security-Policy', rctx.csp))
   331             wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
   332 
   332 
   333         # work with CGI variables to create coherent structure
   333         # work with CGI variables to create coherent structure
   334         # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
   334         # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
   335 
   335 
   336         req.url = req.env[r'SCRIPT_NAME']
   336         wsgireq.url = wsgireq.env[r'SCRIPT_NAME']
   337         if not req.url.endswith(r'/'):
   337         if not wsgireq.url.endswith(r'/'):
   338             req.url += r'/'
   338             wsgireq.url += r'/'
   339         if req.env.get('REPO_NAME'):
   339         if wsgireq.env.get('REPO_NAME'):
   340             req.url += req.env[r'REPO_NAME'] + r'/'
   340             wsgireq.url += wsgireq.env[r'REPO_NAME'] + r'/'
   341 
   341 
   342         if r'PATH_INFO' in req.env:
   342         if r'PATH_INFO' in wsgireq.env:
   343             parts = req.env[r'PATH_INFO'].strip(r'/').split(r'/')
   343             parts = wsgireq.env[r'PATH_INFO'].strip(r'/').split(r'/')
   344             repo_parts = req.env.get(r'REPO_NAME', r'').split(r'/')
   344             repo_parts = wsgireq.env.get(r'REPO_NAME', r'').split(r'/')
   345             if parts[:len(repo_parts)] == repo_parts:
   345             if parts[:len(repo_parts)] == repo_parts:
   346                 parts = parts[len(repo_parts):]
   346                 parts = parts[len(repo_parts):]
   347             query = r'/'.join(parts)
   347             query = r'/'.join(parts)
   348         else:
   348         else:
   349             query = req.env[r'QUERY_STRING'].partition(r'&')[0]
   349             query = wsgireq.env[r'QUERY_STRING'].partition(r'&')[0]
   350             query = query.partition(r';')[0]
   350             query = query.partition(r';')[0]
   351 
   351 
   352         # Route it to a wire protocol handler if it looks like a wire protocol
   352         # Route it to a wire protocol handler if it looks like a wire protocol
   353         # request.
   353         # request.
   354         protohandler = wireprotoserver.parsehttprequest(rctx, req, query,
   354         protohandler = wireprotoserver.parsehttprequest(rctx, wsgireq, query,
   355                                                         self.check_perm)
   355                                                         self.check_perm)
   356 
   356 
   357         if protohandler:
   357         if protohandler:
   358             try:
   358             try:
   359                 if query:
   359                 if query:
   364                 return protohandler['handleerror'](inst)
   364                 return protohandler['handleerror'](inst)
   365 
   365 
   366         # translate user-visible url structure to internal structure
   366         # translate user-visible url structure to internal structure
   367 
   367 
   368         args = query.split(r'/', 2)
   368         args = query.split(r'/', 2)
   369         if 'cmd' not in req.form and args and args[0]:
   369         if 'cmd' not in wsgireq.form and args and args[0]:
   370             cmd = args.pop(0)
   370             cmd = args.pop(0)
   371             style = cmd.rfind('-')
   371             style = cmd.rfind('-')
   372             if style != -1:
   372             if style != -1:
   373                 req.form['style'] = [cmd[:style]]
   373                 wsgireq.form['style'] = [cmd[:style]]
   374                 cmd = cmd[style + 1:]
   374                 cmd = cmd[style + 1:]
   375 
   375 
   376             # avoid accepting e.g. style parameter as command
   376             # avoid accepting e.g. style parameter as command
   377             if util.safehasattr(webcommands, cmd):
   377             if util.safehasattr(webcommands, cmd):
   378                 req.form['cmd'] = [cmd]
   378                 wsgireq.form['cmd'] = [cmd]
   379 
   379 
   380             if cmd == 'static':
   380             if cmd == 'static':
   381                 req.form['file'] = ['/'.join(args)]
   381                 wsgireq.form['file'] = ['/'.join(args)]
   382             else:
   382             else:
   383                 if args and args[0]:
   383                 if args and args[0]:
   384                     node = args.pop(0).replace('%2F', '/')
   384                     node = args.pop(0).replace('%2F', '/')
   385                     req.form['node'] = [node]
   385                     wsgireq.form['node'] = [node]
   386                 if args:
   386                 if args:
   387                     req.form['file'] = args
   387                     wsgireq.form['file'] = args
   388 
   388 
   389             ua = req.env.get('HTTP_USER_AGENT', '')
   389             ua = wsgireq.env.get('HTTP_USER_AGENT', '')
   390             if cmd == 'rev' and 'mercurial' in ua:
   390             if cmd == 'rev' and 'mercurial' in ua:
   391                 req.form['style'] = ['raw']
   391                 wsgireq.form['style'] = ['raw']
   392 
   392 
   393             if cmd == 'archive':
   393             if cmd == 'archive':
   394                 fn = req.form['node'][0]
   394                 fn = wsgireq.form['node'][0]
   395                 for type_, spec in rctx.archivespecs.iteritems():
   395                 for type_, spec in rctx.archivespecs.iteritems():
   396                     ext = spec[2]
   396                     ext = spec[2]
   397                     if fn.endswith(ext):
   397                     if fn.endswith(ext):
   398                         req.form['node'] = [fn[:-len(ext)]]
   398                         wsgireq.form['node'] = [fn[:-len(ext)]]
   399                         req.form['type'] = [type_]
   399                         wsgireq.form['type'] = [type_]
   400         else:
   400         else:
   401             cmd = req.form.get('cmd', [''])[0]
   401             cmd = wsgireq.form.get('cmd', [''])[0]
   402 
   402 
   403         # process the web interface request
   403         # process the web interface request
   404 
   404 
   405         try:
   405         try:
   406             tmpl = rctx.templater(req)
   406             tmpl = rctx.templater(wsgireq)
   407             ctype = tmpl('mimetype', encoding=encoding.encoding)
   407             ctype = tmpl('mimetype', encoding=encoding.encoding)
   408             ctype = templater.stringify(ctype)
   408             ctype = templater.stringify(ctype)
   409 
   409 
   410             # check read permissions non-static content
   410             # check read permissions non-static content
   411             if cmd != 'static':
   411             if cmd != 'static':
   412                 self.check_perm(rctx, req, None)
   412                 self.check_perm(rctx, wsgireq, None)
   413 
   413 
   414             if cmd == '':
   414             if cmd == '':
   415                 req.form['cmd'] = [tmpl.cache['default']]
   415                 wsgireq.form['cmd'] = [tmpl.cache['default']]
   416                 cmd = req.form['cmd'][0]
   416                 cmd = wsgireq.form['cmd'][0]
   417 
   417 
   418             # Don't enable caching if using a CSP nonce because then it wouldn't
   418             # Don't enable caching if using a CSP nonce because then it wouldn't
   419             # be a nonce.
   419             # be a nonce.
   420             if rctx.configbool('web', 'cache') and not rctx.nonce:
   420             if rctx.configbool('web', 'cache') and not rctx.nonce:
   421                 caching(self, req) # sets ETag header or raises NOT_MODIFIED
   421                 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
   422             if cmd not in webcommands.__all__:
   422             if cmd not in webcommands.__all__:
   423                 msg = 'no such method: %s' % cmd
   423                 msg = 'no such method: %s' % cmd
   424                 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
   424                 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
   425             elif cmd == 'file' and 'raw' in req.form.get('style', []):
   425             elif cmd == 'file' and 'raw' in wsgireq.form.get('style', []):
   426                 rctx.ctype = ctype
   426                 rctx.ctype = ctype
   427                 content = webcommands.rawfile(rctx, req, tmpl)
   427                 content = webcommands.rawfile(rctx, wsgireq, tmpl)
   428             else:
   428             else:
   429                 content = getattr(webcommands, cmd)(rctx, req, tmpl)
   429                 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
   430                 req.respond(HTTP_OK, ctype)
   430                 wsgireq.respond(HTTP_OK, ctype)
   431 
   431 
   432             return content
   432             return content
   433 
   433 
   434         except (error.LookupError, error.RepoLookupError) as err:
   434         except (error.LookupError, error.RepoLookupError) as err:
   435             req.respond(HTTP_NOT_FOUND, ctype)
   435             wsgireq.respond(HTTP_NOT_FOUND, ctype)
   436             msg = pycompat.bytestr(err)
   436             msg = pycompat.bytestr(err)
   437             if (util.safehasattr(err, 'name') and
   437             if (util.safehasattr(err, 'name') and
   438                 not isinstance(err,  error.ManifestLookupError)):
   438                 not isinstance(err,  error.ManifestLookupError)):
   439                 msg = 'revision not found: %s' % err.name
   439                 msg = 'revision not found: %s' % err.name
   440             return tmpl('error', error=msg)
   440             return tmpl('error', error=msg)
   441         except (error.RepoError, error.RevlogError) as inst:
   441         except (error.RepoError, error.RevlogError) as inst:
   442             req.respond(HTTP_SERVER_ERROR, ctype)
   442             wsgireq.respond(HTTP_SERVER_ERROR, ctype)
   443             return tmpl('error', error=pycompat.bytestr(inst))
   443             return tmpl('error', error=pycompat.bytestr(inst))
   444         except ErrorResponse as inst:
   444         except ErrorResponse as inst:
   445             req.respond(inst, ctype)
   445             wsgireq.respond(inst, ctype)
   446             if inst.code == HTTP_NOT_MODIFIED:
   446             if inst.code == HTTP_NOT_MODIFIED:
   447                 # Not allowed to return a body on a 304
   447                 # Not allowed to return a body on a 304
   448                 return ['']
   448                 return ['']
   449             return tmpl('error', error=pycompat.bytestr(inst))
   449             return tmpl('error', error=pycompat.bytestr(inst))
   450 
   450