##// END OF EJS Templates
hgweb: use the parsed application path directly...
hgweb: use the parsed application path directly Previously, we assigned a custom system string with a trailing slash to wsgirequest.url. The addition of the trailing slash felt arbitrary and seems to go against how things typically work in WSGI. We also want our URLs to be bytes, not system strings. And, assigning a custom attribute to wsgirequest felt wrong. This commit fixes all those things by removing the trailing slash from the app path, changing consumers to use that variable and to use it without a trailing slash, and removing the custom attribute from wsgirequest. We preserve the trailing slash on {url}. Also, makebreadcrumb strips the trailing slash. So no change to it was needed. Differential Revision: https://phab.mercurial-scm.org/D2736

File last commit:

r36826:0031e972 default
r36826:0031e972 default
Show More
request.py
279 lines | 10.0 KiB | text/x-python | PythonLexer
# hgweb/request.py - An http request from either CGI or the standalone server.
#
# Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
# Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
from __future__ import absolute_import
import cgi
import errno
import socket
#import wsgiref.validate
from .common import (
ErrorResponse,
HTTP_NOT_MODIFIED,
statusmessage,
)
from ..thirdparty import (
attr,
)
from .. import (
pycompat,
util,
)
shortcuts = {
'cl': [('cmd', ['changelog']), ('rev', None)],
'sl': [('cmd', ['shortlog']), ('rev', None)],
'cs': [('cmd', ['changeset']), ('node', None)],
'f': [('cmd', ['file']), ('filenode', None)],
'fl': [('cmd', ['filelog']), ('filenode', None)],
'fd': [('cmd', ['filediff']), ('node', None)],
'fa': [('cmd', ['annotate']), ('filenode', None)],
'mf': [('cmd', ['manifest']), ('manifest', None)],
'ca': [('cmd', ['archive']), ('node', None)],
'tags': [('cmd', ['tags'])],
'tip': [('cmd', ['changeset']), ('node', ['tip'])],
'static': [('cmd', ['static']), ('file', None)]
}
def normalize(form):
# first expand the shortcuts
for k in shortcuts:
if k in form:
for name, value in shortcuts[k]:
if value is None:
value = form[k]
form[name] = value
del form[k]
# And strip the values
bytesform = {}
for k, v in form.iteritems():
bytesform[pycompat.bytesurl(k)] = [
pycompat.bytesurl(i.strip()) for i in v]
return bytesform
@attr.s(frozen=True)
class parsedrequest(object):
"""Represents a parsed WSGI request / static HTTP request parameters."""
# Full URL for this request.
url = attr.ib()
# URL without any path components. Just <proto>://<host><port>.
baseurl = attr.ib()
# Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
# of HTTP: Host header for hostname. This is likely what clients used.
advertisedurl = attr.ib()
advertisedbaseurl = attr.ib()
# WSGI application path.
apppath = attr.ib()
# List of path parts to be used for dispatch.
dispatchparts = attr.ib()
# URL path component (no query string) used for dispatch.
dispatchpath = attr.ib()
# Raw query string (part after "?" in URL).
querystring = attr.ib()
def parserequestfromenv(env):
"""Parse URL components from environment variables.
WSGI defines request attributes via environment variables. This function
parses the environment variables into a data structure.
"""
# PEP-0333 defines the WSGI spec and is a useful reference for this code.
# We first validate that the incoming object conforms with the WSGI spec.
# We only want to be dealing with spec-conforming WSGI implementations.
# TODO enable this once we fix internal violations.
#wsgiref.validate.check_environ(env)
# PEP-0333 states that environment keys and values are native strings
# (bytes on Python 2 and str on Python 3). The code points for the Unicode
# strings on Python 3 must be between \00000-\000FF. We deal with bytes
# in Mercurial, so mass convert string keys and values to bytes.
if pycompat.ispy3:
env = {k.encode('latin-1'): v for k, v in env.iteritems()}
env = {k: v.encode('latin-1') if isinstance(v, str) else v
for k, v in env.iteritems()}
# https://www.python.org/dev/peps/pep-0333/#environ-variables defines
# the environment variables.
# https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
# how URLs are reconstructed.
fullurl = env['wsgi.url_scheme'] + '://'
advertisedfullurl = fullurl
def addport(s):
if env['wsgi.url_scheme'] == 'https':
if env['SERVER_PORT'] != '443':
s += ':' + env['SERVER_PORT']
else:
if env['SERVER_PORT'] != '80':
s += ':' + env['SERVER_PORT']
return s
if env.get('HTTP_HOST'):
fullurl += env['HTTP_HOST']
else:
fullurl += env['SERVER_NAME']
fullurl = addport(fullurl)
advertisedfullurl += env['SERVER_NAME']
advertisedfullurl = addport(advertisedfullurl)
baseurl = fullurl
advertisedbaseurl = advertisedfullurl
fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
if env.get('QUERY_STRING'):
fullurl += '?' + env['QUERY_STRING']
advertisedfullurl += '?' + env['QUERY_STRING']
# When dispatching requests, we look at the URL components (PATH_INFO
# and QUERY_STRING) after the application root (SCRIPT_NAME). But hgwebdir
# has the concept of "virtual" repositories. This is defined via REPO_NAME.
# If REPO_NAME is defined, we append it to SCRIPT_NAME to form a new app
# root. We also exclude its path components from PATH_INFO when resolving
# the dispatch path.
apppath = env['SCRIPT_NAME']
if env.get('REPO_NAME'):
if not apppath.endswith('/'):
apppath += '/'
apppath += env.get('REPO_NAME')
if 'PATH_INFO' in env:
dispatchparts = env['PATH_INFO'].strip('/').split('/')
# Strip out repo parts.
repoparts = env.get('REPO_NAME', '').split('/')
if dispatchparts[:len(repoparts)] == repoparts:
dispatchparts = dispatchparts[len(repoparts):]
else:
dispatchparts = []
dispatchpath = '/'.join(dispatchparts)
querystring = env.get('QUERY_STRING', '')
return parsedrequest(url=fullurl, baseurl=baseurl,
advertisedurl=advertisedfullurl,
advertisedbaseurl=advertisedbaseurl,
apppath=apppath,
dispatchparts=dispatchparts, dispatchpath=dispatchpath,
querystring=querystring)
class wsgirequest(object):
"""Higher-level API for a WSGI request.
WSGI applications are invoked with 2 arguments. They are used to
instantiate instances of this class, which provides higher-level APIs
for obtaining request parameters, writing HTTP output, etc.
"""
def __init__(self, wsgienv, start_response):
version = wsgienv[r'wsgi.version']
if (version < (1, 0)) or (version >= (2, 0)):
raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
% version)
self.inp = wsgienv[r'wsgi.input']
self.err = wsgienv[r'wsgi.errors']
self.threaded = wsgienv[r'wsgi.multithread']
self.multiprocess = wsgienv[r'wsgi.multiprocess']
self.run_once = wsgienv[r'wsgi.run_once']
self.env = wsgienv
self.form = normalize(cgi.parse(self.inp,
self.env,
keep_blank_values=1))
self._start_response = start_response
self.server_write = None
self.headers = []
def __iter__(self):
return iter([])
def read(self, count=-1):
return self.inp.read(count)
def drain(self):
'''need to read all data from request, httplib is half-duplex'''
length = int(self.env.get('CONTENT_LENGTH') or 0)
for s in util.filechunkiter(self.inp, limit=length):
pass
def respond(self, status, type, filename=None, body=None):
if not isinstance(type, str):
type = pycompat.sysstr(type)
if self._start_response is not None:
self.headers.append((r'Content-Type', type))
if filename:
filename = (filename.rpartition('/')[-1]
.replace('\\', '\\\\').replace('"', '\\"'))
self.headers.append(('Content-Disposition',
'inline; filename="%s"' % filename))
if body is not None:
self.headers.append((r'Content-Length', str(len(body))))
for k, v in self.headers:
if not isinstance(v, str):
raise TypeError('header value must be string: %r' % (v,))
if isinstance(status, ErrorResponse):
self.headers.extend(status.headers)
if status.code == HTTP_NOT_MODIFIED:
# RFC 2616 Section 10.3.5: 304 Not Modified has cases where
# it MUST NOT include any headers other than these and no
# body
self.headers = [(k, v) for (k, v) in self.headers if
k in ('Date', 'ETag', 'Expires',
'Cache-Control', 'Vary')]
status = statusmessage(status.code, pycompat.bytestr(status))
elif status == 200:
status = '200 Script output follows'
elif isinstance(status, int):
status = statusmessage(status)
self.server_write = self._start_response(
pycompat.sysstr(status), self.headers)
self._start_response = None
self.headers = []
if body is not None:
self.write(body)
self.server_write = None
def write(self, thing):
if thing:
try:
self.server_write(thing)
except socket.error as inst:
if inst[0] != errno.ECONNRESET:
raise
def writelines(self, lines):
for line in lines:
self.write(line)
def flush(self):
return None
def close(self):
return None
def wsgiapplication(app_maker):
'''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
can and should now be used as a WSGI application.'''
application = app_maker()
def run_wsgi(env, respond):
return application(env, respond)
return run_wsgi