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