##// END OF EJS Templates
hgweb: more "http headers are native strs" cleanup...
Augie Fackler -
r34741:b2601c59 default
parent child Browse files
Show More
@@ -1,491 +1,491 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 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 )
40 40
41 41 from . import (
42 42 protocol,
43 43 webcommands,
44 44 webutil,
45 45 wsgicgi,
46 46 )
47 47
48 48 perms = {
49 49 'changegroup': 'pull',
50 50 'changegroupsubset': 'pull',
51 51 'getbundle': 'pull',
52 52 'stream_out': 'pull',
53 53 'listkeys': 'pull',
54 54 'unbundle': 'push',
55 55 'pushkey': 'push',
56 56 }
57 57
58 58 archivespecs = util.sortdict((
59 59 ('zip', ('application/zip', 'zip', '.zip', None)),
60 60 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
61 61 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
62 62 ))
63 63
64 64 def getstyle(req, configfn, templatepath):
65 65 fromreq = req.form.get('style', [None])[0]
66 66 if fromreq is not None:
67 67 fromreq = pycompat.sysbytes(fromreq)
68 68 styles = (
69 69 fromreq,
70 70 configfn('web', 'style'),
71 71 'paper',
72 72 )
73 73 return styles, templater.stylemap(styles, templatepath)
74 74
75 75 def makebreadcrumb(url, prefix=''):
76 76 '''Return a 'URL breadcrumb' list
77 77
78 78 A 'URL breadcrumb' is a list of URL-name pairs,
79 79 corresponding to each of the path items on a URL.
80 80 This can be used to create path navigation entries.
81 81 '''
82 82 if url.endswith('/'):
83 83 url = url[:-1]
84 84 if prefix:
85 85 url = '/' + prefix + url
86 86 relpath = url
87 87 if relpath.startswith('/'):
88 88 relpath = relpath[1:]
89 89
90 90 breadcrumb = []
91 91 urlel = url
92 92 pathitems = [''] + relpath.split('/')
93 93 for pathel in reversed(pathitems):
94 94 if not pathel or not urlel:
95 95 break
96 96 breadcrumb.append({'url': urlel, 'name': pathel})
97 97 urlel = os.path.dirname(urlel)
98 98 return reversed(breadcrumb)
99 99
100 100 class requestcontext(object):
101 101 """Holds state/context for an individual request.
102 102
103 103 Servers can be multi-threaded. Holding state on the WSGI application
104 104 is prone to race conditions. Instances of this class exist to hold
105 105 mutable and race-free state for requests.
106 106 """
107 107 def __init__(self, app, repo):
108 108 self.repo = repo
109 109 self.reponame = app.reponame
110 110
111 111 self.archivespecs = archivespecs
112 112
113 113 self.maxchanges = self.configint('web', 'maxchanges')
114 114 self.stripecount = self.configint('web', 'stripes')
115 115 self.maxshortchanges = self.configint('web', 'maxshortchanges')
116 116 self.maxfiles = self.configint('web', 'maxfiles')
117 117 self.allowpull = self.configbool('web', 'allowpull')
118 118
119 119 # we use untrusted=False to prevent a repo owner from using
120 120 # web.templates in .hg/hgrc to get access to any file readable
121 121 # by the user running the CGI script
122 122 self.templatepath = self.config('web', 'templates', untrusted=False)
123 123
124 124 # This object is more expensive to build than simple config values.
125 125 # It is shared across requests. The app will replace the object
126 126 # if it is updated. Since this is a reference and nothing should
127 127 # modify the underlying object, it should be constant for the lifetime
128 128 # of the request.
129 129 self.websubtable = app.websubtable
130 130
131 131 self.csp, self.nonce = cspvalues(self.repo.ui)
132 132
133 133 # Trust the settings from the .hg/hgrc files by default.
134 134 def config(self, section, name, default=uimod._unset, untrusted=True):
135 135 return self.repo.ui.config(section, name, default,
136 136 untrusted=untrusted)
137 137
138 138 def configbool(self, section, name, default=uimod._unset, untrusted=True):
139 139 return self.repo.ui.configbool(section, name, default,
140 140 untrusted=untrusted)
141 141
142 142 def configint(self, section, name, default=uimod._unset, untrusted=True):
143 143 return self.repo.ui.configint(section, name, default,
144 144 untrusted=untrusted)
145 145
146 146 def configlist(self, section, name, default=uimod._unset, untrusted=True):
147 147 return self.repo.ui.configlist(section, name, default,
148 148 untrusted=untrusted)
149 149
150 150 def archivelist(self, nodeid):
151 151 allowed = self.configlist('web', 'allow_archive')
152 152 for typ, spec in self.archivespecs.iteritems():
153 153 if typ in allowed or self.configbool('web', 'allow%s' % typ):
154 154 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
155 155
156 156 def templater(self, req):
157 157 # determine scheme, port and server name
158 158 # this is needed to create absolute urls
159 159
160 160 proto = req.env.get('wsgi.url_scheme')
161 161 if proto == 'https':
162 162 proto = 'https'
163 163 default_port = '443'
164 164 else:
165 165 proto = 'http'
166 166 default_port = '80'
167 167
168 168 port = req.env[r'SERVER_PORT']
169 169 port = port != default_port and (r':' + port) or r''
170 170 urlbase = r'%s://%s%s' % (proto, req.env[r'SERVER_NAME'], port)
171 171 logourl = self.config('web', 'logourl')
172 172 logoimg = self.config('web', 'logoimg')
173 173 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
174 174 if not staticurl.endswith('/'):
175 175 staticurl += '/'
176 176
177 177 # some functions for the templater
178 178
179 179 def motd(**map):
180 180 yield self.config('web', 'motd')
181 181
182 182 # figure out which style to use
183 183
184 184 vars = {}
185 185 styles, (style, mapfile) = getstyle(req, self.config,
186 186 self.templatepath)
187 187 if style == styles[0]:
188 188 vars['style'] = style
189 189
190 190 start = r'&' if req.url[-1] == r'?' else r'?'
191 191 sessionvars = webutil.sessionvars(vars, start)
192 192
193 193 if not self.reponame:
194 194 self.reponame = (self.config('web', 'name', '')
195 195 or req.env.get('REPO_NAME')
196 196 or req.url.strip('/') or self.repo.root)
197 197
198 198 def websubfilter(text):
199 199 return templatefilters.websub(text, self.websubtable)
200 200
201 201 # create the templater
202 202
203 203 defaults = {
204 204 'url': req.url,
205 205 'logourl': logourl,
206 206 'logoimg': logoimg,
207 207 'staticurl': staticurl,
208 208 'urlbase': urlbase,
209 209 'repo': self.reponame,
210 210 'encoding': encoding.encoding,
211 211 'motd': motd,
212 212 'sessionvars': sessionvars,
213 213 'pathdef': makebreadcrumb(req.url),
214 214 'style': style,
215 215 'nonce': self.nonce,
216 216 }
217 217 tmpl = templater.templater.frommapfile(mapfile,
218 218 filters={'websub': websubfilter},
219 219 defaults=defaults)
220 220 return tmpl
221 221
222 222
223 223 class hgweb(object):
224 224 """HTTP server for individual repositories.
225 225
226 226 Instances of this class serve HTTP responses for a particular
227 227 repository.
228 228
229 229 Instances are typically used as WSGI applications.
230 230
231 231 Some servers are multi-threaded. On these servers, there may
232 232 be multiple active threads inside __call__.
233 233 """
234 234 def __init__(self, repo, name=None, baseui=None):
235 235 if isinstance(repo, str):
236 236 if baseui:
237 237 u = baseui.copy()
238 238 else:
239 239 u = uimod.ui.load()
240 240 r = hg.repository(u, repo)
241 241 else:
242 242 # we trust caller to give us a private copy
243 243 r = repo
244 244
245 245 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
246 246 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
247 247 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
248 248 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
249 249 # resolve file patterns relative to repo root
250 250 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
251 251 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
252 252 # displaying bundling progress bar while serving feel wrong and may
253 253 # break some wsgi implementation.
254 254 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
255 255 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
256 256 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
257 257 self._lastrepo = self._repos[0]
258 258 hook.redirect(True)
259 259 self.reponame = name
260 260
261 261 def _webifyrepo(self, repo):
262 262 repo = getwebview(repo)
263 263 self.websubtable = webutil.getwebsubs(repo)
264 264 return repo
265 265
266 266 @contextlib.contextmanager
267 267 def _obtainrepo(self):
268 268 """Obtain a repo unique to the caller.
269 269
270 270 Internally we maintain a stack of cachedlocalrepo instances
271 271 to be handed out. If one is available, we pop it and return it,
272 272 ensuring it is up to date in the process. If one is not available,
273 273 we clone the most recently used repo instance and return it.
274 274
275 275 It is currently possible for the stack to grow without bounds
276 276 if the server allows infinite threads. However, servers should
277 277 have a thread limit, thus establishing our limit.
278 278 """
279 279 if self._repos:
280 280 cached = self._repos.pop()
281 281 r, created = cached.fetch()
282 282 else:
283 283 cached = self._lastrepo.copy()
284 284 r, created = cached.fetch()
285 285 if created:
286 286 r = self._webifyrepo(r)
287 287
288 288 self._lastrepo = cached
289 289 self.mtime = cached.mtime
290 290 try:
291 291 yield r
292 292 finally:
293 293 self._repos.append(cached)
294 294
295 295 def run(self):
296 296 """Start a server from CGI environment.
297 297
298 298 Modern servers should be using WSGI and should avoid this
299 299 method, if possible.
300 300 """
301 301 if not encoding.environ.get('GATEWAY_INTERFACE',
302 302 '').startswith("CGI/1."):
303 303 raise RuntimeError("This function is only intended to be "
304 304 "called while running as a CGI script.")
305 305 wsgicgi.launch(self)
306 306
307 307 def __call__(self, env, respond):
308 308 """Run the WSGI application.
309 309
310 310 This may be called by multiple threads.
311 311 """
312 312 req = wsgirequest(env, respond)
313 313 return self.run_wsgi(req)
314 314
315 315 def run_wsgi(self, req):
316 316 """Internal method to run the WSGI application.
317 317
318 318 This is typically only called by Mercurial. External consumers
319 319 should be using instances of this class as the WSGI application.
320 320 """
321 321 with self._obtainrepo() as repo:
322 322 profile = repo.ui.configbool('profiling', 'enabled')
323 323 with profiling.profile(repo.ui, enabled=profile):
324 324 for r in self._runwsgi(req, repo):
325 325 yield r
326 326
327 327 def _runwsgi(self, req, repo):
328 328 rctx = requestcontext(self, repo)
329 329
330 330 # This state is global across all threads.
331 331 encoding.encoding = rctx.config('web', 'encoding')
332 332 rctx.repo.ui.environ = req.env
333 333
334 334 if rctx.csp:
335 335 # hgwebdir may have added CSP header. Since we generate our own,
336 336 # replace it.
337 337 req.headers = [h for h in req.headers
338 338 if h[0] != 'Content-Security-Policy']
339 339 req.headers.append(('Content-Security-Policy', rctx.csp))
340 340
341 341 # work with CGI variables to create coherent structure
342 342 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
343 343
344 344 req.url = req.env[r'SCRIPT_NAME']
345 345 if not req.url.endswith('/'):
346 346 req.url += '/'
347 347 if req.env.get('REPO_NAME'):
348 348 req.url += req.env[r'REPO_NAME'] + r'/'
349 349
350 350 if r'PATH_INFO' in req.env:
351 351 parts = req.env[r'PATH_INFO'].strip('/').split('/')
352 352 repo_parts = req.env.get(r'REPO_NAME', r'').split(r'/')
353 353 if parts[:len(repo_parts)] == repo_parts:
354 354 parts = parts[len(repo_parts):]
355 355 query = '/'.join(parts)
356 356 else:
357 357 query = req.env[r'QUERY_STRING'].partition(r'&')[0]
358 358 query = query.partition(r';')[0]
359 359
360 360 # process this if it's a protocol request
361 361 # protocol bits don't need to create any URLs
362 362 # and the clients always use the old URL structure
363 363
364 364 cmd = pycompat.sysbytes(req.form.get(r'cmd', [r''])[0])
365 365 if protocol.iscmd(cmd):
366 366 try:
367 367 if query:
368 368 raise ErrorResponse(HTTP_NOT_FOUND)
369 369 if cmd in perms:
370 370 self.check_perm(rctx, req, perms[cmd])
371 371 return protocol.call(rctx.repo, req, cmd)
372 372 except ErrorResponse as inst:
373 373 # A client that sends unbundle without 100-continue will
374 374 # break if we respond early.
375 375 if (cmd == 'unbundle' and
376 376 (req.env.get('HTTP_EXPECT',
377 377 '').lower() != '100-continue') or
378 378 req.env.get('X-HgHttp2', '')):
379 379 req.drain()
380 380 else:
381 req.headers.append(('Connection', 'Close'))
381 req.headers.append((r'Connection', r'Close'))
382 382 req.respond(inst, protocol.HGTYPE,
383 383 body='0\n%s\n' % inst)
384 384 return ''
385 385
386 386 # translate user-visible url structure to internal structure
387 387
388 388 args = query.split('/', 2)
389 389 if r'cmd' not in req.form and args and args[0]:
390 390 cmd = args.pop(0)
391 391 style = cmd.rfind('-')
392 392 if style != -1:
393 393 req.form['style'] = [cmd[:style]]
394 394 cmd = cmd[style + 1:]
395 395
396 396 # avoid accepting e.g. style parameter as command
397 397 if util.safehasattr(webcommands, cmd):
398 398 req.form[r'cmd'] = [cmd]
399 399
400 400 if cmd == 'static':
401 401 req.form['file'] = ['/'.join(args)]
402 402 else:
403 403 if args and args[0]:
404 404 node = args.pop(0).replace('%2F', '/')
405 405 req.form['node'] = [node]
406 406 if args:
407 407 req.form['file'] = args
408 408
409 409 ua = req.env.get('HTTP_USER_AGENT', '')
410 410 if cmd == 'rev' and 'mercurial' in ua:
411 411 req.form['style'] = ['raw']
412 412
413 413 if cmd == 'archive':
414 414 fn = req.form['node'][0]
415 415 for type_, spec in rctx.archivespecs.iteritems():
416 416 ext = spec[2]
417 417 if fn.endswith(ext):
418 418 req.form['node'] = [fn[:-len(ext)]]
419 419 req.form['type'] = [type_]
420 420
421 421 # process the web interface request
422 422
423 423 try:
424 424 tmpl = rctx.templater(req)
425 425 ctype = tmpl('mimetype', encoding=encoding.encoding)
426 426 ctype = templater.stringify(ctype)
427 427
428 428 # check read permissions non-static content
429 429 if cmd != 'static':
430 430 self.check_perm(rctx, req, None)
431 431
432 432 if cmd == '':
433 433 req.form[r'cmd'] = [tmpl.cache['default']]
434 434 cmd = req.form[r'cmd'][0]
435 435
436 436 # Don't enable caching if using a CSP nonce because then it wouldn't
437 437 # be a nonce.
438 438 if rctx.configbool('web', 'cache') and not rctx.nonce:
439 439 caching(self, req) # sets ETag header or raises NOT_MODIFIED
440 440 if cmd not in webcommands.__all__:
441 441 msg = 'no such method: %s' % cmd
442 442 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
443 443 elif cmd == 'file' and r'raw' in req.form.get(r'style', []):
444 444 rctx.ctype = ctype
445 445 content = webcommands.rawfile(rctx, req, tmpl)
446 446 else:
447 447 content = getattr(webcommands, cmd)(rctx, req, tmpl)
448 448 req.respond(HTTP_OK, ctype)
449 449
450 450 return content
451 451
452 452 except (error.LookupError, error.RepoLookupError) as err:
453 453 req.respond(HTTP_NOT_FOUND, ctype)
454 454 msg = str(err)
455 455 if (util.safehasattr(err, 'name') and
456 456 not isinstance(err, error.ManifestLookupError)):
457 457 msg = 'revision not found: %s' % err.name
458 458 return tmpl('error', error=msg)
459 459 except (error.RepoError, error.RevlogError) as inst:
460 460 req.respond(HTTP_SERVER_ERROR, ctype)
461 461 return tmpl('error', error=str(inst))
462 462 except ErrorResponse as inst:
463 463 req.respond(inst, ctype)
464 464 if inst.code == HTTP_NOT_MODIFIED:
465 465 # Not allowed to return a body on a 304
466 466 return ['']
467 467 return tmpl('error', error=str(inst))
468 468
469 469 def check_perm(self, rctx, req, op):
470 470 for permhook in permhooks:
471 471 permhook(rctx, req, op)
472 472
473 473 def getwebview(repo):
474 474 """The 'web.view' config controls changeset filter to hgweb. Possible
475 475 values are ``served``, ``visible`` and ``all``. Default is ``served``.
476 476 The ``served`` filter only shows changesets that can be pulled from the
477 477 hgweb instance. The``visible`` filter includes secret changesets but
478 478 still excludes "hidden" one.
479 479
480 480 See the repoview module for details.
481 481
482 482 The option has been around undocumented since Mercurial 2.5, but no
483 483 user ever asked about it. So we better keep it undocumented for now."""
484 484 # experimental config: web.view
485 485 viewconfig = repo.ui.config('web', 'view', untrusted=True)
486 486 if viewconfig == 'all':
487 487 return repo.unfiltered()
488 488 elif viewconfig in repoview.filtertable:
489 489 return repo.filtered(viewconfig)
490 490 else:
491 491 return repo.filtered('served')
@@ -1,203 +1,203 b''
1 1 #
2 2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import cgi
11 11 import struct
12 12
13 13 from .common import (
14 14 HTTP_OK,
15 15 )
16 16
17 17 from .. import (
18 18 error,
19 19 util,
20 20 wireproto,
21 21 )
22 22 stringio = util.stringio
23 23
24 24 urlerr = util.urlerr
25 25 urlreq = util.urlreq
26 26
27 27 HGTYPE = 'application/mercurial-0.1'
28 28 HGTYPE2 = 'application/mercurial-0.2'
29 29 HGERRTYPE = 'application/hg-error'
30 30
31 31 def decodevaluefromheaders(req, headerprefix):
32 32 """Decode a long value from multiple HTTP request headers."""
33 33 chunks = []
34 34 i = 1
35 35 while True:
36 36 v = req.env.get('HTTP_%s_%d' % (
37 37 headerprefix.upper().replace('-', '_'), i))
38 38 if v is None:
39 39 break
40 40 chunks.append(v)
41 41 i += 1
42 42
43 43 return ''.join(chunks)
44 44
45 45 class webproto(wireproto.abstractserverproto):
46 46 def __init__(self, req, ui):
47 47 self.req = req
48 48 self.response = ''
49 49 self.ui = ui
50 50 self.name = 'http'
51 51
52 52 def getargs(self, args):
53 53 knownargs = self._args()
54 54 data = {}
55 55 keys = args.split()
56 56 for k in keys:
57 57 if k == '*':
58 58 star = {}
59 59 for key in knownargs.keys():
60 60 if key != 'cmd' and key not in keys:
61 61 star[key] = knownargs[key][0]
62 62 data['*'] = star
63 63 else:
64 64 data[k] = knownargs[k][0]
65 65 return [data[k] for k in keys]
66 66 def _args(self):
67 67 args = self.req.form.copy()
68 68 postlen = int(self.req.env.get('HTTP_X_HGARGS_POST', 0))
69 69 if postlen:
70 70 args.update(cgi.parse_qs(
71 71 self.req.read(postlen), keep_blank_values=True))
72 72 return args
73 73
74 74 argvalue = decodevaluefromheaders(self.req, 'X-HgArg')
75 75 args.update(cgi.parse_qs(argvalue, keep_blank_values=True))
76 76 return args
77 77 def getfile(self, fp):
78 length = int(self.req.env['CONTENT_LENGTH'])
78 length = int(self.req.env[r'CONTENT_LENGTH'])
79 79 # If httppostargs is used, we need to read Content-Length
80 80 # minus the amount that was consumed by args.
81 length -= int(self.req.env.get('HTTP_X_HGARGS_POST', 0))
81 length -= int(self.req.env.get(r'HTTP_X_HGARGS_POST', 0))
82 82 for s in util.filechunkiter(self.req, limit=length):
83 83 fp.write(s)
84 84 def redirect(self):
85 85 self.oldio = self.ui.fout, self.ui.ferr
86 86 self.ui.ferr = self.ui.fout = stringio()
87 87 def restore(self):
88 88 val = self.ui.fout.getvalue()
89 89 self.ui.ferr, self.ui.fout = self.oldio
90 90 return val
91 91
92 92 def _client(self):
93 93 return 'remote:%s:%s:%s' % (
94 94 self.req.env.get('wsgi.url_scheme') or 'http',
95 95 urlreq.quote(self.req.env.get('REMOTE_HOST', '')),
96 96 urlreq.quote(self.req.env.get('REMOTE_USER', '')))
97 97
98 98 def responsetype(self, v1compressible=False):
99 99 """Determine the appropriate response type and compression settings.
100 100
101 101 The ``v1compressible`` argument states whether the response with
102 102 application/mercurial-0.1 media types should be zlib compressed.
103 103
104 104 Returns a tuple of (mediatype, compengine, engineopts).
105 105 """
106 106 # For now, if it isn't compressible in the old world, it's never
107 107 # compressible. We can change this to send uncompressed 0.2 payloads
108 108 # later.
109 109 if not v1compressible:
110 110 return HGTYPE, None, None
111 111
112 112 # Determine the response media type and compression engine based
113 113 # on the request parameters.
114 114 protocaps = decodevaluefromheaders(self.req, 'X-HgProto').split(' ')
115 115
116 116 if '0.2' in protocaps:
117 117 # Default as defined by wire protocol spec.
118 118 compformats = ['zlib', 'none']
119 119 for cap in protocaps:
120 120 if cap.startswith('comp='):
121 121 compformats = cap[5:].split(',')
122 122 break
123 123
124 124 # Now find an agreed upon compression format.
125 125 for engine in wireproto.supportedcompengines(self.ui, self,
126 126 util.SERVERROLE):
127 127 if engine.wireprotosupport().name in compformats:
128 128 opts = {}
129 129 level = self.ui.configint('server',
130 130 '%slevel' % engine.name())
131 131 if level is not None:
132 132 opts['level'] = level
133 133
134 134 return HGTYPE2, engine, opts
135 135
136 136 # No mutually supported compression format. Fall back to the
137 137 # legacy protocol.
138 138
139 139 # Don't allow untrusted settings because disabling compression or
140 140 # setting a very high compression level could lead to flooding
141 141 # the server's network or CPU.
142 142 opts = {'level': self.ui.configint('server', 'zliblevel')}
143 143 return HGTYPE, util.compengines['zlib'], opts
144 144
145 145 def iscmd(cmd):
146 146 return cmd in wireproto.commands
147 147
148 148 def call(repo, req, cmd):
149 149 p = webproto(req, repo.ui)
150 150
151 151 def genversion2(gen, compress, engine, engineopts):
152 152 # application/mercurial-0.2 always sends a payload header
153 153 # identifying the compression engine.
154 154 name = engine.wireprotosupport().name
155 155 assert 0 < len(name) < 256
156 156 yield struct.pack('B', len(name))
157 157 yield name
158 158
159 159 if compress:
160 160 for chunk in engine.compressstream(gen, opts=engineopts):
161 161 yield chunk
162 162 else:
163 163 for chunk in gen:
164 164 yield chunk
165 165
166 166 rsp = wireproto.dispatch(repo, p, cmd)
167 167 if isinstance(rsp, bytes):
168 168 req.respond(HTTP_OK, HGTYPE, body=rsp)
169 169 return []
170 170 elif isinstance(rsp, wireproto.streamres):
171 171 if rsp.reader:
172 172 gen = iter(lambda: rsp.reader.read(32768), '')
173 173 else:
174 174 gen = rsp.gen
175 175
176 176 # This code for compression should not be streamres specific. It
177 177 # is here because we only compress streamres at the moment.
178 178 mediatype, engine, engineopts = p.responsetype(rsp.v1compressible)
179 179
180 180 if mediatype == HGTYPE and rsp.v1compressible:
181 181 gen = engine.compressstream(gen, engineopts)
182 182 elif mediatype == HGTYPE2:
183 183 gen = genversion2(gen, rsp.v1compressible, engine, engineopts)
184 184
185 185 req.respond(HTTP_OK, mediatype)
186 186 return gen
187 187 elif isinstance(rsp, wireproto.pushres):
188 188 val = p.restore()
189 189 rsp = '%d\n%s' % (rsp.res, val)
190 190 req.respond(HTTP_OK, HGTYPE, body=rsp)
191 191 return []
192 192 elif isinstance(rsp, wireproto.pusherr):
193 193 # drain the incoming bundle
194 194 req.drain()
195 195 p.restore()
196 196 rsp = '0\n%s\n' % rsp.res
197 197 req.respond(HTTP_OK, HGTYPE, body=rsp)
198 198 return []
199 199 elif isinstance(rsp, wireproto.ooberror):
200 200 rsp = rsp.message
201 201 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
202 202 return []
203 203 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
@@ -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 127 env[r'SCRIPT_NAME'] = self.server.prefix
128 128 env[r'PATH_INFO'] = 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 157 env[r'wsgi.url_scheme'] = 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 self.send_header('Transfer-Encoding', 'chunked')
197 self.send_header(r'Transfer-Encoding', r'chunked')
198 198 else:
199 self.send_header('Connection', 'close')
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 = socket._fileobject(self.request, "rb", self.rbufsize)
261 261 self.wfile = socket._fileobject(self.request, "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, 'a')
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