##// END OF EJS Templates
hgweb: ensure all wsgi environment values are str...
Gregory Szorc -
r36820:7fc80c98 default
parent child Browse files
Show More
@@ -1,472 +1,473 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 from .request import wsgirequest
26 26
27 27 from .. import (
28 28 encoding,
29 29 error,
30 30 formatter,
31 31 hg,
32 32 hook,
33 33 profiling,
34 34 pycompat,
35 35 repoview,
36 36 templatefilters,
37 37 templater,
38 38 ui as uimod,
39 39 util,
40 40 wireprotoserver,
41 41 )
42 42
43 43 from . import (
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, req):
146 146 # determine scheme, port and server name
147 147 # this is needed to create absolute urls
148 148
149 149 proto = req.env.get('wsgi.url_scheme')
150 150 if proto == 'https':
151 151 proto = 'https'
152 152 default_port = '443'
153 153 else:
154 154 proto = 'http'
155 155 default_port = '80'
156 156
157 157 port = req.env[r'SERVER_PORT']
158 158 port = port != default_port and (r':' + port) or r''
159 159 urlbase = r'%s://%s%s' % (proto, req.env[r'SERVER_NAME'], port)
160 160 logourl = self.config('web', 'logourl')
161 161 logoimg = self.config('web', 'logoimg')
162 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
162 staticurl = (self.config('web', 'staticurl')
163 or pycompat.sysbytes(req.url) + 'static/')
163 164 if not staticurl.endswith('/'):
164 165 staticurl += '/'
165 166
166 167 # some functions for the templater
167 168
168 169 def motd(**map):
169 170 yield self.config('web', 'motd')
170 171
171 172 # figure out which style to use
172 173
173 174 vars = {}
174 175 styles, (style, mapfile) = getstyle(req, self.config,
175 176 self.templatepath)
176 177 if style == styles[0]:
177 178 vars['style'] = style
178 179
179 180 start = '&' if req.url[-1] == r'?' else '?'
180 181 sessionvars = webutil.sessionvars(vars, start)
181 182
182 183 if not self.reponame:
183 184 self.reponame = (self.config('web', 'name', '')
184 185 or req.env.get('REPO_NAME')
185 or req.url.strip('/') or self.repo.root)
186 or req.url.strip(r'/') or self.repo.root)
186 187
187 188 def websubfilter(text):
188 189 return templatefilters.websub(text, self.websubtable)
189 190
190 191 # create the templater
191 192 # TODO: export all keywords: defaults = templatekw.keywords.copy()
192 193 defaults = {
193 'url': req.url,
194 'url': pycompat.sysbytes(req.url),
194 195 'logourl': logourl,
195 196 'logoimg': logoimg,
196 197 'staticurl': staticurl,
197 198 'urlbase': urlbase,
198 199 'repo': self.reponame,
199 200 'encoding': encoding.encoding,
200 201 'motd': motd,
201 202 'sessionvars': sessionvars,
202 'pathdef': makebreadcrumb(req.url),
203 'pathdef': makebreadcrumb(pycompat.sysbytes(req.url)),
203 204 'style': style,
204 205 'nonce': self.nonce,
205 206 }
206 207 tres = formatter.templateresources(self.repo.ui, self.repo)
207 208 tmpl = templater.templater.frommapfile(mapfile,
208 209 filters={'websub': websubfilter},
209 210 defaults=defaults,
210 211 resources=tres)
211 212 return tmpl
212 213
213 214
214 215 class hgweb(object):
215 216 """HTTP server for individual repositories.
216 217
217 218 Instances of this class serve HTTP responses for a particular
218 219 repository.
219 220
220 221 Instances are typically used as WSGI applications.
221 222
222 223 Some servers are multi-threaded. On these servers, there may
223 224 be multiple active threads inside __call__.
224 225 """
225 226 def __init__(self, repo, name=None, baseui=None):
226 227 if isinstance(repo, str):
227 228 if baseui:
228 229 u = baseui.copy()
229 230 else:
230 231 u = uimod.ui.load()
231 232 r = hg.repository(u, repo)
232 233 else:
233 234 # we trust caller to give us a private copy
234 235 r = repo
235 236
236 237 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
237 238 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
238 239 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
239 240 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
240 241 # resolve file patterns relative to repo root
241 242 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
242 243 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
243 244 # displaying bundling progress bar while serving feel wrong and may
244 245 # break some wsgi implementation.
245 246 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
246 247 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
247 248 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
248 249 self._lastrepo = self._repos[0]
249 250 hook.redirect(True)
250 251 self.reponame = name
251 252
252 253 def _webifyrepo(self, repo):
253 254 repo = getwebview(repo)
254 255 self.websubtable = webutil.getwebsubs(repo)
255 256 return repo
256 257
257 258 @contextlib.contextmanager
258 259 def _obtainrepo(self):
259 260 """Obtain a repo unique to the caller.
260 261
261 262 Internally we maintain a stack of cachedlocalrepo instances
262 263 to be handed out. If one is available, we pop it and return it,
263 264 ensuring it is up to date in the process. If one is not available,
264 265 we clone the most recently used repo instance and return it.
265 266
266 267 It is currently possible for the stack to grow without bounds
267 268 if the server allows infinite threads. However, servers should
268 269 have a thread limit, thus establishing our limit.
269 270 """
270 271 if self._repos:
271 272 cached = self._repos.pop()
272 273 r, created = cached.fetch()
273 274 else:
274 275 cached = self._lastrepo.copy()
275 276 r, created = cached.fetch()
276 277 if created:
277 278 r = self._webifyrepo(r)
278 279
279 280 self._lastrepo = cached
280 281 self.mtime = cached.mtime
281 282 try:
282 283 yield r
283 284 finally:
284 285 self._repos.append(cached)
285 286
286 287 def run(self):
287 288 """Start a server from CGI environment.
288 289
289 290 Modern servers should be using WSGI and should avoid this
290 291 method, if possible.
291 292 """
292 293 if not encoding.environ.get('GATEWAY_INTERFACE',
293 294 '').startswith("CGI/1."):
294 295 raise RuntimeError("This function is only intended to be "
295 296 "called while running as a CGI script.")
296 297 wsgicgi.launch(self)
297 298
298 299 def __call__(self, env, respond):
299 300 """Run the WSGI application.
300 301
301 302 This may be called by multiple threads.
302 303 """
303 304 req = wsgirequest(env, respond)
304 305 return self.run_wsgi(req)
305 306
306 307 def run_wsgi(self, req):
307 308 """Internal method to run the WSGI application.
308 309
309 310 This is typically only called by Mercurial. External consumers
310 311 should be using instances of this class as the WSGI application.
311 312 """
312 313 with self._obtainrepo() as repo:
313 314 profile = repo.ui.configbool('profiling', 'enabled')
314 315 with profiling.profile(repo.ui, enabled=profile):
315 316 for r in self._runwsgi(req, repo):
316 317 yield r
317 318
318 319 def _runwsgi(self, req, repo):
319 320 rctx = requestcontext(self, repo)
320 321
321 322 # This state is global across all threads.
322 323 encoding.encoding = rctx.config('web', 'encoding')
323 324 rctx.repo.ui.environ = req.env
324 325
325 326 if rctx.csp:
326 327 # hgwebdir may have added CSP header. Since we generate our own,
327 328 # replace it.
328 329 req.headers = [h for h in req.headers
329 330 if h[0] != 'Content-Security-Policy']
330 331 req.headers.append(('Content-Security-Policy', rctx.csp))
331 332
332 333 # work with CGI variables to create coherent structure
333 334 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
334 335
335 336 req.url = req.env[r'SCRIPT_NAME']
336 if not req.url.endswith('/'):
337 req.url += '/'
337 if not req.url.endswith(r'/'):
338 req.url += r'/'
338 339 if req.env.get('REPO_NAME'):
339 340 req.url += req.env[r'REPO_NAME'] + r'/'
340 341
341 342 if r'PATH_INFO' in req.env:
342 parts = req.env[r'PATH_INFO'].strip('/').split('/')
343 parts = req.env[r'PATH_INFO'].strip(r'/').split(r'/')
343 344 repo_parts = req.env.get(r'REPO_NAME', r'').split(r'/')
344 345 if parts[:len(repo_parts)] == repo_parts:
345 346 parts = parts[len(repo_parts):]
346 query = '/'.join(parts)
347 query = r'/'.join(parts)
347 348 else:
348 349 query = req.env[r'QUERY_STRING'].partition(r'&')[0]
349 350 query = query.partition(r';')[0]
350 351
351 352 # Route it to a wire protocol handler if it looks like a wire protocol
352 353 # request.
353 354 protohandler = wireprotoserver.parsehttprequest(rctx, req, query,
354 355 self.check_perm)
355 356
356 357 if protohandler:
357 358 try:
358 359 if query:
359 360 raise ErrorResponse(HTTP_NOT_FOUND)
360 361
361 362 return protohandler['dispatch']()
362 363 except ErrorResponse as inst:
363 364 return protohandler['handleerror'](inst)
364 365
365 366 # translate user-visible url structure to internal structure
366 367
367 args = query.split('/', 2)
368 args = query.split(r'/', 2)
368 369 if 'cmd' not in req.form and args and args[0]:
369 370 cmd = args.pop(0)
370 371 style = cmd.rfind('-')
371 372 if style != -1:
372 373 req.form['style'] = [cmd[:style]]
373 374 cmd = cmd[style + 1:]
374 375
375 376 # avoid accepting e.g. style parameter as command
376 377 if util.safehasattr(webcommands, cmd):
377 378 req.form['cmd'] = [cmd]
378 379
379 380 if cmd == 'static':
380 381 req.form['file'] = ['/'.join(args)]
381 382 else:
382 383 if args and args[0]:
383 384 node = args.pop(0).replace('%2F', '/')
384 385 req.form['node'] = [node]
385 386 if args:
386 387 req.form['file'] = args
387 388
388 389 ua = req.env.get('HTTP_USER_AGENT', '')
389 390 if cmd == 'rev' and 'mercurial' in ua:
390 391 req.form['style'] = ['raw']
391 392
392 393 if cmd == 'archive':
393 394 fn = req.form['node'][0]
394 395 for type_, spec in rctx.archivespecs.iteritems():
395 396 ext = spec[2]
396 397 if fn.endswith(ext):
397 398 req.form['node'] = [fn[:-len(ext)]]
398 399 req.form['type'] = [type_]
399 400 else:
400 401 cmd = req.form.get('cmd', [''])[0]
401 402
402 403 # process the web interface request
403 404
404 405 try:
405 406 tmpl = rctx.templater(req)
406 407 ctype = tmpl('mimetype', encoding=encoding.encoding)
407 408 ctype = templater.stringify(ctype)
408 409
409 410 # check read permissions non-static content
410 411 if cmd != 'static':
411 412 self.check_perm(rctx, req, None)
412 413
413 414 if cmd == '':
414 415 req.form['cmd'] = [tmpl.cache['default']]
415 416 cmd = req.form['cmd'][0]
416 417
417 418 # Don't enable caching if using a CSP nonce because then it wouldn't
418 419 # be a nonce.
419 420 if rctx.configbool('web', 'cache') and not rctx.nonce:
420 421 caching(self, req) # sets ETag header or raises NOT_MODIFIED
421 422 if cmd not in webcommands.__all__:
422 423 msg = 'no such method: %s' % cmd
423 424 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
424 425 elif cmd == 'file' and 'raw' in req.form.get('style', []):
425 426 rctx.ctype = ctype
426 427 content = webcommands.rawfile(rctx, req, tmpl)
427 428 else:
428 429 content = getattr(webcommands, cmd)(rctx, req, tmpl)
429 430 req.respond(HTTP_OK, ctype)
430 431
431 432 return content
432 433
433 434 except (error.LookupError, error.RepoLookupError) as err:
434 435 req.respond(HTTP_NOT_FOUND, ctype)
435 436 msg = pycompat.bytestr(err)
436 437 if (util.safehasattr(err, 'name') and
437 438 not isinstance(err, error.ManifestLookupError)):
438 439 msg = 'revision not found: %s' % err.name
439 440 return tmpl('error', error=msg)
440 441 except (error.RepoError, error.RevlogError) as inst:
441 442 req.respond(HTTP_SERVER_ERROR, ctype)
442 443 return tmpl('error', error=pycompat.bytestr(inst))
443 444 except ErrorResponse as inst:
444 445 req.respond(inst, ctype)
445 446 if inst.code == HTTP_NOT_MODIFIED:
446 447 # Not allowed to return a body on a 304
447 448 return ['']
448 449 return tmpl('error', error=pycompat.bytestr(inst))
449 450
450 451 def check_perm(self, rctx, req, op):
451 452 for permhook in permhooks:
452 453 permhook(rctx, req, op)
453 454
454 455 def getwebview(repo):
455 456 """The 'web.view' config controls changeset filter to hgweb. Possible
456 457 values are ``served``, ``visible`` and ``all``. Default is ``served``.
457 458 The ``served`` filter only shows changesets that can be pulled from the
458 459 hgweb instance. The``visible`` filter includes secret changesets but
459 460 still excludes "hidden" one.
460 461
461 462 See the repoview module for details.
462 463
463 464 The option has been around undocumented since Mercurial 2.5, but no
464 465 user ever asked about it. So we better keep it undocumented for now."""
465 466 # experimental config: web.view
466 467 viewconfig = repo.ui.config('web', 'view', untrusted=True)
467 468 if viewconfig == 'all':
468 469 return repo.unfiltered()
469 470 elif viewconfig in repoview.filtertable:
470 471 return repo.filtered(viewconfig)
471 472 else:
472 473 return repo.filtered('served')
@@ -1,347 +1,347 b''
1 1 # hgweb/server.py - The standalone hg web server.
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 errno
12 12 import os
13 13 import socket
14 14 import sys
15 15 import traceback
16 16
17 17 from ..i18n import _
18 18
19 19 from .. import (
20 20 encoding,
21 21 error,
22 22 pycompat,
23 23 util,
24 24 )
25 25
26 26 httpservermod = util.httpserver
27 27 socketserver = util.socketserver
28 28 urlerr = util.urlerr
29 29 urlreq = util.urlreq
30 30
31 31 from . import (
32 32 common,
33 33 )
34 34
35 35 def _splitURI(uri):
36 36 """Return path and query that has been split from uri
37 37
38 38 Just like CGI environment, the path is unquoted, the query is
39 39 not.
40 40 """
41 41 if r'?' in uri:
42 42 path, query = uri.split(r'?', 1)
43 43 else:
44 44 path, query = uri, r''
45 45 return urlreq.unquote(path), query
46 46
47 47 class _error_logger(object):
48 48 def __init__(self, handler):
49 49 self.handler = handler
50 50 def flush(self):
51 51 pass
52 52 def write(self, str):
53 53 self.writelines(str.split('\n'))
54 54 def writelines(self, seq):
55 55 for msg in seq:
56 56 self.handler.log_error("HG error: %s", msg)
57 57
58 58 class _httprequesthandler(httpservermod.basehttprequesthandler):
59 59
60 60 url_scheme = 'http'
61 61
62 62 @staticmethod
63 63 def preparehttpserver(httpserver, ui):
64 64 """Prepare .socket of new HTTPServer instance"""
65 65
66 66 def __init__(self, *args, **kargs):
67 67 self.protocol_version = r'HTTP/1.1'
68 68 httpservermod.basehttprequesthandler.__init__(self, *args, **kargs)
69 69
70 70 def _log_any(self, fp, format, *args):
71 71 fp.write(pycompat.sysbytes(
72 72 r"%s - - [%s] %s" % (self.client_address[0],
73 73 self.log_date_time_string(),
74 74 format % args)) + '\n')
75 75 fp.flush()
76 76
77 77 def log_error(self, format, *args):
78 78 self._log_any(self.server.errorlog, format, *args)
79 79
80 80 def log_message(self, format, *args):
81 81 self._log_any(self.server.accesslog, format, *args)
82 82
83 83 def log_request(self, code=r'-', size=r'-'):
84 84 xheaders = []
85 85 if util.safehasattr(self, 'headers'):
86 86 xheaders = [h for h in self.headers.items()
87 87 if h[0].startswith(r'x-')]
88 88 self.log_message(r'"%s" %s %s%s',
89 89 self.requestline, str(code), str(size),
90 90 r''.join([r' %s:%s' % h for h in sorted(xheaders)]))
91 91
92 92 def do_write(self):
93 93 try:
94 94 self.do_hgweb()
95 95 except socket.error as inst:
96 96 if inst[0] != errno.EPIPE:
97 97 raise
98 98
99 99 def do_POST(self):
100 100 try:
101 101 self.do_write()
102 102 except Exception:
103 103 self._start_response("500 Internal Server Error", [])
104 104 self._write("Internal Server Error")
105 105 self._done()
106 106 tb = r"".join(traceback.format_exception(*sys.exc_info()))
107 107 # We need a native-string newline to poke in the log
108 108 # message, because we won't get a newline when using an
109 109 # r-string. This is the easy way out.
110 110 newline = chr(10)
111 111 self.log_error(r"Exception happened during processing "
112 112 r"request '%s':%s%s", self.path, newline, tb)
113 113
114 114 def do_GET(self):
115 115 self.do_POST()
116 116
117 117 def do_hgweb(self):
118 118 self.sent_headers = False
119 119 path, query = _splitURI(self.path)
120 120
121 121 env = {}
122 122 env[r'GATEWAY_INTERFACE'] = r'CGI/1.1'
123 123 env[r'REQUEST_METHOD'] = self.command
124 124 env[r'SERVER_NAME'] = self.server.server_name
125 125 env[r'SERVER_PORT'] = str(self.server.server_port)
126 126 env[r'REQUEST_URI'] = self.path
127 env[r'SCRIPT_NAME'] = self.server.prefix
128 env[r'PATH_INFO'] = path[len(self.server.prefix):]
127 env[r'SCRIPT_NAME'] = pycompat.sysstr(self.server.prefix)
128 env[r'PATH_INFO'] = pycompat.sysstr(path[len(self.server.prefix):])
129 129 env[r'REMOTE_HOST'] = self.client_address[0]
130 130 env[r'REMOTE_ADDR'] = self.client_address[0]
131 131 if query:
132 132 env[r'QUERY_STRING'] = query
133 133
134 134 if pycompat.ispy3:
135 135 if self.headers.get_content_type() is None:
136 136 env[r'CONTENT_TYPE'] = self.headers.get_default_type()
137 137 else:
138 138 env[r'CONTENT_TYPE'] = self.headers.get_content_type()
139 139 length = self.headers.get('content-length')
140 140 else:
141 141 if self.headers.typeheader is None:
142 142 env[r'CONTENT_TYPE'] = self.headers.type
143 143 else:
144 144 env[r'CONTENT_TYPE'] = self.headers.typeheader
145 145 length = self.headers.getheader('content-length')
146 146 if length:
147 147 env[r'CONTENT_LENGTH'] = length
148 148 for header in [h for h in self.headers.keys()
149 149 if h not in ('content-type', 'content-length')]:
150 150 hkey = r'HTTP_' + header.replace(r'-', r'_').upper()
151 151 hval = self.headers.get(header)
152 152 hval = hval.replace(r'\n', r'').strip()
153 153 if hval:
154 154 env[hkey] = hval
155 155 env[r'SERVER_PROTOCOL'] = self.request_version
156 156 env[r'wsgi.version'] = (1, 0)
157 env[r'wsgi.url_scheme'] = self.url_scheme
157 env[r'wsgi.url_scheme'] = pycompat.sysstr(self.url_scheme)
158 158 if env.get(r'HTTP_EXPECT', '').lower() == '100-continue':
159 159 self.rfile = common.continuereader(self.rfile, self.wfile.write)
160 160
161 161 env[r'wsgi.input'] = self.rfile
162 162 env[r'wsgi.errors'] = _error_logger(self)
163 163 env[r'wsgi.multithread'] = isinstance(self.server,
164 164 socketserver.ThreadingMixIn)
165 165 env[r'wsgi.multiprocess'] = isinstance(self.server,
166 166 socketserver.ForkingMixIn)
167 167 env[r'wsgi.run_once'] = 0
168 168
169 169 self.saved_status = None
170 170 self.saved_headers = []
171 171 self.length = None
172 172 self._chunked = None
173 173 for chunk in self.server.application(env, self._start_response):
174 174 self._write(chunk)
175 175 if not self.sent_headers:
176 176 self.send_headers()
177 177 self._done()
178 178
179 179 def send_headers(self):
180 180 if not self.saved_status:
181 181 raise AssertionError("Sending headers before "
182 182 "start_response() called")
183 183 saved_status = self.saved_status.split(None, 1)
184 184 saved_status[0] = int(saved_status[0])
185 185 self.send_response(*saved_status)
186 186 self.length = None
187 187 self._chunked = False
188 188 for h in self.saved_headers:
189 189 self.send_header(*h)
190 190 if h[0].lower() == 'content-length':
191 191 self.length = int(h[1])
192 192 if (self.length is None and
193 193 saved_status[0] != common.HTTP_NOT_MODIFIED):
194 194 self._chunked = (not self.close_connection and
195 195 self.request_version == "HTTP/1.1")
196 196 if self._chunked:
197 197 self.send_header(r'Transfer-Encoding', r'chunked')
198 198 else:
199 199 self.send_header(r'Connection', r'close')
200 200 self.end_headers()
201 201 self.sent_headers = True
202 202
203 203 def _start_response(self, http_status, headers, exc_info=None):
204 204 code, msg = http_status.split(None, 1)
205 205 code = int(code)
206 206 self.saved_status = http_status
207 207 bad_headers = ('connection', 'transfer-encoding')
208 208 self.saved_headers = [h for h in headers
209 209 if h[0].lower() not in bad_headers]
210 210 return self._write
211 211
212 212 def _write(self, data):
213 213 if not self.saved_status:
214 214 raise AssertionError("data written before start_response() called")
215 215 elif not self.sent_headers:
216 216 self.send_headers()
217 217 if self.length is not None:
218 218 if len(data) > self.length:
219 219 raise AssertionError("Content-length header sent, but more "
220 220 "bytes than specified are being written.")
221 221 self.length = self.length - len(data)
222 222 elif self._chunked and data:
223 223 data = '%x\r\n%s\r\n' % (len(data), data)
224 224 self.wfile.write(data)
225 225 self.wfile.flush()
226 226
227 227 def _done(self):
228 228 if self._chunked:
229 229 self.wfile.write('0\r\n\r\n')
230 230 self.wfile.flush()
231 231
232 232 class _httprequesthandlerssl(_httprequesthandler):
233 233 """HTTPS handler based on Python's ssl module"""
234 234
235 235 url_scheme = 'https'
236 236
237 237 @staticmethod
238 238 def preparehttpserver(httpserver, ui):
239 239 try:
240 240 from .. import sslutil
241 241 sslutil.modernssl
242 242 except ImportError:
243 243 raise error.Abort(_("SSL support is unavailable"))
244 244
245 245 certfile = ui.config('web', 'certificate')
246 246
247 247 # These config options are currently only meant for testing. Use
248 248 # at your own risk.
249 249 cafile = ui.config('devel', 'servercafile')
250 250 reqcert = ui.configbool('devel', 'serverrequirecert')
251 251
252 252 httpserver.socket = sslutil.wrapserversocket(httpserver.socket,
253 253 ui,
254 254 certfile=certfile,
255 255 cafile=cafile,
256 256 requireclientcert=reqcert)
257 257
258 258 def setup(self):
259 259 self.connection = self.request
260 260 self.rfile = self.request.makefile(r"rb", self.rbufsize)
261 261 self.wfile = self.request.makefile(r"wb", self.wbufsize)
262 262
263 263 try:
264 264 import threading
265 265 threading.activeCount() # silence pyflakes and bypass demandimport
266 266 _mixin = socketserver.ThreadingMixIn
267 267 except ImportError:
268 268 if util.safehasattr(os, "fork"):
269 269 _mixin = socketserver.ForkingMixIn
270 270 else:
271 271 class _mixin(object):
272 272 pass
273 273
274 274 def openlog(opt, default):
275 275 if opt and opt != '-':
276 276 return open(opt, 'ab')
277 277 return default
278 278
279 279 class MercurialHTTPServer(_mixin, httpservermod.httpserver, object):
280 280
281 281 # SO_REUSEADDR has broken semantics on windows
282 282 if pycompat.iswindows:
283 283 allow_reuse_address = 0
284 284
285 285 def __init__(self, ui, app, addr, handler, **kwargs):
286 286 httpservermod.httpserver.__init__(self, addr, handler, **kwargs)
287 287 self.daemon_threads = True
288 288 self.application = app
289 289
290 290 handler.preparehttpserver(self, ui)
291 291
292 292 prefix = ui.config('web', 'prefix')
293 293 if prefix:
294 294 prefix = '/' + prefix.strip('/')
295 295 self.prefix = prefix
296 296
297 297 alog = openlog(ui.config('web', 'accesslog'), ui.fout)
298 298 elog = openlog(ui.config('web', 'errorlog'), ui.ferr)
299 299 self.accesslog = alog
300 300 self.errorlog = elog
301 301
302 302 self.addr, self.port = self.socket.getsockname()[0:2]
303 303 self.fqaddr = socket.getfqdn(addr[0])
304 304
305 305 class IPv6HTTPServer(MercurialHTTPServer):
306 306 address_family = getattr(socket, 'AF_INET6', None)
307 307 def __init__(self, *args, **kwargs):
308 308 if self.address_family is None:
309 309 raise error.RepoError(_('IPv6 is not available on this system'))
310 310 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
311 311
312 312 def create_server(ui, app):
313 313
314 314 if ui.config('web', 'certificate'):
315 315 handler = _httprequesthandlerssl
316 316 else:
317 317 handler = _httprequesthandler
318 318
319 319 if ui.configbool('web', 'ipv6'):
320 320 cls = IPv6HTTPServer
321 321 else:
322 322 cls = MercurialHTTPServer
323 323
324 324 # ugly hack due to python issue5853 (for threaded use)
325 325 try:
326 326 import mimetypes
327 327 mimetypes.init()
328 328 except UnicodeDecodeError:
329 329 # Python 2.x's mimetypes module attempts to decode strings
330 330 # from Windows' ANSI APIs as ascii (fail), then re-encode them
331 331 # as ascii (clown fail), because the default Python Unicode
332 332 # codec is hardcoded as ascii.
333 333
334 334 sys.argv # unwrap demand-loader so that reload() works
335 335 reload(sys) # resurrect sys.setdefaultencoding()
336 336 oldenc = sys.getdefaultencoding()
337 337 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
338 338 mimetypes.init()
339 339 sys.setdefaultencoding(oldenc)
340 340
341 341 address = ui.config('web', 'address')
342 342 port = util.getport(ui.config('web', 'port'))
343 343 try:
344 344 return cls(ui, app, (address, port), handler)
345 345 except socket.error as inst:
346 346 raise error.Abort(_("cannot start server at '%s:%d': %s")
347 347 % (address, port, encoding.strtolocal(inst.args[1])))
General Comments 0
You need to be logged in to leave comments. Login now