##// END OF EJS Templates
hgweb: parse and store HTTP request headers...
Gregory Szorc -
r36832:f9078c6c default
parent child Browse files
Show More
@@ -1,438 +1,438 b''
1 1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import contextlib
12 12 import os
13 13
14 14 from .common import (
15 15 ErrorResponse,
16 16 HTTP_BAD_REQUEST,
17 17 HTTP_NOT_FOUND,
18 18 HTTP_NOT_MODIFIED,
19 19 HTTP_OK,
20 20 HTTP_SERVER_ERROR,
21 21 caching,
22 22 cspvalues,
23 23 permhooks,
24 24 )
25 25
26 26 from .. import (
27 27 encoding,
28 28 error,
29 29 formatter,
30 30 hg,
31 31 hook,
32 32 profiling,
33 33 pycompat,
34 34 repoview,
35 35 templatefilters,
36 36 templater,
37 37 ui as uimod,
38 38 util,
39 39 wireprotoserver,
40 40 )
41 41
42 42 from . import (
43 43 request as requestmod,
44 44 webcommands,
45 45 webutil,
46 46 wsgicgi,
47 47 )
48 48
49 49 archivespecs = util.sortdict((
50 50 ('zip', ('application/zip', 'zip', '.zip', None)),
51 51 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
52 52 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
53 53 ))
54 54
55 55 def getstyle(req, configfn, templatepath):
56 56 fromreq = req.form.get('style', [None])[0]
57 57 styles = (
58 58 fromreq,
59 59 configfn('web', 'style'),
60 60 'paper',
61 61 )
62 62 return styles, templater.stylemap(styles, templatepath)
63 63
64 64 def makebreadcrumb(url, prefix=''):
65 65 '''Return a 'URL breadcrumb' list
66 66
67 67 A 'URL breadcrumb' is a list of URL-name pairs,
68 68 corresponding to each of the path items on a URL.
69 69 This can be used to create path navigation entries.
70 70 '''
71 71 if url.endswith('/'):
72 72 url = url[:-1]
73 73 if prefix:
74 74 url = '/' + prefix + url
75 75 relpath = url
76 76 if relpath.startswith('/'):
77 77 relpath = relpath[1:]
78 78
79 79 breadcrumb = []
80 80 urlel = url
81 81 pathitems = [''] + relpath.split('/')
82 82 for pathel in reversed(pathitems):
83 83 if not pathel or not urlel:
84 84 break
85 85 breadcrumb.append({'url': urlel, 'name': pathel})
86 86 urlel = os.path.dirname(urlel)
87 87 return reversed(breadcrumb)
88 88
89 89 class requestcontext(object):
90 90 """Holds state/context for an individual request.
91 91
92 92 Servers can be multi-threaded. Holding state on the WSGI application
93 93 is prone to race conditions. Instances of this class exist to hold
94 94 mutable and race-free state for requests.
95 95 """
96 96 def __init__(self, app, repo):
97 97 self.repo = repo
98 98 self.reponame = app.reponame
99 99
100 100 self.archivespecs = archivespecs
101 101
102 102 self.maxchanges = self.configint('web', 'maxchanges')
103 103 self.stripecount = self.configint('web', 'stripes')
104 104 self.maxshortchanges = self.configint('web', 'maxshortchanges')
105 105 self.maxfiles = self.configint('web', 'maxfiles')
106 106 self.allowpull = self.configbool('web', 'allow-pull')
107 107
108 108 # we use untrusted=False to prevent a repo owner from using
109 109 # web.templates in .hg/hgrc to get access to any file readable
110 110 # by the user running the CGI script
111 111 self.templatepath = self.config('web', 'templates', untrusted=False)
112 112
113 113 # This object is more expensive to build than simple config values.
114 114 # It is shared across requests. The app will replace the object
115 115 # if it is updated. Since this is a reference and nothing should
116 116 # modify the underlying object, it should be constant for the lifetime
117 117 # of the request.
118 118 self.websubtable = app.websubtable
119 119
120 120 self.csp, self.nonce = cspvalues(self.repo.ui)
121 121
122 122 # Trust the settings from the .hg/hgrc files by default.
123 123 def config(self, section, name, default=uimod._unset, untrusted=True):
124 124 return self.repo.ui.config(section, name, default,
125 125 untrusted=untrusted)
126 126
127 127 def configbool(self, section, name, default=uimod._unset, untrusted=True):
128 128 return self.repo.ui.configbool(section, name, default,
129 129 untrusted=untrusted)
130 130
131 131 def configint(self, section, name, default=uimod._unset, untrusted=True):
132 132 return self.repo.ui.configint(section, name, default,
133 133 untrusted=untrusted)
134 134
135 135 def configlist(self, section, name, default=uimod._unset, untrusted=True):
136 136 return self.repo.ui.configlist(section, name, default,
137 137 untrusted=untrusted)
138 138
139 139 def archivelist(self, nodeid):
140 140 allowed = self.configlist('web', 'allow_archive')
141 141 for typ, spec in self.archivespecs.iteritems():
142 142 if typ in allowed or self.configbool('web', 'allow%s' % typ):
143 143 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
144 144
145 145 def templater(self, wsgireq, req):
146 146 # determine scheme, port and server name
147 147 # this is needed to create absolute urls
148 148 logourl = self.config('web', 'logourl')
149 149 logoimg = self.config('web', 'logoimg')
150 150 staticurl = (self.config('web', 'staticurl')
151 151 or req.apppath + '/static/')
152 152 if not staticurl.endswith('/'):
153 153 staticurl += '/'
154 154
155 155 # some functions for the templater
156 156
157 157 def motd(**map):
158 158 yield self.config('web', 'motd')
159 159
160 160 # figure out which style to use
161 161
162 162 vars = {}
163 163 styles, (style, mapfile) = getstyle(wsgireq, self.config,
164 164 self.templatepath)
165 165 if style == styles[0]:
166 166 vars['style'] = style
167 167
168 168 sessionvars = webutil.sessionvars(vars, '?')
169 169
170 170 if not self.reponame:
171 171 self.reponame = (self.config('web', 'name', '')
172 172 or wsgireq.env.get('REPO_NAME')
173 173 or req.apppath or self.repo.root)
174 174
175 175 def websubfilter(text):
176 176 return templatefilters.websub(text, self.websubtable)
177 177
178 178 # create the templater
179 179 # TODO: export all keywords: defaults = templatekw.keywords.copy()
180 180 defaults = {
181 181 'url': req.apppath + '/',
182 182 'logourl': logourl,
183 183 'logoimg': logoimg,
184 184 'staticurl': staticurl,
185 185 'urlbase': req.advertisedbaseurl,
186 186 'repo': self.reponame,
187 187 'encoding': encoding.encoding,
188 188 'motd': motd,
189 189 'sessionvars': sessionvars,
190 190 'pathdef': makebreadcrumb(req.apppath),
191 191 'style': style,
192 192 'nonce': self.nonce,
193 193 }
194 194 tres = formatter.templateresources(self.repo.ui, self.repo)
195 195 tmpl = templater.templater.frommapfile(mapfile,
196 196 filters={'websub': websubfilter},
197 197 defaults=defaults,
198 198 resources=tres)
199 199 return tmpl
200 200
201 201
202 202 class hgweb(object):
203 203 """HTTP server for individual repositories.
204 204
205 205 Instances of this class serve HTTP responses for a particular
206 206 repository.
207 207
208 208 Instances are typically used as WSGI applications.
209 209
210 210 Some servers are multi-threaded. On these servers, there may
211 211 be multiple active threads inside __call__.
212 212 """
213 213 def __init__(self, repo, name=None, baseui=None):
214 214 if isinstance(repo, str):
215 215 if baseui:
216 216 u = baseui.copy()
217 217 else:
218 218 u = uimod.ui.load()
219 219 r = hg.repository(u, repo)
220 220 else:
221 221 # we trust caller to give us a private copy
222 222 r = repo
223 223
224 224 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
225 225 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
226 226 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
227 227 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
228 228 # resolve file patterns relative to repo root
229 229 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
230 230 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
231 231 # displaying bundling progress bar while serving feel wrong and may
232 232 # break some wsgi implementation.
233 233 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
234 234 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
235 235 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
236 236 self._lastrepo = self._repos[0]
237 237 hook.redirect(True)
238 238 self.reponame = name
239 239
240 240 def _webifyrepo(self, repo):
241 241 repo = getwebview(repo)
242 242 self.websubtable = webutil.getwebsubs(repo)
243 243 return repo
244 244
245 245 @contextlib.contextmanager
246 246 def _obtainrepo(self):
247 247 """Obtain a repo unique to the caller.
248 248
249 249 Internally we maintain a stack of cachedlocalrepo instances
250 250 to be handed out. If one is available, we pop it and return it,
251 251 ensuring it is up to date in the process. If one is not available,
252 252 we clone the most recently used repo instance and return it.
253 253
254 254 It is currently possible for the stack to grow without bounds
255 255 if the server allows infinite threads. However, servers should
256 256 have a thread limit, thus establishing our limit.
257 257 """
258 258 if self._repos:
259 259 cached = self._repos.pop()
260 260 r, created = cached.fetch()
261 261 else:
262 262 cached = self._lastrepo.copy()
263 263 r, created = cached.fetch()
264 264 if created:
265 265 r = self._webifyrepo(r)
266 266
267 267 self._lastrepo = cached
268 268 self.mtime = cached.mtime
269 269 try:
270 270 yield r
271 271 finally:
272 272 self._repos.append(cached)
273 273
274 274 def run(self):
275 275 """Start a server from CGI environment.
276 276
277 277 Modern servers should be using WSGI and should avoid this
278 278 method, if possible.
279 279 """
280 280 if not encoding.environ.get('GATEWAY_INTERFACE',
281 281 '').startswith("CGI/1."):
282 282 raise RuntimeError("This function is only intended to be "
283 283 "called while running as a CGI script.")
284 284 wsgicgi.launch(self)
285 285
286 286 def __call__(self, env, respond):
287 287 """Run the WSGI application.
288 288
289 289 This may be called by multiple threads.
290 290 """
291 291 req = requestmod.wsgirequest(env, respond)
292 292 return self.run_wsgi(req)
293 293
294 294 def run_wsgi(self, wsgireq):
295 295 """Internal method to run the WSGI application.
296 296
297 297 This is typically only called by Mercurial. External consumers
298 298 should be using instances of this class as the WSGI application.
299 299 """
300 300 with self._obtainrepo() as repo:
301 301 profile = repo.ui.configbool('profiling', 'enabled')
302 302 with profiling.profile(repo.ui, enabled=profile):
303 303 for r in self._runwsgi(wsgireq, repo):
304 304 yield r
305 305
306 306 def _runwsgi(self, wsgireq, repo):
307 307 req = requestmod.parserequestfromenv(wsgireq.env)
308 308 rctx = requestcontext(self, repo)
309 309
310 310 # This state is global across all threads.
311 311 encoding.encoding = rctx.config('web', 'encoding')
312 312 rctx.repo.ui.environ = wsgireq.env
313 313
314 314 if rctx.csp:
315 315 # hgwebdir may have added CSP header. Since we generate our own,
316 316 # replace it.
317 317 wsgireq.headers = [h for h in wsgireq.headers
318 318 if h[0] != 'Content-Security-Policy']
319 319 wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
320 320
321 321 handled, res = wireprotoserver.handlewsgirequest(
322 322 rctx, wsgireq, req, self.check_perm)
323 323 if handled:
324 324 return res
325 325
326 326 if req.havepathinfo:
327 327 query = req.dispatchpath
328 328 else:
329 329 query = req.querystring.partition('&')[0].partition(';')[0]
330 330
331 331 # translate user-visible url structure to internal structure
332 332
333 333 args = query.split('/', 2)
334 334 if 'cmd' not in wsgireq.form and args and args[0]:
335 335 cmd = args.pop(0)
336 336 style = cmd.rfind('-')
337 337 if style != -1:
338 338 wsgireq.form['style'] = [cmd[:style]]
339 339 cmd = cmd[style + 1:]
340 340
341 341 # avoid accepting e.g. style parameter as command
342 342 if util.safehasattr(webcommands, cmd):
343 343 wsgireq.form['cmd'] = [cmd]
344 344
345 345 if cmd == 'static':
346 346 wsgireq.form['file'] = ['/'.join(args)]
347 347 else:
348 348 if args and args[0]:
349 349 node = args.pop(0).replace('%2F', '/')
350 350 wsgireq.form['node'] = [node]
351 351 if args:
352 352 wsgireq.form['file'] = args
353 353
354 ua = wsgireq.env.get('HTTP_USER_AGENT', '')
354 ua = req.headers.get('User-Agent', '')
355 355 if cmd == 'rev' and 'mercurial' in ua:
356 356 wsgireq.form['style'] = ['raw']
357 357
358 358 if cmd == 'archive':
359 359 fn = wsgireq.form['node'][0]
360 360 for type_, spec in rctx.archivespecs.iteritems():
361 361 ext = spec[2]
362 362 if fn.endswith(ext):
363 363 wsgireq.form['node'] = [fn[:-len(ext)]]
364 364 wsgireq.form['type'] = [type_]
365 365 else:
366 366 cmd = wsgireq.form.get('cmd', [''])[0]
367 367
368 368 # process the web interface request
369 369
370 370 try:
371 371 tmpl = rctx.templater(wsgireq, req)
372 372 ctype = tmpl('mimetype', encoding=encoding.encoding)
373 373 ctype = templater.stringify(ctype)
374 374
375 375 # check read permissions non-static content
376 376 if cmd != 'static':
377 377 self.check_perm(rctx, wsgireq, None)
378 378
379 379 if cmd == '':
380 380 wsgireq.form['cmd'] = [tmpl.cache['default']]
381 381 cmd = wsgireq.form['cmd'][0]
382 382
383 383 # Don't enable caching if using a CSP nonce because then it wouldn't
384 384 # be a nonce.
385 385 if rctx.configbool('web', 'cache') and not rctx.nonce:
386 386 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
387 387 if cmd not in webcommands.__all__:
388 388 msg = 'no such method: %s' % cmd
389 389 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
390 390 elif cmd == 'file' and 'raw' in wsgireq.form.get('style', []):
391 391 rctx.ctype = ctype
392 392 content = webcommands.rawfile(rctx, wsgireq, tmpl)
393 393 else:
394 394 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
395 395 wsgireq.respond(HTTP_OK, ctype)
396 396
397 397 return content
398 398
399 399 except (error.LookupError, error.RepoLookupError) as err:
400 400 wsgireq.respond(HTTP_NOT_FOUND, ctype)
401 401 msg = pycompat.bytestr(err)
402 402 if (util.safehasattr(err, 'name') and
403 403 not isinstance(err, error.ManifestLookupError)):
404 404 msg = 'revision not found: %s' % err.name
405 405 return tmpl('error', error=msg)
406 406 except (error.RepoError, error.RevlogError) as inst:
407 407 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
408 408 return tmpl('error', error=pycompat.bytestr(inst))
409 409 except ErrorResponse as inst:
410 410 wsgireq.respond(inst, ctype)
411 411 if inst.code == HTTP_NOT_MODIFIED:
412 412 # Not allowed to return a body on a 304
413 413 return ['']
414 414 return tmpl('error', error=pycompat.bytestr(inst))
415 415
416 416 def check_perm(self, rctx, req, op):
417 417 for permhook in permhooks:
418 418 permhook(rctx, req, op)
419 419
420 420 def getwebview(repo):
421 421 """The 'web.view' config controls changeset filter to hgweb. Possible
422 422 values are ``served``, ``visible`` and ``all``. Default is ``served``.
423 423 The ``served`` filter only shows changesets that can be pulled from the
424 424 hgweb instance. The``visible`` filter includes secret changesets but
425 425 still excludes "hidden" one.
426 426
427 427 See the repoview module for details.
428 428
429 429 The option has been around undocumented since Mercurial 2.5, but no
430 430 user ever asked about it. So we better keep it undocumented for now."""
431 431 # experimental config: web.view
432 432 viewconfig = repo.ui.config('web', 'view', untrusted=True)
433 433 if viewconfig == 'all':
434 434 return repo.unfiltered()
435 435 elif viewconfig in repoview.filtertable:
436 436 return repo.filtered(viewconfig)
437 437 else:
438 438 return repo.filtered('served')
@@ -1,300 +1,315 b''
1 1 # hgweb/request.py - An http request from either CGI or the standalone server.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import cgi
12 12 import errno
13 13 import socket
14 import wsgiref.headers as wsgiheaders
14 15 #import wsgiref.validate
15 16
16 17 from .common import (
17 18 ErrorResponse,
18 19 HTTP_NOT_MODIFIED,
19 20 statusmessage,
20 21 )
21 22
22 23 from ..thirdparty import (
23 24 attr,
24 25 )
25 26 from .. import (
26 27 pycompat,
27 28 util,
28 29 )
29 30
30 31 shortcuts = {
31 32 'cl': [('cmd', ['changelog']), ('rev', None)],
32 33 'sl': [('cmd', ['shortlog']), ('rev', None)],
33 34 'cs': [('cmd', ['changeset']), ('node', None)],
34 35 'f': [('cmd', ['file']), ('filenode', None)],
35 36 'fl': [('cmd', ['filelog']), ('filenode', None)],
36 37 'fd': [('cmd', ['filediff']), ('node', None)],
37 38 'fa': [('cmd', ['annotate']), ('filenode', None)],
38 39 'mf': [('cmd', ['manifest']), ('manifest', None)],
39 40 'ca': [('cmd', ['archive']), ('node', None)],
40 41 'tags': [('cmd', ['tags'])],
41 42 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
42 43 'static': [('cmd', ['static']), ('file', None)]
43 44 }
44 45
45 46 def normalize(form):
46 47 # first expand the shortcuts
47 48 for k in shortcuts:
48 49 if k in form:
49 50 for name, value in shortcuts[k]:
50 51 if value is None:
51 52 value = form[k]
52 53 form[name] = value
53 54 del form[k]
54 55 # And strip the values
55 56 bytesform = {}
56 57 for k, v in form.iteritems():
57 58 bytesform[pycompat.bytesurl(k)] = [
58 59 pycompat.bytesurl(i.strip()) for i in v]
59 60 return bytesform
60 61
61 62 @attr.s(frozen=True)
62 63 class parsedrequest(object):
63 64 """Represents a parsed WSGI request / static HTTP request parameters."""
64 65
65 66 # Full URL for this request.
66 67 url = attr.ib()
67 68 # URL without any path components. Just <proto>://<host><port>.
68 69 baseurl = attr.ib()
69 70 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
70 71 # of HTTP: Host header for hostname. This is likely what clients used.
71 72 advertisedurl = attr.ib()
72 73 advertisedbaseurl = attr.ib()
73 74 # WSGI application path.
74 75 apppath = attr.ib()
75 76 # List of path parts to be used for dispatch.
76 77 dispatchparts = attr.ib()
77 78 # URL path component (no query string) used for dispatch.
78 79 dispatchpath = attr.ib()
79 80 # Whether there is a path component to this request. This can be true
80 81 # when ``dispatchpath`` is empty due to REPO_NAME muckery.
81 82 havepathinfo = attr.ib()
82 83 # Raw query string (part after "?" in URL).
83 84 querystring = attr.ib()
84 85 # List of 2-tuples of query string arguments.
85 86 querystringlist = attr.ib()
86 87 # Dict of query string arguments. Values are lists with at least 1 item.
87 88 querystringdict = attr.ib()
89 # wsgiref.headers.Headers instance. Operates like a dict with case
90 # insensitive keys.
91 headers = attr.ib()
88 92
89 93 def parserequestfromenv(env):
90 94 """Parse URL components from environment variables.
91 95
92 96 WSGI defines request attributes via environment variables. This function
93 97 parses the environment variables into a data structure.
94 98 """
95 99 # PEP-0333 defines the WSGI spec and is a useful reference for this code.
96 100
97 101 # We first validate that the incoming object conforms with the WSGI spec.
98 102 # We only want to be dealing with spec-conforming WSGI implementations.
99 103 # TODO enable this once we fix internal violations.
100 104 #wsgiref.validate.check_environ(env)
101 105
102 106 # PEP-0333 states that environment keys and values are native strings
103 107 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
104 108 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
105 109 # in Mercurial, so mass convert string keys and values to bytes.
106 110 if pycompat.ispy3:
107 111 env = {k.encode('latin-1'): v for k, v in env.iteritems()}
108 112 env = {k: v.encode('latin-1') if isinstance(v, str) else v
109 113 for k, v in env.iteritems()}
110 114
111 115 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
112 116 # the environment variables.
113 117 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
114 118 # how URLs are reconstructed.
115 119 fullurl = env['wsgi.url_scheme'] + '://'
116 120 advertisedfullurl = fullurl
117 121
118 122 def addport(s):
119 123 if env['wsgi.url_scheme'] == 'https':
120 124 if env['SERVER_PORT'] != '443':
121 125 s += ':' + env['SERVER_PORT']
122 126 else:
123 127 if env['SERVER_PORT'] != '80':
124 128 s += ':' + env['SERVER_PORT']
125 129
126 130 return s
127 131
128 132 if env.get('HTTP_HOST'):
129 133 fullurl += env['HTTP_HOST']
130 134 else:
131 135 fullurl += env['SERVER_NAME']
132 136 fullurl = addport(fullurl)
133 137
134 138 advertisedfullurl += env['SERVER_NAME']
135 139 advertisedfullurl = addport(advertisedfullurl)
136 140
137 141 baseurl = fullurl
138 142 advertisedbaseurl = advertisedfullurl
139 143
140 144 fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
141 145 advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
142 146 fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
143 147 advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
144 148
145 149 if env.get('QUERY_STRING'):
146 150 fullurl += '?' + env['QUERY_STRING']
147 151 advertisedfullurl += '?' + env['QUERY_STRING']
148 152
149 153 # When dispatching requests, we look at the URL components (PATH_INFO
150 154 # and QUERY_STRING) after the application root (SCRIPT_NAME). But hgwebdir
151 155 # has the concept of "virtual" repositories. This is defined via REPO_NAME.
152 156 # If REPO_NAME is defined, we append it to SCRIPT_NAME to form a new app
153 157 # root. We also exclude its path components from PATH_INFO when resolving
154 158 # the dispatch path.
155 159
156 160 apppath = env['SCRIPT_NAME']
157 161
158 162 if env.get('REPO_NAME'):
159 163 if not apppath.endswith('/'):
160 164 apppath += '/'
161 165
162 166 apppath += env.get('REPO_NAME')
163 167
164 168 if 'PATH_INFO' in env:
165 169 dispatchparts = env['PATH_INFO'].strip('/').split('/')
166 170
167 171 # Strip out repo parts.
168 172 repoparts = env.get('REPO_NAME', '').split('/')
169 173 if dispatchparts[:len(repoparts)] == repoparts:
170 174 dispatchparts = dispatchparts[len(repoparts):]
171 175 else:
172 176 dispatchparts = []
173 177
174 178 dispatchpath = '/'.join(dispatchparts)
175 179
176 180 querystring = env.get('QUERY_STRING', '')
177 181
178 182 # We store as a list so we have ordering information. We also store as
179 183 # a dict to facilitate fast lookup.
180 184 querystringlist = util.urlreq.parseqsl(querystring, keep_blank_values=True)
181 185
182 186 querystringdict = {}
183 187 for k, v in querystringlist:
184 188 if k in querystringdict:
185 189 querystringdict[k].append(v)
186 190 else:
187 191 querystringdict[k] = [v]
188 192
193 # HTTP_* keys contain HTTP request headers. The Headers structure should
194 # perform case normalization for us. We just rewrite underscore to dash
195 # so keys match what likely went over the wire.
196 headers = []
197 for k, v in env.iteritems():
198 if k.startswith('HTTP_'):
199 headers.append((k[len('HTTP_'):].replace('_', '-'), v))
200
201 headers = wsgiheaders.Headers(headers)
202
189 203 return parsedrequest(url=fullurl, baseurl=baseurl,
190 204 advertisedurl=advertisedfullurl,
191 205 advertisedbaseurl=advertisedbaseurl,
192 206 apppath=apppath,
193 207 dispatchparts=dispatchparts, dispatchpath=dispatchpath,
194 208 havepathinfo='PATH_INFO' in env,
195 209 querystring=querystring,
196 210 querystringlist=querystringlist,
197 querystringdict=querystringdict)
211 querystringdict=querystringdict,
212 headers=headers)
198 213
199 214 class wsgirequest(object):
200 215 """Higher-level API for a WSGI request.
201 216
202 217 WSGI applications are invoked with 2 arguments. They are used to
203 218 instantiate instances of this class, which provides higher-level APIs
204 219 for obtaining request parameters, writing HTTP output, etc.
205 220 """
206 221 def __init__(self, wsgienv, start_response):
207 222 version = wsgienv[r'wsgi.version']
208 223 if (version < (1, 0)) or (version >= (2, 0)):
209 224 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
210 225 % version)
211 226 self.inp = wsgienv[r'wsgi.input']
212 227 self.err = wsgienv[r'wsgi.errors']
213 228 self.threaded = wsgienv[r'wsgi.multithread']
214 229 self.multiprocess = wsgienv[r'wsgi.multiprocess']
215 230 self.run_once = wsgienv[r'wsgi.run_once']
216 231 self.env = wsgienv
217 232 self.form = normalize(cgi.parse(self.inp,
218 233 self.env,
219 234 keep_blank_values=1))
220 235 self._start_response = start_response
221 236 self.server_write = None
222 237 self.headers = []
223 238
224 239 def __iter__(self):
225 240 return iter([])
226 241
227 242 def read(self, count=-1):
228 243 return self.inp.read(count)
229 244
230 245 def drain(self):
231 246 '''need to read all data from request, httplib is half-duplex'''
232 247 length = int(self.env.get('CONTENT_LENGTH') or 0)
233 248 for s in util.filechunkiter(self.inp, limit=length):
234 249 pass
235 250
236 251 def respond(self, status, type, filename=None, body=None):
237 252 if not isinstance(type, str):
238 253 type = pycompat.sysstr(type)
239 254 if self._start_response is not None:
240 255 self.headers.append((r'Content-Type', type))
241 256 if filename:
242 257 filename = (filename.rpartition('/')[-1]
243 258 .replace('\\', '\\\\').replace('"', '\\"'))
244 259 self.headers.append(('Content-Disposition',
245 260 'inline; filename="%s"' % filename))
246 261 if body is not None:
247 262 self.headers.append((r'Content-Length', str(len(body))))
248 263
249 264 for k, v in self.headers:
250 265 if not isinstance(v, str):
251 266 raise TypeError('header value must be string: %r' % (v,))
252 267
253 268 if isinstance(status, ErrorResponse):
254 269 self.headers.extend(status.headers)
255 270 if status.code == HTTP_NOT_MODIFIED:
256 271 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
257 272 # it MUST NOT include any headers other than these and no
258 273 # body
259 274 self.headers = [(k, v) for (k, v) in self.headers if
260 275 k in ('Date', 'ETag', 'Expires',
261 276 'Cache-Control', 'Vary')]
262 277 status = statusmessage(status.code, pycompat.bytestr(status))
263 278 elif status == 200:
264 279 status = '200 Script output follows'
265 280 elif isinstance(status, int):
266 281 status = statusmessage(status)
267 282
268 283 self.server_write = self._start_response(
269 284 pycompat.sysstr(status), self.headers)
270 285 self._start_response = None
271 286 self.headers = []
272 287 if body is not None:
273 288 self.write(body)
274 289 self.server_write = None
275 290
276 291 def write(self, thing):
277 292 if thing:
278 293 try:
279 294 self.server_write(thing)
280 295 except socket.error as inst:
281 296 if inst[0] != errno.ECONNRESET:
282 297 raise
283 298
284 299 def writelines(self, lines):
285 300 for line in lines:
286 301 self.write(line)
287 302
288 303 def flush(self):
289 304 return None
290 305
291 306 def close(self):
292 307 return None
293 308
294 309 def wsgiapplication(app_maker):
295 310 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
296 311 can and should now be used as a WSGI application.'''
297 312 application = app_maker()
298 313 def run_wsgi(env, respond):
299 314 return application(env, respond)
300 315 return run_wsgi
General Comments 0
You need to be logged in to leave comments. Login now