##// END OF EJS Templates
hgweb: use parsed request to construct query parameters...
Gregory Szorc -
r36829:cfb9ef24 default
parent child Browse files
Show More
@@ -1,452 +1,447 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 if r'PATH_INFO' in wsgireq.env:
322 parts = wsgireq.env[r'PATH_INFO'].strip(r'/').split(r'/')
323 repo_parts = wsgireq.env.get(r'REPO_NAME', r'').split(r'/')
324 if parts[:len(repo_parts)] == repo_parts:
325 parts = parts[len(repo_parts):]
326 query = r'/'.join(parts)
321 if req.havepathinfo:
322 query = req.dispatchpath
327 323 else:
328 query = wsgireq.env[r'QUERY_STRING'].partition(r'&')[0]
329 query = query.partition(r';')[0]
324 query = req.querystring.partition('&')[0].partition(';')[0]
330 325
331 326 # Route it to a wire protocol handler if it looks like a wire protocol
332 327 # request.
333 328 protohandler = wireprotoserver.parsehttprequest(rctx, wsgireq, req,
334 329 self.check_perm)
335 330
336 331 if protohandler:
337 332 try:
338 333 if query:
339 334 raise ErrorResponse(HTTP_NOT_FOUND)
340 335
341 336 return protohandler['dispatch']()
342 337 except ErrorResponse as inst:
343 338 return protohandler['handleerror'](inst)
344 339
345 340 # translate user-visible url structure to internal structure
346 341
347 args = query.split(r'/', 2)
342 args = query.split('/', 2)
348 343 if 'cmd' not in wsgireq.form and args and args[0]:
349 344 cmd = args.pop(0)
350 345 style = cmd.rfind('-')
351 346 if style != -1:
352 347 wsgireq.form['style'] = [cmd[:style]]
353 348 cmd = cmd[style + 1:]
354 349
355 350 # avoid accepting e.g. style parameter as command
356 351 if util.safehasattr(webcommands, cmd):
357 352 wsgireq.form['cmd'] = [cmd]
358 353
359 354 if cmd == 'static':
360 355 wsgireq.form['file'] = ['/'.join(args)]
361 356 else:
362 357 if args and args[0]:
363 358 node = args.pop(0).replace('%2F', '/')
364 359 wsgireq.form['node'] = [node]
365 360 if args:
366 361 wsgireq.form['file'] = args
367 362
368 363 ua = wsgireq.env.get('HTTP_USER_AGENT', '')
369 364 if cmd == 'rev' and 'mercurial' in ua:
370 365 wsgireq.form['style'] = ['raw']
371 366
372 367 if cmd == 'archive':
373 368 fn = wsgireq.form['node'][0]
374 369 for type_, spec in rctx.archivespecs.iteritems():
375 370 ext = spec[2]
376 371 if fn.endswith(ext):
377 372 wsgireq.form['node'] = [fn[:-len(ext)]]
378 373 wsgireq.form['type'] = [type_]
379 374 else:
380 375 cmd = wsgireq.form.get('cmd', [''])[0]
381 376
382 377 # process the web interface request
383 378
384 379 try:
385 380 tmpl = rctx.templater(wsgireq, req)
386 381 ctype = tmpl('mimetype', encoding=encoding.encoding)
387 382 ctype = templater.stringify(ctype)
388 383
389 384 # check read permissions non-static content
390 385 if cmd != 'static':
391 386 self.check_perm(rctx, wsgireq, None)
392 387
393 388 if cmd == '':
394 389 wsgireq.form['cmd'] = [tmpl.cache['default']]
395 390 cmd = wsgireq.form['cmd'][0]
396 391
397 392 # Don't enable caching if using a CSP nonce because then it wouldn't
398 393 # be a nonce.
399 394 if rctx.configbool('web', 'cache') and not rctx.nonce:
400 395 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
401 396 if cmd not in webcommands.__all__:
402 397 msg = 'no such method: %s' % cmd
403 398 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
404 399 elif cmd == 'file' and 'raw' in wsgireq.form.get('style', []):
405 400 rctx.ctype = ctype
406 401 content = webcommands.rawfile(rctx, wsgireq, tmpl)
407 402 else:
408 403 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
409 404 wsgireq.respond(HTTP_OK, ctype)
410 405
411 406 return content
412 407
413 408 except (error.LookupError, error.RepoLookupError) as err:
414 409 wsgireq.respond(HTTP_NOT_FOUND, ctype)
415 410 msg = pycompat.bytestr(err)
416 411 if (util.safehasattr(err, 'name') and
417 412 not isinstance(err, error.ManifestLookupError)):
418 413 msg = 'revision not found: %s' % err.name
419 414 return tmpl('error', error=msg)
420 415 except (error.RepoError, error.RevlogError) as inst:
421 416 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
422 417 return tmpl('error', error=pycompat.bytestr(inst))
423 418 except ErrorResponse as inst:
424 419 wsgireq.respond(inst, ctype)
425 420 if inst.code == HTTP_NOT_MODIFIED:
426 421 # Not allowed to return a body on a 304
427 422 return ['']
428 423 return tmpl('error', error=pycompat.bytestr(inst))
429 424
430 425 def check_perm(self, rctx, req, op):
431 426 for permhook in permhooks:
432 427 permhook(rctx, req, op)
433 428
434 429 def getwebview(repo):
435 430 """The 'web.view' config controls changeset filter to hgweb. Possible
436 431 values are ``served``, ``visible`` and ``all``. Default is ``served``.
437 432 The ``served`` filter only shows changesets that can be pulled from the
438 433 hgweb instance. The``visible`` filter includes secret changesets but
439 434 still excludes "hidden" one.
440 435
441 436 See the repoview module for details.
442 437
443 438 The option has been around undocumented since Mercurial 2.5, but no
444 439 user ever asked about it. So we better keep it undocumented for now."""
445 440 # experimental config: web.view
446 441 viewconfig = repo.ui.config('web', 'view', untrusted=True)
447 442 if viewconfig == 'all':
448 443 return repo.unfiltered()
449 444 elif viewconfig in repoview.filtertable:
450 445 return repo.filtered(viewconfig)
451 446 else:
452 447 return repo.filtered('served')
@@ -1,296 +1,300 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 14 #import wsgiref.validate
15 15
16 16 from .common import (
17 17 ErrorResponse,
18 18 HTTP_NOT_MODIFIED,
19 19 statusmessage,
20 20 )
21 21
22 22 from ..thirdparty import (
23 23 attr,
24 24 )
25 25 from .. import (
26 26 pycompat,
27 27 util,
28 28 )
29 29
30 30 shortcuts = {
31 31 'cl': [('cmd', ['changelog']), ('rev', None)],
32 32 'sl': [('cmd', ['shortlog']), ('rev', None)],
33 33 'cs': [('cmd', ['changeset']), ('node', None)],
34 34 'f': [('cmd', ['file']), ('filenode', None)],
35 35 'fl': [('cmd', ['filelog']), ('filenode', None)],
36 36 'fd': [('cmd', ['filediff']), ('node', None)],
37 37 'fa': [('cmd', ['annotate']), ('filenode', None)],
38 38 'mf': [('cmd', ['manifest']), ('manifest', None)],
39 39 'ca': [('cmd', ['archive']), ('node', None)],
40 40 'tags': [('cmd', ['tags'])],
41 41 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
42 42 'static': [('cmd', ['static']), ('file', None)]
43 43 }
44 44
45 45 def normalize(form):
46 46 # first expand the shortcuts
47 47 for k in shortcuts:
48 48 if k in form:
49 49 for name, value in shortcuts[k]:
50 50 if value is None:
51 51 value = form[k]
52 52 form[name] = value
53 53 del form[k]
54 54 # And strip the values
55 55 bytesform = {}
56 56 for k, v in form.iteritems():
57 57 bytesform[pycompat.bytesurl(k)] = [
58 58 pycompat.bytesurl(i.strip()) for i in v]
59 59 return bytesform
60 60
61 61 @attr.s(frozen=True)
62 62 class parsedrequest(object):
63 63 """Represents a parsed WSGI request / static HTTP request parameters."""
64 64
65 65 # Full URL for this request.
66 66 url = attr.ib()
67 67 # URL without any path components. Just <proto>://<host><port>.
68 68 baseurl = attr.ib()
69 69 # Advertised URL. Like ``url`` and ``baseurl`` but uses SERVER_NAME instead
70 70 # of HTTP: Host header for hostname. This is likely what clients used.
71 71 advertisedurl = attr.ib()
72 72 advertisedbaseurl = attr.ib()
73 73 # WSGI application path.
74 74 apppath = attr.ib()
75 75 # List of path parts to be used for dispatch.
76 76 dispatchparts = attr.ib()
77 77 # URL path component (no query string) used for dispatch.
78 78 dispatchpath = attr.ib()
79 # Whether there is a path component to this request. This can be true
80 # when ``dispatchpath`` is empty due to REPO_NAME muckery.
81 havepathinfo = attr.ib()
79 82 # Raw query string (part after "?" in URL).
80 83 querystring = attr.ib()
81 84 # List of 2-tuples of query string arguments.
82 85 querystringlist = attr.ib()
83 86 # Dict of query string arguments. Values are lists with at least 1 item.
84 87 querystringdict = attr.ib()
85 88
86 89 def parserequestfromenv(env):
87 90 """Parse URL components from environment variables.
88 91
89 92 WSGI defines request attributes via environment variables. This function
90 93 parses the environment variables into a data structure.
91 94 """
92 95 # PEP-0333 defines the WSGI spec and is a useful reference for this code.
93 96
94 97 # We first validate that the incoming object conforms with the WSGI spec.
95 98 # We only want to be dealing with spec-conforming WSGI implementations.
96 99 # TODO enable this once we fix internal violations.
97 100 #wsgiref.validate.check_environ(env)
98 101
99 102 # PEP-0333 states that environment keys and values are native strings
100 103 # (bytes on Python 2 and str on Python 3). The code points for the Unicode
101 104 # strings on Python 3 must be between \00000-\000FF. We deal with bytes
102 105 # in Mercurial, so mass convert string keys and values to bytes.
103 106 if pycompat.ispy3:
104 107 env = {k.encode('latin-1'): v for k, v in env.iteritems()}
105 108 env = {k: v.encode('latin-1') if isinstance(v, str) else v
106 109 for k, v in env.iteritems()}
107 110
108 111 # https://www.python.org/dev/peps/pep-0333/#environ-variables defines
109 112 # the environment variables.
110 113 # https://www.python.org/dev/peps/pep-0333/#url-reconstruction defines
111 114 # how URLs are reconstructed.
112 115 fullurl = env['wsgi.url_scheme'] + '://'
113 116 advertisedfullurl = fullurl
114 117
115 118 def addport(s):
116 119 if env['wsgi.url_scheme'] == 'https':
117 120 if env['SERVER_PORT'] != '443':
118 121 s += ':' + env['SERVER_PORT']
119 122 else:
120 123 if env['SERVER_PORT'] != '80':
121 124 s += ':' + env['SERVER_PORT']
122 125
123 126 return s
124 127
125 128 if env.get('HTTP_HOST'):
126 129 fullurl += env['HTTP_HOST']
127 130 else:
128 131 fullurl += env['SERVER_NAME']
129 132 fullurl = addport(fullurl)
130 133
131 134 advertisedfullurl += env['SERVER_NAME']
132 135 advertisedfullurl = addport(advertisedfullurl)
133 136
134 137 baseurl = fullurl
135 138 advertisedbaseurl = advertisedfullurl
136 139
137 140 fullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
138 141 advertisedfullurl += util.urlreq.quote(env.get('SCRIPT_NAME', ''))
139 142 fullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
140 143 advertisedfullurl += util.urlreq.quote(env.get('PATH_INFO', ''))
141 144
142 145 if env.get('QUERY_STRING'):
143 146 fullurl += '?' + env['QUERY_STRING']
144 147 advertisedfullurl += '?' + env['QUERY_STRING']
145 148
146 149 # When dispatching requests, we look at the URL components (PATH_INFO
147 150 # and QUERY_STRING) after the application root (SCRIPT_NAME). But hgwebdir
148 151 # has the concept of "virtual" repositories. This is defined via REPO_NAME.
149 152 # If REPO_NAME is defined, we append it to SCRIPT_NAME to form a new app
150 153 # root. We also exclude its path components from PATH_INFO when resolving
151 154 # the dispatch path.
152 155
153 156 apppath = env['SCRIPT_NAME']
154 157
155 158 if env.get('REPO_NAME'):
156 159 if not apppath.endswith('/'):
157 160 apppath += '/'
158 161
159 162 apppath += env.get('REPO_NAME')
160 163
161 164 if 'PATH_INFO' in env:
162 165 dispatchparts = env['PATH_INFO'].strip('/').split('/')
163 166
164 167 # Strip out repo parts.
165 168 repoparts = env.get('REPO_NAME', '').split('/')
166 169 if dispatchparts[:len(repoparts)] == repoparts:
167 170 dispatchparts = dispatchparts[len(repoparts):]
168 171 else:
169 172 dispatchparts = []
170 173
171 174 dispatchpath = '/'.join(dispatchparts)
172 175
173 176 querystring = env.get('QUERY_STRING', '')
174 177
175 178 # We store as a list so we have ordering information. We also store as
176 179 # a dict to facilitate fast lookup.
177 180 querystringlist = util.urlreq.parseqsl(querystring, keep_blank_values=True)
178 181
179 182 querystringdict = {}
180 183 for k, v in querystringlist:
181 184 if k in querystringdict:
182 185 querystringdict[k].append(v)
183 186 else:
184 187 querystringdict[k] = [v]
185 188
186 189 return parsedrequest(url=fullurl, baseurl=baseurl,
187 190 advertisedurl=advertisedfullurl,
188 191 advertisedbaseurl=advertisedbaseurl,
189 192 apppath=apppath,
190 193 dispatchparts=dispatchparts, dispatchpath=dispatchpath,
194 havepathinfo='PATH_INFO' in env,
191 195 querystring=querystring,
192 196 querystringlist=querystringlist,
193 197 querystringdict=querystringdict)
194 198
195 199 class wsgirequest(object):
196 200 """Higher-level API for a WSGI request.
197 201
198 202 WSGI applications are invoked with 2 arguments. They are used to
199 203 instantiate instances of this class, which provides higher-level APIs
200 204 for obtaining request parameters, writing HTTP output, etc.
201 205 """
202 206 def __init__(self, wsgienv, start_response):
203 207 version = wsgienv[r'wsgi.version']
204 208 if (version < (1, 0)) or (version >= (2, 0)):
205 209 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
206 210 % version)
207 211 self.inp = wsgienv[r'wsgi.input']
208 212 self.err = wsgienv[r'wsgi.errors']
209 213 self.threaded = wsgienv[r'wsgi.multithread']
210 214 self.multiprocess = wsgienv[r'wsgi.multiprocess']
211 215 self.run_once = wsgienv[r'wsgi.run_once']
212 216 self.env = wsgienv
213 217 self.form = normalize(cgi.parse(self.inp,
214 218 self.env,
215 219 keep_blank_values=1))
216 220 self._start_response = start_response
217 221 self.server_write = None
218 222 self.headers = []
219 223
220 224 def __iter__(self):
221 225 return iter([])
222 226
223 227 def read(self, count=-1):
224 228 return self.inp.read(count)
225 229
226 230 def drain(self):
227 231 '''need to read all data from request, httplib is half-duplex'''
228 232 length = int(self.env.get('CONTENT_LENGTH') or 0)
229 233 for s in util.filechunkiter(self.inp, limit=length):
230 234 pass
231 235
232 236 def respond(self, status, type, filename=None, body=None):
233 237 if not isinstance(type, str):
234 238 type = pycompat.sysstr(type)
235 239 if self._start_response is not None:
236 240 self.headers.append((r'Content-Type', type))
237 241 if filename:
238 242 filename = (filename.rpartition('/')[-1]
239 243 .replace('\\', '\\\\').replace('"', '\\"'))
240 244 self.headers.append(('Content-Disposition',
241 245 'inline; filename="%s"' % filename))
242 246 if body is not None:
243 247 self.headers.append((r'Content-Length', str(len(body))))
244 248
245 249 for k, v in self.headers:
246 250 if not isinstance(v, str):
247 251 raise TypeError('header value must be string: %r' % (v,))
248 252
249 253 if isinstance(status, ErrorResponse):
250 254 self.headers.extend(status.headers)
251 255 if status.code == HTTP_NOT_MODIFIED:
252 256 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
253 257 # it MUST NOT include any headers other than these and no
254 258 # body
255 259 self.headers = [(k, v) for (k, v) in self.headers if
256 260 k in ('Date', 'ETag', 'Expires',
257 261 'Cache-Control', 'Vary')]
258 262 status = statusmessage(status.code, pycompat.bytestr(status))
259 263 elif status == 200:
260 264 status = '200 Script output follows'
261 265 elif isinstance(status, int):
262 266 status = statusmessage(status)
263 267
264 268 self.server_write = self._start_response(
265 269 pycompat.sysstr(status), self.headers)
266 270 self._start_response = None
267 271 self.headers = []
268 272 if body is not None:
269 273 self.write(body)
270 274 self.server_write = None
271 275
272 276 def write(self, thing):
273 277 if thing:
274 278 try:
275 279 self.server_write(thing)
276 280 except socket.error as inst:
277 281 if inst[0] != errno.ECONNRESET:
278 282 raise
279 283
280 284 def writelines(self, lines):
281 285 for line in lines:
282 286 self.write(line)
283 287
284 288 def flush(self):
285 289 return None
286 290
287 291 def close(self):
288 292 return None
289 293
290 294 def wsgiapplication(app_maker):
291 295 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
292 296 can and should now be used as a WSGI application.'''
293 297 application = app_maker()
294 298 def run_wsgi(env, respond):
295 299 return application(env, respond)
296 300 return run_wsgi
General Comments 0
You need to be logged in to leave comments. Login now