##// END OF EJS Templates
py3: get bytes-repr of network errors portably...
Augie Fackler -
r36272:af0a19d8 default
parent child Browse files
Show More
@@ -1,483 +1,483 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 wireprotoserver,
40 40 )
41 41
42 42 from . import (
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', 'allow-pull')
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 = '&' if req.url[-1] == r'?' else '?'
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 # Route it to a wire protocol handler if it looks like a wire protocol
361 361 # request.
362 362 protohandler = wireprotoserver.parsehttprequest(rctx.repo, req, query)
363 363
364 364 if protohandler:
365 365 cmd = protohandler['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 except ErrorResponse as inst:
372 372 return protohandler['handleerror'](inst)
373 373
374 374 return protohandler['dispatch']()
375 375
376 376 # translate user-visible url structure to internal structure
377 377
378 378 args = query.split('/', 2)
379 379 if r'cmd' not in req.form and args and args[0]:
380 380 cmd = args.pop(0)
381 381 style = cmd.rfind('-')
382 382 if style != -1:
383 383 req.form['style'] = [cmd[:style]]
384 384 cmd = cmd[style + 1:]
385 385
386 386 # avoid accepting e.g. style parameter as command
387 387 if util.safehasattr(webcommands, cmd):
388 388 req.form[r'cmd'] = [cmd]
389 389
390 390 if cmd == 'static':
391 391 req.form['file'] = ['/'.join(args)]
392 392 else:
393 393 if args and args[0]:
394 394 node = args.pop(0).replace('%2F', '/')
395 395 req.form['node'] = [node]
396 396 if args:
397 397 req.form['file'] = args
398 398
399 399 ua = req.env.get('HTTP_USER_AGENT', '')
400 400 if cmd == 'rev' and 'mercurial' in ua:
401 401 req.form['style'] = ['raw']
402 402
403 403 if cmd == 'archive':
404 404 fn = req.form['node'][0]
405 405 for type_, spec in rctx.archivespecs.iteritems():
406 406 ext = spec[2]
407 407 if fn.endswith(ext):
408 408 req.form['node'] = [fn[:-len(ext)]]
409 409 req.form['type'] = [type_]
410 410 else:
411 411 cmd = pycompat.sysbytes(req.form.get(r'cmd', [r''])[0])
412 412
413 413 # process the web interface request
414 414
415 415 try:
416 416 tmpl = rctx.templater(req)
417 417 ctype = tmpl('mimetype', encoding=encoding.encoding)
418 418 ctype = templater.stringify(ctype)
419 419
420 420 # check read permissions non-static content
421 421 if cmd != 'static':
422 422 self.check_perm(rctx, req, None)
423 423
424 424 if cmd == '':
425 425 req.form[r'cmd'] = [tmpl.cache['default']]
426 426 cmd = req.form[r'cmd'][0]
427 427
428 428 # Don't enable caching if using a CSP nonce because then it wouldn't
429 429 # be a nonce.
430 430 if rctx.configbool('web', 'cache') and not rctx.nonce:
431 431 caching(self, req) # sets ETag header or raises NOT_MODIFIED
432 432 if cmd not in webcommands.__all__:
433 433 msg = 'no such method: %s' % cmd
434 434 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
435 435 elif cmd == 'file' and r'raw' in req.form.get(r'style', []):
436 436 rctx.ctype = ctype
437 437 content = webcommands.rawfile(rctx, req, tmpl)
438 438 else:
439 439 content = getattr(webcommands, cmd)(rctx, req, tmpl)
440 440 req.respond(HTTP_OK, ctype)
441 441
442 442 return content
443 443
444 444 except (error.LookupError, error.RepoLookupError) as err:
445 445 req.respond(HTTP_NOT_FOUND, ctype)
446 msg = str(err)
446 msg = pycompat.bytestr(err)
447 447 if (util.safehasattr(err, 'name') and
448 448 not isinstance(err, error.ManifestLookupError)):
449 449 msg = 'revision not found: %s' % err.name
450 450 return tmpl('error', error=msg)
451 451 except (error.RepoError, error.RevlogError) as inst:
452 452 req.respond(HTTP_SERVER_ERROR, ctype)
453 return tmpl('error', error=str(inst))
453 return tmpl('error', error=pycompat.bytestr(inst))
454 454 except ErrorResponse as inst:
455 455 req.respond(inst, ctype)
456 456 if inst.code == HTTP_NOT_MODIFIED:
457 457 # Not allowed to return a body on a 304
458 458 return ['']
459 return tmpl('error', error=str(inst))
459 return tmpl('error', error=pycompat.bytestr(inst))
460 460
461 461 def check_perm(self, rctx, req, op):
462 462 for permhook in permhooks:
463 463 permhook(rctx, req, op)
464 464
465 465 def getwebview(repo):
466 466 """The 'web.view' config controls changeset filter to hgweb. Possible
467 467 values are ``served``, ``visible`` and ``all``. Default is ``served``.
468 468 The ``served`` filter only shows changesets that can be pulled from the
469 469 hgweb instance. The``visible`` filter includes secret changesets but
470 470 still excludes "hidden" one.
471 471
472 472 See the repoview module for details.
473 473
474 474 The option has been around undocumented since Mercurial 2.5, but no
475 475 user ever asked about it. So we better keep it undocumented for now."""
476 476 # experimental config: web.view
477 477 viewconfig = repo.ui.config('web', 'view', untrusted=True)
478 478 if viewconfig == 'all':
479 479 return repo.unfiltered()
480 480 elif viewconfig in repoview.filtertable:
481 481 return repo.filtered(viewconfig)
482 482 else:
483 483 return repo.filtered('served')
@@ -1,155 +1,156 b''
1 1 # hgweb/request.py - An http request from either CGI or the standalone server.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import cgi
12 12 import errno
13 13 import socket
14 14
15 15 from .common import (
16 16 ErrorResponse,
17 17 HTTP_NOT_MODIFIED,
18 18 statusmessage,
19 19 )
20 20
21 21 from .. import (
22 22 pycompat,
23 23 util,
24 24 )
25 25
26 26 shortcuts = {
27 27 'cl': [('cmd', ['changelog']), ('rev', None)],
28 28 'sl': [('cmd', ['shortlog']), ('rev', None)],
29 29 'cs': [('cmd', ['changeset']), ('node', None)],
30 30 'f': [('cmd', ['file']), ('filenode', None)],
31 31 'fl': [('cmd', ['filelog']), ('filenode', None)],
32 32 'fd': [('cmd', ['filediff']), ('node', None)],
33 33 'fa': [('cmd', ['annotate']), ('filenode', None)],
34 34 'mf': [('cmd', ['manifest']), ('manifest', None)],
35 35 'ca': [('cmd', ['archive']), ('node', None)],
36 36 'tags': [('cmd', ['tags'])],
37 37 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
38 38 'static': [('cmd', ['static']), ('file', None)]
39 39 }
40 40
41 41 def normalize(form):
42 42 # first expand the shortcuts
43 43 for k in shortcuts:
44 44 if k in form:
45 45 for name, value in shortcuts[k]:
46 46 if value is None:
47 47 value = form[k]
48 48 form[name] = value
49 49 del form[k]
50 50 # And strip the values
51 51 for k, v in form.iteritems():
52 52 form[k] = [i.strip() for i in v]
53 53 return form
54 54
55 55 class wsgirequest(object):
56 56 """Higher-level API for a WSGI request.
57 57
58 58 WSGI applications are invoked with 2 arguments. They are used to
59 59 instantiate instances of this class, which provides higher-level APIs
60 60 for obtaining request parameters, writing HTTP output, etc.
61 61 """
62 62 def __init__(self, wsgienv, start_response):
63 63 version = wsgienv[r'wsgi.version']
64 64 if (version < (1, 0)) or (version >= (2, 0)):
65 65 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
66 66 % version)
67 67 self.inp = wsgienv[r'wsgi.input']
68 68 self.err = wsgienv[r'wsgi.errors']
69 69 self.threaded = wsgienv[r'wsgi.multithread']
70 70 self.multiprocess = wsgienv[r'wsgi.multiprocess']
71 71 self.run_once = wsgienv[r'wsgi.run_once']
72 72 self.env = wsgienv
73 73 self.form = normalize(cgi.parse(self.inp,
74 74 self.env,
75 75 keep_blank_values=1))
76 76 self._start_response = start_response
77 77 self.server_write = None
78 78 self.headers = []
79 79
80 80 def __iter__(self):
81 81 return iter([])
82 82
83 83 def read(self, count=-1):
84 84 return self.inp.read(count)
85 85
86 86 def drain(self):
87 87 '''need to read all data from request, httplib is half-duplex'''
88 88 length = int(self.env.get('CONTENT_LENGTH') or 0)
89 89 for s in util.filechunkiter(self.inp, limit=length):
90 90 pass
91 91
92 92 def respond(self, status, type, filename=None, body=None):
93 93 if not isinstance(type, str):
94 94 type = pycompat.sysstr(type)
95 95 if self._start_response is not None:
96 96 self.headers.append((r'Content-Type', type))
97 97 if filename:
98 98 filename = (filename.rpartition('/')[-1]
99 99 .replace('\\', '\\\\').replace('"', '\\"'))
100 100 self.headers.append(('Content-Disposition',
101 101 'inline; filename="%s"' % filename))
102 102 if body is not None:
103 103 self.headers.append((r'Content-Length', str(len(body))))
104 104
105 105 for k, v in self.headers:
106 106 if not isinstance(v, str):
107 107 raise TypeError('header value must be string: %r' % (v,))
108 108
109 109 if isinstance(status, ErrorResponse):
110 110 self.headers.extend(status.headers)
111 111 if status.code == HTTP_NOT_MODIFIED:
112 112 # RFC 2616 Section 10.3.5: 304 Not Modified has cases where
113 113 # it MUST NOT include any headers other than these and no
114 114 # body
115 115 self.headers = [(k, v) for (k, v) in self.headers if
116 116 k in ('Date', 'ETag', 'Expires',
117 117 'Cache-Control', 'Vary')]
118 118 status = statusmessage(status.code, pycompat.bytestr(status))
119 119 elif status == 200:
120 120 status = '200 Script output follows'
121 121 elif isinstance(status, int):
122 122 status = statusmessage(status)
123 123
124 self.server_write = self._start_response(status, self.headers)
124 self.server_write = self._start_response(
125 pycompat.sysstr(status), self.headers)
125 126 self._start_response = None
126 127 self.headers = []
127 128 if body is not None:
128 129 self.write(body)
129 130 self.server_write = None
130 131
131 132 def write(self, thing):
132 133 if thing:
133 134 try:
134 135 self.server_write(thing)
135 136 except socket.error as inst:
136 137 if inst[0] != errno.ECONNRESET:
137 138 raise
138 139
139 140 def writelines(self, lines):
140 141 for line in lines:
141 142 self.write(line)
142 143
143 144 def flush(self):
144 145 return None
145 146
146 147 def close(self):
147 148 return None
148 149
149 150 def wsgiapplication(app_maker):
150 151 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
151 152 can and should now be used as a WSGI application.'''
152 153 application = app_maker()
153 154 def run_wsgi(env, respond):
154 155 return application(env, respond)
155 156 return run_wsgi
@@ -1,654 +1,654 b''
1 1 # hgweb/webutil.py - utility library for the web interface.
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 copy
12 12 import difflib
13 13 import os
14 14 import re
15 15
16 16 from ..i18n import _
17 17 from ..node import hex, nullid, short
18 18
19 19 from .common import (
20 20 ErrorResponse,
21 21 HTTP_BAD_REQUEST,
22 22 HTTP_NOT_FOUND,
23 23 paritygen,
24 24 )
25 25
26 26 from .. import (
27 27 context,
28 28 error,
29 29 match,
30 30 mdiff,
31 31 patch,
32 32 pathutil,
33 33 pycompat,
34 34 templatefilters,
35 35 templatekw,
36 36 ui as uimod,
37 37 util,
38 38 )
39 39
40 40 def up(p):
41 41 if p[0] != "/":
42 42 p = "/" + p
43 43 if p[-1] == "/":
44 44 p = p[:-1]
45 45 up = os.path.dirname(p)
46 46 if up == "/":
47 47 return "/"
48 48 return up + "/"
49 49
50 50 def _navseq(step, firststep=None):
51 51 if firststep:
52 52 yield firststep
53 53 if firststep >= 20 and firststep <= 40:
54 54 firststep = 50
55 55 yield firststep
56 56 assert step > 0
57 57 assert firststep > 0
58 58 while step <= firststep:
59 59 step *= 10
60 60 while True:
61 61 yield 1 * step
62 62 yield 3 * step
63 63 step *= 10
64 64
65 65 class revnav(object):
66 66
67 67 def __init__(self, repo):
68 68 """Navigation generation object
69 69
70 70 :repo: repo object we generate nav for
71 71 """
72 72 # used for hex generation
73 73 self._revlog = repo.changelog
74 74
75 75 def __nonzero__(self):
76 76 """return True if any revision to navigate over"""
77 77 return self._first() is not None
78 78
79 79 __bool__ = __nonzero__
80 80
81 81 def _first(self):
82 82 """return the minimum non-filtered changeset or None"""
83 83 try:
84 84 return next(iter(self._revlog))
85 85 except StopIteration:
86 86 return None
87 87
88 88 def hex(self, rev):
89 89 return hex(self._revlog.node(rev))
90 90
91 91 def gen(self, pos, pagelen, limit):
92 92 """computes label and revision id for navigation link
93 93
94 94 :pos: is the revision relative to which we generate navigation.
95 95 :pagelen: the size of each navigation page
96 96 :limit: how far shall we link
97 97
98 98 The return is:
99 99 - a single element tuple
100 100 - containing a dictionary with a `before` and `after` key
101 101 - values are generator functions taking arbitrary number of kwargs
102 102 - yield items are dictionaries with `label` and `node` keys
103 103 """
104 104 if not self:
105 105 # empty repo
106 106 return ({'before': (), 'after': ()},)
107 107
108 108 targets = []
109 109 for f in _navseq(1, pagelen):
110 110 if f > limit:
111 111 break
112 112 targets.append(pos + f)
113 113 targets.append(pos - f)
114 114 targets.sort()
115 115
116 116 first = self._first()
117 117 navbefore = [("(%i)" % first, self.hex(first))]
118 118 navafter = []
119 119 for rev in targets:
120 120 if rev not in self._revlog:
121 121 continue
122 122 if pos < rev < limit:
123 123 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
124 124 if 0 < rev < pos:
125 125 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
126 126
127 127
128 128 navafter.append(("tip", "tip"))
129 129
130 130 data = lambda i: {"label": i[0], "node": i[1]}
131 131 return ({'before': lambda **map: (data(i) for i in navbefore),
132 132 'after': lambda **map: (data(i) for i in navafter)},)
133 133
134 134 class filerevnav(revnav):
135 135
136 136 def __init__(self, repo, path):
137 137 """Navigation generation object
138 138
139 139 :repo: repo object we generate nav for
140 140 :path: path of the file we generate nav for
141 141 """
142 142 # used for iteration
143 143 self._changelog = repo.unfiltered().changelog
144 144 # used for hex generation
145 145 self._revlog = repo.file(path)
146 146
147 147 def hex(self, rev):
148 148 return hex(self._changelog.node(self._revlog.linkrev(rev)))
149 149
150 150 class _siblings(object):
151 151 def __init__(self, siblings=None, hiderev=None):
152 152 if siblings is None:
153 153 siblings = []
154 154 self.siblings = [s for s in siblings if s.node() != nullid]
155 155 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
156 156 self.siblings = []
157 157
158 158 def __iter__(self):
159 159 for s in self.siblings:
160 160 d = {
161 161 'node': s.hex(),
162 162 'rev': s.rev(),
163 163 'user': s.user(),
164 164 'date': s.date(),
165 165 'description': s.description(),
166 166 'branch': s.branch(),
167 167 }
168 168 if util.safehasattr(s, 'path'):
169 169 d['file'] = s.path()
170 170 yield d
171 171
172 172 def __len__(self):
173 173 return len(self.siblings)
174 174
175 175 def difffeatureopts(req, ui, section):
176 176 diffopts = patch.difffeatureopts(ui, untrusted=True,
177 177 section=section, whitespace=True)
178 178
179 179 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
180 180 v = req.form.get(k, [None])[0]
181 181 if v is not None:
182 182 v = util.parsebool(v)
183 183 setattr(diffopts, k, v if v is not None else True)
184 184
185 185 return diffopts
186 186
187 187 def annotate(req, fctx, ui):
188 188 diffopts = difffeatureopts(req, ui, 'annotate')
189 189 return fctx.annotate(follow=True, linenumber=True, diffopts=diffopts)
190 190
191 191 def parents(ctx, hide=None):
192 192 if isinstance(ctx, context.basefilectx):
193 193 introrev = ctx.introrev()
194 194 if ctx.changectx().rev() != introrev:
195 195 return _siblings([ctx.repo()[introrev]], hide)
196 196 return _siblings(ctx.parents(), hide)
197 197
198 198 def children(ctx, hide=None):
199 199 return _siblings(ctx.children(), hide)
200 200
201 201 def renamelink(fctx):
202 202 r = fctx.renamed()
203 203 if r:
204 204 return [{'file': r[0], 'node': hex(r[1])}]
205 205 return []
206 206
207 207 def nodetagsdict(repo, node):
208 208 return [{"name": i} for i in repo.nodetags(node)]
209 209
210 210 def nodebookmarksdict(repo, node):
211 211 return [{"name": i} for i in repo.nodebookmarks(node)]
212 212
213 213 def nodebranchdict(repo, ctx):
214 214 branches = []
215 215 branch = ctx.branch()
216 216 # If this is an empty repo, ctx.node() == nullid,
217 217 # ctx.branch() == 'default'.
218 218 try:
219 219 branchnode = repo.branchtip(branch)
220 220 except error.RepoLookupError:
221 221 branchnode = None
222 222 if branchnode == ctx.node():
223 223 branches.append({"name": branch})
224 224 return branches
225 225
226 226 def nodeinbranch(repo, ctx):
227 227 branches = []
228 228 branch = ctx.branch()
229 229 try:
230 230 branchnode = repo.branchtip(branch)
231 231 except error.RepoLookupError:
232 232 branchnode = None
233 233 if branch != 'default' and branchnode != ctx.node():
234 234 branches.append({"name": branch})
235 235 return branches
236 236
237 237 def nodebranchnodefault(ctx):
238 238 branches = []
239 239 branch = ctx.branch()
240 240 if branch != 'default':
241 241 branches.append({"name": branch})
242 242 return branches
243 243
244 244 def showtag(repo, tmpl, t1, node=nullid, **args):
245 245 for t in repo.nodetags(node):
246 246 yield tmpl(t1, tag=t, **args)
247 247
248 248 def showbookmark(repo, tmpl, t1, node=nullid, **args):
249 249 for t in repo.nodebookmarks(node):
250 250 yield tmpl(t1, bookmark=t, **args)
251 251
252 252 def branchentries(repo, stripecount, limit=0):
253 253 tips = []
254 254 heads = repo.heads()
255 255 parity = paritygen(stripecount)
256 256 sortkey = lambda item: (not item[1], item[0].rev())
257 257
258 258 def entries(**map):
259 259 count = 0
260 260 if not tips:
261 261 for tag, hs, tip, closed in repo.branchmap().iterbranches():
262 262 tips.append((repo[tip], closed))
263 263 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
264 264 if limit > 0 and count >= limit:
265 265 return
266 266 count += 1
267 267 if closed:
268 268 status = 'closed'
269 269 elif ctx.node() not in heads:
270 270 status = 'inactive'
271 271 else:
272 272 status = 'open'
273 273 yield {
274 274 'parity': next(parity),
275 275 'branch': ctx.branch(),
276 276 'status': status,
277 277 'node': ctx.hex(),
278 278 'date': ctx.date()
279 279 }
280 280
281 281 return entries
282 282
283 283 def cleanpath(repo, path):
284 284 path = path.lstrip('/')
285 285 return pathutil.canonpath(repo.root, '', path)
286 286
287 287 def changeidctx(repo, changeid):
288 288 try:
289 289 ctx = repo[changeid]
290 290 except error.RepoError:
291 291 man = repo.manifestlog._revlog
292 292 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
293 293
294 294 return ctx
295 295
296 296 def changectx(repo, req):
297 297 changeid = "tip"
298 298 if 'node' in req.form:
299 299 changeid = req.form['node'][0]
300 300 ipos = changeid.find(':')
301 301 if ipos != -1:
302 302 changeid = changeid[(ipos + 1):]
303 303 elif 'manifest' in req.form:
304 304 changeid = req.form['manifest'][0]
305 305
306 306 return changeidctx(repo, changeid)
307 307
308 308 def basechangectx(repo, req):
309 309 if 'node' in req.form:
310 310 changeid = req.form['node'][0]
311 311 ipos = changeid.find(':')
312 312 if ipos != -1:
313 313 changeid = changeid[:ipos]
314 314 return changeidctx(repo, changeid)
315 315
316 316 return None
317 317
318 318 def filectx(repo, req):
319 319 if 'file' not in req.form:
320 320 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
321 321 path = cleanpath(repo, req.form['file'][0])
322 322 if 'node' in req.form:
323 323 changeid = req.form['node'][0]
324 324 elif 'filenode' in req.form:
325 325 changeid = req.form['filenode'][0]
326 326 else:
327 327 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
328 328 try:
329 329 fctx = repo[changeid][path]
330 330 except error.RepoError:
331 331 fctx = repo.filectx(path, fileid=changeid)
332 332
333 333 return fctx
334 334
335 335 def linerange(req):
336 336 linerange = req.form.get('linerange')
337 337 if linerange is None:
338 338 return None
339 339 if len(linerange) > 1:
340 340 raise ErrorResponse(HTTP_BAD_REQUEST,
341 341 'redundant linerange parameter')
342 342 try:
343 343 fromline, toline = map(int, linerange[0].split(':', 1))
344 344 except ValueError:
345 345 raise ErrorResponse(HTTP_BAD_REQUEST,
346 346 'invalid linerange parameter')
347 347 try:
348 348 return util.processlinerange(fromline, toline)
349 349 except error.ParseError as exc:
350 raise ErrorResponse(HTTP_BAD_REQUEST, str(exc))
350 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
351 351
352 352 def formatlinerange(fromline, toline):
353 353 return '%d:%d' % (fromline + 1, toline)
354 354
355 355 def succsandmarkers(repo, ctx):
356 356 for item in templatekw.showsuccsandmarkers(repo, ctx):
357 357 item['successors'] = _siblings(repo[successor]
358 358 for successor in item['successors'])
359 359 yield item
360 360
361 361 def commonentry(repo, ctx):
362 362 node = ctx.node()
363 363 return {
364 364 'rev': ctx.rev(),
365 365 'node': hex(node),
366 366 'author': ctx.user(),
367 367 'desc': ctx.description(),
368 368 'date': ctx.date(),
369 369 'extra': ctx.extra(),
370 370 'phase': ctx.phasestr(),
371 371 'obsolete': ctx.obsolete(),
372 372 'succsandmarkers': lambda **x: succsandmarkers(repo, ctx),
373 373 'instabilities': [{"instability": i} for i in ctx.instabilities()],
374 374 'branch': nodebranchnodefault(ctx),
375 375 'inbranch': nodeinbranch(repo, ctx),
376 376 'branches': nodebranchdict(repo, ctx),
377 377 'tags': nodetagsdict(repo, node),
378 378 'bookmarks': nodebookmarksdict(repo, node),
379 379 'parent': lambda **x: parents(ctx),
380 380 'child': lambda **x: children(ctx),
381 381 }
382 382
383 383 def changelistentry(web, ctx, tmpl):
384 384 '''Obtain a dictionary to be used for entries in a changelist.
385 385
386 386 This function is called when producing items for the "entries" list passed
387 387 to the "shortlog" and "changelog" templates.
388 388 '''
389 389 repo = web.repo
390 390 rev = ctx.rev()
391 391 n = ctx.node()
392 392 showtags = showtag(repo, tmpl, 'changelogtag', n)
393 393 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
394 394
395 395 entry = commonentry(repo, ctx)
396 396 entry.update(
397 397 allparents=lambda **x: parents(ctx),
398 398 parent=lambda **x: parents(ctx, rev - 1),
399 399 child=lambda **x: children(ctx, rev + 1),
400 400 changelogtag=showtags,
401 401 files=files,
402 402 )
403 403 return entry
404 404
405 405 def symrevorshortnode(req, ctx):
406 406 if 'node' in req.form:
407 407 return templatefilters.revescape(req.form['node'][0])
408 408 else:
409 409 return short(ctx.node())
410 410
411 411 def changesetentry(web, req, tmpl, ctx):
412 412 '''Obtain a dictionary to be used to render the "changeset" template.'''
413 413
414 414 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
415 415 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
416 416 ctx.node())
417 417 showbranch = nodebranchnodefault(ctx)
418 418
419 419 files = []
420 420 parity = paritygen(web.stripecount)
421 421 for blockno, f in enumerate(ctx.files()):
422 422 template = 'filenodelink' if f in ctx else 'filenolink'
423 423 files.append(tmpl(template,
424 424 node=ctx.hex(), file=f, blockno=blockno + 1,
425 425 parity=next(parity)))
426 426
427 427 basectx = basechangectx(web.repo, req)
428 428 if basectx is None:
429 429 basectx = ctx.p1()
430 430
431 431 style = web.config('web', 'style')
432 432 if 'style' in req.form:
433 433 style = req.form['style'][0]
434 434
435 435 diff = diffs(web, tmpl, ctx, basectx, None, style)
436 436
437 437 parity = paritygen(web.stripecount)
438 438 diffstatsgen = diffstatgen(ctx, basectx)
439 439 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
440 440
441 441 return dict(
442 442 diff=diff,
443 443 symrev=symrevorshortnode(req, ctx),
444 444 basenode=basectx.hex(),
445 445 changesettag=showtags,
446 446 changesetbookmark=showbookmarks,
447 447 changesetbranch=showbranch,
448 448 files=files,
449 449 diffsummary=lambda **x: diffsummary(diffstatsgen),
450 450 diffstat=diffstats,
451 451 archives=web.archivelist(ctx.hex()),
452 452 **commonentry(web.repo, ctx))
453 453
454 454 def listfilediffs(tmpl, files, node, max):
455 455 for f in files[:max]:
456 456 yield tmpl('filedifflink', node=hex(node), file=f)
457 457 if len(files) > max:
458 458 yield tmpl('fileellipses')
459 459
460 460 def diffs(web, tmpl, ctx, basectx, files, style, linerange=None,
461 461 lineidprefix=''):
462 462
463 463 def prettyprintlines(lines, blockno):
464 464 for lineno, l in enumerate(lines, 1):
465 465 difflineno = "%d.%d" % (blockno, lineno)
466 466 if l.startswith('+'):
467 467 ltype = "difflineplus"
468 468 elif l.startswith('-'):
469 469 ltype = "difflineminus"
470 470 elif l.startswith('@'):
471 471 ltype = "difflineat"
472 472 else:
473 473 ltype = "diffline"
474 474 yield tmpl(ltype,
475 475 line=l,
476 476 lineno=lineno,
477 477 lineid=lineidprefix + "l%s" % difflineno,
478 478 linenumber="% 8s" % difflineno)
479 479
480 480 repo = web.repo
481 481 if files:
482 482 m = match.exact(repo.root, repo.getcwd(), files)
483 483 else:
484 484 m = match.always(repo.root, repo.getcwd())
485 485
486 486 diffopts = patch.diffopts(repo.ui, untrusted=True)
487 487 node1 = basectx.node()
488 488 node2 = ctx.node()
489 489 parity = paritygen(web.stripecount)
490 490
491 491 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
492 492 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
493 493 if style != 'raw':
494 494 header = header[1:]
495 495 lines = [h + '\n' for h in header]
496 496 for hunkrange, hunklines in hunks:
497 497 if linerange is not None and hunkrange is not None:
498 498 s1, l1, s2, l2 = hunkrange
499 499 if not mdiff.hunkinrange((s2, l2), linerange):
500 500 continue
501 501 lines.extend(hunklines)
502 502 if lines:
503 503 yield tmpl('diffblock', parity=next(parity), blockno=blockno,
504 504 lines=prettyprintlines(lines, blockno))
505 505
506 506 def compare(tmpl, context, leftlines, rightlines):
507 507 '''Generator function that provides side-by-side comparison data.'''
508 508
509 509 def compline(type, leftlineno, leftline, rightlineno, rightline):
510 510 lineid = leftlineno and ("l%s" % leftlineno) or ''
511 511 lineid += rightlineno and ("r%s" % rightlineno) or ''
512 512 return tmpl('comparisonline',
513 513 type=type,
514 514 lineid=lineid,
515 515 leftlineno=leftlineno,
516 516 leftlinenumber="% 6s" % (leftlineno or ''),
517 517 leftline=leftline or '',
518 518 rightlineno=rightlineno,
519 519 rightlinenumber="% 6s" % (rightlineno or ''),
520 520 rightline=rightline or '')
521 521
522 522 def getblock(opcodes):
523 523 for type, llo, lhi, rlo, rhi in opcodes:
524 524 len1 = lhi - llo
525 525 len2 = rhi - rlo
526 526 count = min(len1, len2)
527 527 for i in xrange(count):
528 528 yield compline(type=type,
529 529 leftlineno=llo + i + 1,
530 530 leftline=leftlines[llo + i],
531 531 rightlineno=rlo + i + 1,
532 532 rightline=rightlines[rlo + i])
533 533 if len1 > len2:
534 534 for i in xrange(llo + count, lhi):
535 535 yield compline(type=type,
536 536 leftlineno=i + 1,
537 537 leftline=leftlines[i],
538 538 rightlineno=None,
539 539 rightline=None)
540 540 elif len2 > len1:
541 541 for i in xrange(rlo + count, rhi):
542 542 yield compline(type=type,
543 543 leftlineno=None,
544 544 leftline=None,
545 545 rightlineno=i + 1,
546 546 rightline=rightlines[i])
547 547
548 548 s = difflib.SequenceMatcher(None, leftlines, rightlines)
549 549 if context < 0:
550 550 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
551 551 else:
552 552 for oc in s.get_grouped_opcodes(n=context):
553 553 yield tmpl('comparisonblock', lines=getblock(oc))
554 554
555 555 def diffstatgen(ctx, basectx):
556 556 '''Generator function that provides the diffstat data.'''
557 557
558 558 stats = patch.diffstatdata(
559 559 util.iterlines(ctx.diff(basectx, noprefix=False)))
560 560 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
561 561 while True:
562 562 yield stats, maxname, maxtotal, addtotal, removetotal, binary
563 563
564 564 def diffsummary(statgen):
565 565 '''Return a short summary of the diff.'''
566 566
567 567 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
568 568 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
569 569 len(stats), addtotal, removetotal)
570 570
571 571 def diffstat(tmpl, ctx, statgen, parity):
572 572 '''Return a diffstat template for each file in the diff.'''
573 573
574 574 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
575 575 files = ctx.files()
576 576
577 577 def pct(i):
578 578 if maxtotal == 0:
579 579 return 0
580 580 return (float(i) / maxtotal) * 100
581 581
582 582 fileno = 0
583 583 for filename, adds, removes, isbinary in stats:
584 584 template = 'diffstatlink' if filename in files else 'diffstatnolink'
585 585 total = adds + removes
586 586 fileno += 1
587 587 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
588 588 total=total, addpct=pct(adds), removepct=pct(removes),
589 589 parity=next(parity))
590 590
591 591 class sessionvars(object):
592 592 def __init__(self, vars, start='?'):
593 593 self.start = start
594 594 self.vars = vars
595 595 def __getitem__(self, key):
596 596 return self.vars[key]
597 597 def __setitem__(self, key, value):
598 598 self.vars[key] = value
599 599 def __copy__(self):
600 600 return sessionvars(copy.copy(self.vars), self.start)
601 601 def __iter__(self):
602 602 separator = self.start
603 603 for key, value in sorted(self.vars.iteritems()):
604 604 yield {'name': key,
605 605 'value': pycompat.bytestr(value),
606 606 'separator': separator,
607 607 }
608 608 separator = '&'
609 609
610 610 class wsgiui(uimod.ui):
611 611 # default termwidth breaks under mod_wsgi
612 612 def termwidth(self):
613 613 return 80
614 614
615 615 def getwebsubs(repo):
616 616 websubtable = []
617 617 websubdefs = repo.ui.configitems('websub')
618 618 # we must maintain interhg backwards compatibility
619 619 websubdefs += repo.ui.configitems('interhg')
620 620 for key, pattern in websubdefs:
621 621 # grab the delimiter from the character after the "s"
622 622 unesc = pattern[1:2]
623 623 delim = re.escape(unesc)
624 624
625 625 # identify portions of the pattern, taking care to avoid escaped
626 626 # delimiters. the replace format and flags are optional, but
627 627 # delimiters are required.
628 628 match = re.match(
629 629 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
630 630 % (delim, delim, delim), pattern)
631 631 if not match:
632 632 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
633 633 % (key, pattern))
634 634 continue
635 635
636 636 # we need to unescape the delimiter for regexp and format
637 637 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
638 638 regexp = delim_re.sub(unesc, match.group(1))
639 639 format = delim_re.sub(unesc, match.group(2))
640 640
641 641 # the pattern allows for 6 regexp flags, so set them if necessary
642 642 flagin = match.group(3)
643 643 flags = 0
644 644 if flagin:
645 645 for flag in flagin.upper():
646 646 flags |= re.__dict__[flag]
647 647
648 648 try:
649 649 regexp = re.compile(regexp, flags)
650 650 websubtable.append((regexp, format))
651 651 except re.error:
652 652 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
653 653 % (key, regexp))
654 654 return websubtable
@@ -1,1066 +1,1066 b''
1 1 # wireproto.py - generic wire protocol support functions
2 2 #
3 3 # Copyright 2005-2010 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 hashlib
11 11 import os
12 12 import tempfile
13 13
14 14 from .i18n import _
15 15 from .node import (
16 16 bin,
17 17 hex,
18 18 nullid,
19 19 )
20 20
21 21 from . import (
22 22 bundle2,
23 23 changegroup as changegroupmod,
24 24 discovery,
25 25 encoding,
26 26 error,
27 27 exchange,
28 28 peer,
29 29 pushkey as pushkeymod,
30 30 pycompat,
31 31 repository,
32 32 streamclone,
33 33 util,
34 34 wireprototypes,
35 35 )
36 36
37 37 urlerr = util.urlerr
38 38 urlreq = util.urlreq
39 39
40 40 bytesresponse = wireprototypes.bytesresponse
41 41 ooberror = wireprototypes.ooberror
42 42 pushres = wireprototypes.pushres
43 43 pusherr = wireprototypes.pusherr
44 44 streamres = wireprototypes.streamres
45 45 streamres_legacy = wireprototypes.streamreslegacy
46 46
47 47 bundle2requiredmain = _('incompatible Mercurial client; bundle2 required')
48 48 bundle2requiredhint = _('see https://www.mercurial-scm.org/wiki/'
49 49 'IncompatibleClient')
50 50 bundle2required = '%s\n(%s)\n' % (bundle2requiredmain, bundle2requiredhint)
51 51
52 52 class remoteiterbatcher(peer.iterbatcher):
53 53 def __init__(self, remote):
54 54 super(remoteiterbatcher, self).__init__()
55 55 self._remote = remote
56 56
57 57 def __getattr__(self, name):
58 58 # Validate this method is batchable, since submit() only supports
59 59 # batchable methods.
60 60 fn = getattr(self._remote, name)
61 61 if not getattr(fn, 'batchable', None):
62 62 raise error.ProgrammingError('Attempted to batch a non-batchable '
63 63 'call to %r' % name)
64 64
65 65 return super(remoteiterbatcher, self).__getattr__(name)
66 66
67 67 def submit(self):
68 68 """Break the batch request into many patch calls and pipeline them.
69 69
70 70 This is mostly valuable over http where request sizes can be
71 71 limited, but can be used in other places as well.
72 72 """
73 73 # 2-tuple of (command, arguments) that represents what will be
74 74 # sent over the wire.
75 75 requests = []
76 76
77 77 # 4-tuple of (command, final future, @batchable generator, remote
78 78 # future).
79 79 results = []
80 80
81 81 for command, args, opts, finalfuture in self.calls:
82 82 mtd = getattr(self._remote, command)
83 83 batchable = mtd.batchable(mtd.__self__, *args, **opts)
84 84
85 85 commandargs, fremote = next(batchable)
86 86 assert fremote
87 87 requests.append((command, commandargs))
88 88 results.append((command, finalfuture, batchable, fremote))
89 89
90 90 if requests:
91 91 self._resultiter = self._remote._submitbatch(requests)
92 92
93 93 self._results = results
94 94
95 95 def results(self):
96 96 for command, finalfuture, batchable, remotefuture in self._results:
97 97 # Get the raw result, set it in the remote future, feed it
98 98 # back into the @batchable generator so it can be decoded, and
99 99 # set the result on the final future to this value.
100 100 remoteresult = next(self._resultiter)
101 101 remotefuture.set(remoteresult)
102 102 finalfuture.set(next(batchable))
103 103
104 104 # Verify our @batchable generators only emit 2 values.
105 105 try:
106 106 next(batchable)
107 107 except StopIteration:
108 108 pass
109 109 else:
110 110 raise error.ProgrammingError('%s @batchable generator emitted '
111 111 'unexpected value count' % command)
112 112
113 113 yield finalfuture.value
114 114
115 115 # Forward a couple of names from peer to make wireproto interactions
116 116 # slightly more sensible.
117 117 batchable = peer.batchable
118 118 future = peer.future
119 119
120 120 # list of nodes encoding / decoding
121 121
122 122 def decodelist(l, sep=' '):
123 123 if l:
124 124 return [bin(v) for v in l.split(sep)]
125 125 return []
126 126
127 127 def encodelist(l, sep=' '):
128 128 try:
129 129 return sep.join(map(hex, l))
130 130 except TypeError:
131 131 raise
132 132
133 133 # batched call argument encoding
134 134
135 135 def escapearg(plain):
136 136 return (plain
137 137 .replace(':', ':c')
138 138 .replace(',', ':o')
139 139 .replace(';', ':s')
140 140 .replace('=', ':e'))
141 141
142 142 def unescapearg(escaped):
143 143 return (escaped
144 144 .replace(':e', '=')
145 145 .replace(':s', ';')
146 146 .replace(':o', ',')
147 147 .replace(':c', ':'))
148 148
149 149 def encodebatchcmds(req):
150 150 """Return a ``cmds`` argument value for the ``batch`` command."""
151 151 cmds = []
152 152 for op, argsdict in req:
153 153 # Old servers didn't properly unescape argument names. So prevent
154 154 # the sending of argument names that may not be decoded properly by
155 155 # servers.
156 156 assert all(escapearg(k) == k for k in argsdict)
157 157
158 158 args = ','.join('%s=%s' % (escapearg(k), escapearg(v))
159 159 for k, v in argsdict.iteritems())
160 160 cmds.append('%s %s' % (op, args))
161 161
162 162 return ';'.join(cmds)
163 163
164 164 # mapping of options accepted by getbundle and their types
165 165 #
166 166 # Meant to be extended by extensions. It is extensions responsibility to ensure
167 167 # such options are properly processed in exchange.getbundle.
168 168 #
169 169 # supported types are:
170 170 #
171 171 # :nodes: list of binary nodes
172 172 # :csv: list of comma-separated values
173 173 # :scsv: list of comma-separated values return as set
174 174 # :plain: string with no transformation needed.
175 175 gboptsmap = {'heads': 'nodes',
176 176 'bookmarks': 'boolean',
177 177 'common': 'nodes',
178 178 'obsmarkers': 'boolean',
179 179 'phases': 'boolean',
180 180 'bundlecaps': 'scsv',
181 181 'listkeys': 'csv',
182 182 'cg': 'boolean',
183 183 'cbattempted': 'boolean',
184 184 'stream': 'boolean',
185 185 }
186 186
187 187 # client side
188 188
189 189 class wirepeer(repository.legacypeer):
190 190 """Client-side interface for communicating with a peer repository.
191 191
192 192 Methods commonly call wire protocol commands of the same name.
193 193
194 194 See also httppeer.py and sshpeer.py for protocol-specific
195 195 implementations of this interface.
196 196 """
197 197 # Begin of basewirepeer interface.
198 198
199 199 def iterbatch(self):
200 200 return remoteiterbatcher(self)
201 201
202 202 @batchable
203 203 def lookup(self, key):
204 204 self.requirecap('lookup', _('look up remote revision'))
205 205 f = future()
206 206 yield {'key': encoding.fromlocal(key)}, f
207 207 d = f.value
208 208 success, data = d[:-1].split(" ", 1)
209 209 if int(success):
210 210 yield bin(data)
211 211 else:
212 212 self._abort(error.RepoError(data))
213 213
214 214 @batchable
215 215 def heads(self):
216 216 f = future()
217 217 yield {}, f
218 218 d = f.value
219 219 try:
220 220 yield decodelist(d[:-1])
221 221 except ValueError:
222 222 self._abort(error.ResponseError(_("unexpected response:"), d))
223 223
224 224 @batchable
225 225 def known(self, nodes):
226 226 f = future()
227 227 yield {'nodes': encodelist(nodes)}, f
228 228 d = f.value
229 229 try:
230 230 yield [bool(int(b)) for b in d]
231 231 except ValueError:
232 232 self._abort(error.ResponseError(_("unexpected response:"), d))
233 233
234 234 @batchable
235 235 def branchmap(self):
236 236 f = future()
237 237 yield {}, f
238 238 d = f.value
239 239 try:
240 240 branchmap = {}
241 241 for branchpart in d.splitlines():
242 242 branchname, branchheads = branchpart.split(' ', 1)
243 243 branchname = encoding.tolocal(urlreq.unquote(branchname))
244 244 branchheads = decodelist(branchheads)
245 245 branchmap[branchname] = branchheads
246 246 yield branchmap
247 247 except TypeError:
248 248 self._abort(error.ResponseError(_("unexpected response:"), d))
249 249
250 250 @batchable
251 251 def listkeys(self, namespace):
252 252 if not self.capable('pushkey'):
253 253 yield {}, None
254 254 f = future()
255 255 self.ui.debug('preparing listkeys for "%s"\n' % namespace)
256 256 yield {'namespace': encoding.fromlocal(namespace)}, f
257 257 d = f.value
258 258 self.ui.debug('received listkey for "%s": %i bytes\n'
259 259 % (namespace, len(d)))
260 260 yield pushkeymod.decodekeys(d)
261 261
262 262 @batchable
263 263 def pushkey(self, namespace, key, old, new):
264 264 if not self.capable('pushkey'):
265 265 yield False, None
266 266 f = future()
267 267 self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key))
268 268 yield {'namespace': encoding.fromlocal(namespace),
269 269 'key': encoding.fromlocal(key),
270 270 'old': encoding.fromlocal(old),
271 271 'new': encoding.fromlocal(new)}, f
272 272 d = f.value
273 273 d, output = d.split('\n', 1)
274 274 try:
275 275 d = bool(int(d))
276 276 except ValueError:
277 277 raise error.ResponseError(
278 278 _('push failed (unexpected response):'), d)
279 279 for l in output.splitlines(True):
280 280 self.ui.status(_('remote: '), l)
281 281 yield d
282 282
283 283 def stream_out(self):
284 284 return self._callstream('stream_out')
285 285
286 286 def getbundle(self, source, **kwargs):
287 287 kwargs = pycompat.byteskwargs(kwargs)
288 288 self.requirecap('getbundle', _('look up remote changes'))
289 289 opts = {}
290 290 bundlecaps = kwargs.get('bundlecaps')
291 291 if bundlecaps is not None:
292 292 kwargs['bundlecaps'] = sorted(bundlecaps)
293 293 else:
294 294 bundlecaps = () # kwargs could have it to None
295 295 for key, value in kwargs.iteritems():
296 296 if value is None:
297 297 continue
298 298 keytype = gboptsmap.get(key)
299 299 if keytype is None:
300 300 raise error.ProgrammingError(
301 301 'Unexpectedly None keytype for key %s' % key)
302 302 elif keytype == 'nodes':
303 303 value = encodelist(value)
304 304 elif keytype in ('csv', 'scsv'):
305 305 value = ','.join(value)
306 306 elif keytype == 'boolean':
307 307 value = '%i' % bool(value)
308 308 elif keytype != 'plain':
309 309 raise KeyError('unknown getbundle option type %s'
310 310 % keytype)
311 311 opts[key] = value
312 312 f = self._callcompressable("getbundle", **pycompat.strkwargs(opts))
313 313 if any((cap.startswith('HG2') for cap in bundlecaps)):
314 314 return bundle2.getunbundler(self.ui, f)
315 315 else:
316 316 return changegroupmod.cg1unpacker(f, 'UN')
317 317
318 318 def unbundle(self, cg, heads, url):
319 319 '''Send cg (a readable file-like object representing the
320 320 changegroup to push, typically a chunkbuffer object) to the
321 321 remote server as a bundle.
322 322
323 323 When pushing a bundle10 stream, return an integer indicating the
324 324 result of the push (see changegroup.apply()).
325 325
326 326 When pushing a bundle20 stream, return a bundle20 stream.
327 327
328 328 `url` is the url the client thinks it's pushing to, which is
329 329 visible to hooks.
330 330 '''
331 331
332 332 if heads != ['force'] and self.capable('unbundlehash'):
333 333 heads = encodelist(['hashed',
334 334 hashlib.sha1(''.join(sorted(heads))).digest()])
335 335 else:
336 336 heads = encodelist(heads)
337 337
338 338 if util.safehasattr(cg, 'deltaheader'):
339 339 # this a bundle10, do the old style call sequence
340 340 ret, output = self._callpush("unbundle", cg, heads=heads)
341 341 if ret == "":
342 342 raise error.ResponseError(
343 343 _('push failed:'), output)
344 344 try:
345 345 ret = int(ret)
346 346 except ValueError:
347 347 raise error.ResponseError(
348 348 _('push failed (unexpected response):'), ret)
349 349
350 350 for l in output.splitlines(True):
351 351 self.ui.status(_('remote: '), l)
352 352 else:
353 353 # bundle2 push. Send a stream, fetch a stream.
354 354 stream = self._calltwowaystream('unbundle', cg, heads=heads)
355 355 ret = bundle2.getunbundler(self.ui, stream)
356 356 return ret
357 357
358 358 # End of basewirepeer interface.
359 359
360 360 # Begin of baselegacywirepeer interface.
361 361
362 362 def branches(self, nodes):
363 363 n = encodelist(nodes)
364 364 d = self._call("branches", nodes=n)
365 365 try:
366 366 br = [tuple(decodelist(b)) for b in d.splitlines()]
367 367 return br
368 368 except ValueError:
369 369 self._abort(error.ResponseError(_("unexpected response:"), d))
370 370
371 371 def between(self, pairs):
372 372 batch = 8 # avoid giant requests
373 373 r = []
374 374 for i in xrange(0, len(pairs), batch):
375 375 n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]])
376 376 d = self._call("between", pairs=n)
377 377 try:
378 378 r.extend(l and decodelist(l) or [] for l in d.splitlines())
379 379 except ValueError:
380 380 self._abort(error.ResponseError(_("unexpected response:"), d))
381 381 return r
382 382
383 383 def changegroup(self, nodes, kind):
384 384 n = encodelist(nodes)
385 385 f = self._callcompressable("changegroup", roots=n)
386 386 return changegroupmod.cg1unpacker(f, 'UN')
387 387
388 388 def changegroupsubset(self, bases, heads, kind):
389 389 self.requirecap('changegroupsubset', _('look up remote changes'))
390 390 bases = encodelist(bases)
391 391 heads = encodelist(heads)
392 392 f = self._callcompressable("changegroupsubset",
393 393 bases=bases, heads=heads)
394 394 return changegroupmod.cg1unpacker(f, 'UN')
395 395
396 396 # End of baselegacywirepeer interface.
397 397
398 398 def _submitbatch(self, req):
399 399 """run batch request <req> on the server
400 400
401 401 Returns an iterator of the raw responses from the server.
402 402 """
403 403 rsp = self._callstream("batch", cmds=encodebatchcmds(req))
404 404 chunk = rsp.read(1024)
405 405 work = [chunk]
406 406 while chunk:
407 407 while ';' not in chunk and chunk:
408 408 chunk = rsp.read(1024)
409 409 work.append(chunk)
410 410 merged = ''.join(work)
411 411 while ';' in merged:
412 412 one, merged = merged.split(';', 1)
413 413 yield unescapearg(one)
414 414 chunk = rsp.read(1024)
415 415 work = [merged, chunk]
416 416 yield unescapearg(''.join(work))
417 417
418 418 def _submitone(self, op, args):
419 419 return self._call(op, **pycompat.strkwargs(args))
420 420
421 421 def debugwireargs(self, one, two, three=None, four=None, five=None):
422 422 # don't pass optional arguments left at their default value
423 423 opts = {}
424 424 if three is not None:
425 425 opts[r'three'] = three
426 426 if four is not None:
427 427 opts[r'four'] = four
428 428 return self._call('debugwireargs', one=one, two=two, **opts)
429 429
430 430 def _call(self, cmd, **args):
431 431 """execute <cmd> on the server
432 432
433 433 The command is expected to return a simple string.
434 434
435 435 returns the server reply as a string."""
436 436 raise NotImplementedError()
437 437
438 438 def _callstream(self, cmd, **args):
439 439 """execute <cmd> on the server
440 440
441 441 The command is expected to return a stream. Note that if the
442 442 command doesn't return a stream, _callstream behaves
443 443 differently for ssh and http peers.
444 444
445 445 returns the server reply as a file like object.
446 446 """
447 447 raise NotImplementedError()
448 448
449 449 def _callcompressable(self, cmd, **args):
450 450 """execute <cmd> on the server
451 451
452 452 The command is expected to return a stream.
453 453
454 454 The stream may have been compressed in some implementations. This
455 455 function takes care of the decompression. This is the only difference
456 456 with _callstream.
457 457
458 458 returns the server reply as a file like object.
459 459 """
460 460 raise NotImplementedError()
461 461
462 462 def _callpush(self, cmd, fp, **args):
463 463 """execute a <cmd> on server
464 464
465 465 The command is expected to be related to a push. Push has a special
466 466 return method.
467 467
468 468 returns the server reply as a (ret, output) tuple. ret is either
469 469 empty (error) or a stringified int.
470 470 """
471 471 raise NotImplementedError()
472 472
473 473 def _calltwowaystream(self, cmd, fp, **args):
474 474 """execute <cmd> on server
475 475
476 476 The command will send a stream to the server and get a stream in reply.
477 477 """
478 478 raise NotImplementedError()
479 479
480 480 def _abort(self, exception):
481 481 """clearly abort the wire protocol connection and raise the exception
482 482 """
483 483 raise NotImplementedError()
484 484
485 485 # server side
486 486
487 487 # wire protocol command can either return a string or one of these classes.
488 488
489 489 def getdispatchrepo(repo, proto, command):
490 490 """Obtain the repo used for processing wire protocol commands.
491 491
492 492 The intent of this function is to serve as a monkeypatch point for
493 493 extensions that need commands to operate on different repo views under
494 494 specialized circumstances.
495 495 """
496 496 return repo.filtered('served')
497 497
498 498 def dispatch(repo, proto, command):
499 499 repo = getdispatchrepo(repo, proto, command)
500 500 func, spec = commands[command]
501 501 args = proto.getargs(spec)
502 502 return func(repo, proto, *args)
503 503
504 504 def options(cmd, keys, others):
505 505 opts = {}
506 506 for k in keys:
507 507 if k in others:
508 508 opts[k] = others[k]
509 509 del others[k]
510 510 if others:
511 511 util.stderr.write("warning: %s ignored unexpected arguments %s\n"
512 512 % (cmd, ",".join(others)))
513 513 return opts
514 514
515 515 def bundle1allowed(repo, action):
516 516 """Whether a bundle1 operation is allowed from the server.
517 517
518 518 Priority is:
519 519
520 520 1. server.bundle1gd.<action> (if generaldelta active)
521 521 2. server.bundle1.<action>
522 522 3. server.bundle1gd (if generaldelta active)
523 523 4. server.bundle1
524 524 """
525 525 ui = repo.ui
526 526 gd = 'generaldelta' in repo.requirements
527 527
528 528 if gd:
529 529 v = ui.configbool('server', 'bundle1gd.%s' % action)
530 530 if v is not None:
531 531 return v
532 532
533 533 v = ui.configbool('server', 'bundle1.%s' % action)
534 534 if v is not None:
535 535 return v
536 536
537 537 if gd:
538 538 v = ui.configbool('server', 'bundle1gd')
539 539 if v is not None:
540 540 return v
541 541
542 542 return ui.configbool('server', 'bundle1')
543 543
544 544 def supportedcompengines(ui, role):
545 545 """Obtain the list of supported compression engines for a request."""
546 546 assert role in (util.CLIENTROLE, util.SERVERROLE)
547 547
548 548 compengines = util.compengines.supportedwireengines(role)
549 549
550 550 # Allow config to override default list and ordering.
551 551 if role == util.SERVERROLE:
552 552 configengines = ui.configlist('server', 'compressionengines')
553 553 config = 'server.compressionengines'
554 554 else:
555 555 # This is currently implemented mainly to facilitate testing. In most
556 556 # cases, the server should be in charge of choosing a compression engine
557 557 # because a server has the most to lose from a sub-optimal choice. (e.g.
558 558 # CPU DoS due to an expensive engine or a network DoS due to poor
559 559 # compression ratio).
560 560 configengines = ui.configlist('experimental',
561 561 'clientcompressionengines')
562 562 config = 'experimental.clientcompressionengines'
563 563
564 564 # No explicit config. Filter out the ones that aren't supposed to be
565 565 # advertised and return default ordering.
566 566 if not configengines:
567 567 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
568 568 return [e for e in compengines
569 569 if getattr(e.wireprotosupport(), attr) > 0]
570 570
571 571 # If compression engines are listed in the config, assume there is a good
572 572 # reason for it (like server operators wanting to achieve specific
573 573 # performance characteristics). So fail fast if the config references
574 574 # unusable compression engines.
575 575 validnames = set(e.name() for e in compengines)
576 576 invalidnames = set(e for e in configengines if e not in validnames)
577 577 if invalidnames:
578 578 raise error.Abort(_('invalid compression engine defined in %s: %s') %
579 579 (config, ', '.join(sorted(invalidnames))))
580 580
581 581 compengines = [e for e in compengines if e.name() in configengines]
582 582 compengines = sorted(compengines,
583 583 key=lambda e: configengines.index(e.name()))
584 584
585 585 if not compengines:
586 586 raise error.Abort(_('%s config option does not specify any known '
587 587 'compression engines') % config,
588 588 hint=_('usable compression engines: %s') %
589 589 ', '.sorted(validnames))
590 590
591 591 return compengines
592 592
593 593 class commandentry(object):
594 594 """Represents a declared wire protocol command."""
595 595 def __init__(self, func, args=''):
596 596 self.func = func
597 597 self.args = args
598 598
599 599 def _merge(self, func, args):
600 600 """Merge this instance with an incoming 2-tuple.
601 601
602 602 This is called when a caller using the old 2-tuple API attempts
603 603 to replace an instance. The incoming values are merged with
604 604 data not captured by the 2-tuple and a new instance containing
605 605 the union of the two objects is returned.
606 606 """
607 607 return commandentry(func, args)
608 608
609 609 # Old code treats instances as 2-tuples. So expose that interface.
610 610 def __iter__(self):
611 611 yield self.func
612 612 yield self.args
613 613
614 614 def __getitem__(self, i):
615 615 if i == 0:
616 616 return self.func
617 617 elif i == 1:
618 618 return self.args
619 619 else:
620 620 raise IndexError('can only access elements 0 and 1')
621 621
622 622 class commanddict(dict):
623 623 """Container for registered wire protocol commands.
624 624
625 625 It behaves like a dict. But __setitem__ is overwritten to allow silent
626 626 coercion of values from 2-tuples for API compatibility.
627 627 """
628 628 def __setitem__(self, k, v):
629 629 if isinstance(v, commandentry):
630 630 pass
631 631 # Cast 2-tuples to commandentry instances.
632 632 elif isinstance(v, tuple):
633 633 if len(v) != 2:
634 634 raise ValueError('command tuples must have exactly 2 elements')
635 635
636 636 # It is common for extensions to wrap wire protocol commands via
637 637 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
638 638 # doing this aren't aware of the new API that uses objects to store
639 639 # command entries, we automatically merge old state with new.
640 640 if k in self:
641 641 v = self[k]._merge(v[0], v[1])
642 642 else:
643 643 v = commandentry(v[0], v[1])
644 644 else:
645 645 raise ValueError('command entries must be commandentry instances '
646 646 'or 2-tuples')
647 647
648 648 return super(commanddict, self).__setitem__(k, v)
649 649
650 650 def commandavailable(self, command, proto):
651 651 """Determine if a command is available for the requested protocol."""
652 652 # For now, commands are available for all protocols. So do a simple
653 653 # membership test.
654 654 return command in self
655 655
656 656 commands = commanddict()
657 657
658 658 def wireprotocommand(name, args=''):
659 659 """Decorator to declare a wire protocol command.
660 660
661 661 ``name`` is the name of the wire protocol command being provided.
662 662
663 663 ``args`` is a space-delimited list of named arguments that the command
664 664 accepts. ``*`` is a special value that says to accept all arguments.
665 665 """
666 666 def register(func):
667 667 commands[name] = commandentry(func, args)
668 668 return func
669 669 return register
670 670
671 671 @wireprotocommand('batch', 'cmds *')
672 672 def batch(repo, proto, cmds, others):
673 673 repo = repo.filtered("served")
674 674 res = []
675 675 for pair in cmds.split(';'):
676 676 op, args = pair.split(' ', 1)
677 677 vals = {}
678 678 for a in args.split(','):
679 679 if a:
680 680 n, v = a.split('=')
681 681 vals[unescapearg(n)] = unescapearg(v)
682 682 func, spec = commands[op]
683 683 if spec:
684 684 keys = spec.split()
685 685 data = {}
686 686 for k in keys:
687 687 if k == '*':
688 688 star = {}
689 689 for key in vals.keys():
690 690 if key not in keys:
691 691 star[key] = vals[key]
692 692 data['*'] = star
693 693 else:
694 694 data[k] = vals[k]
695 695 result = func(repo, proto, *[data[k] for k in keys])
696 696 else:
697 697 result = func(repo, proto)
698 698 if isinstance(result, ooberror):
699 699 return result
700 700
701 701 # For now, all batchable commands must return bytesresponse or
702 702 # raw bytes (for backwards compatibility).
703 703 assert isinstance(result, (bytesresponse, bytes))
704 704 if isinstance(result, bytesresponse):
705 705 result = result.data
706 706 res.append(escapearg(result))
707 707
708 708 return bytesresponse(';'.join(res))
709 709
710 710 @wireprotocommand('between', 'pairs')
711 711 def between(repo, proto, pairs):
712 712 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
713 713 r = []
714 714 for b in repo.between(pairs):
715 715 r.append(encodelist(b) + "\n")
716 716
717 717 return bytesresponse(''.join(r))
718 718
719 719 @wireprotocommand('branchmap')
720 720 def branchmap(repo, proto):
721 721 branchmap = repo.branchmap()
722 722 heads = []
723 723 for branch, nodes in branchmap.iteritems():
724 724 branchname = urlreq.quote(encoding.fromlocal(branch))
725 725 branchnodes = encodelist(nodes)
726 726 heads.append('%s %s' % (branchname, branchnodes))
727 727
728 728 return bytesresponse('\n'.join(heads))
729 729
730 730 @wireprotocommand('branches', 'nodes')
731 731 def branches(repo, proto, nodes):
732 732 nodes = decodelist(nodes)
733 733 r = []
734 734 for b in repo.branches(nodes):
735 735 r.append(encodelist(b) + "\n")
736 736
737 737 return bytesresponse(''.join(r))
738 738
739 739 @wireprotocommand('clonebundles', '')
740 740 def clonebundles(repo, proto):
741 741 """Server command for returning info for available bundles to seed clones.
742 742
743 743 Clients will parse this response and determine what bundle to fetch.
744 744
745 745 Extensions may wrap this command to filter or dynamically emit data
746 746 depending on the request. e.g. you could advertise URLs for the closest
747 747 data center given the client's IP address.
748 748 """
749 749 return bytesresponse(repo.vfs.tryread('clonebundles.manifest'))
750 750
751 751 wireprotocaps = ['lookup', 'changegroupsubset', 'branchmap', 'pushkey',
752 752 'known', 'getbundle', 'unbundlehash', 'batch']
753 753
754 754 def _capabilities(repo, proto):
755 755 """return a list of capabilities for a repo
756 756
757 757 This function exists to allow extensions to easily wrap capabilities
758 758 computation
759 759
760 760 - returns a lists: easy to alter
761 761 - change done here will be propagated to both `capabilities` and `hello`
762 762 command without any other action needed.
763 763 """
764 764 # copy to prevent modification of the global list
765 765 caps = list(wireprotocaps)
766 766 if streamclone.allowservergeneration(repo):
767 767 if repo.ui.configbool('server', 'preferuncompressed'):
768 768 caps.append('stream-preferred')
769 769 requiredformats = repo.requirements & repo.supportedformats
770 770 # if our local revlogs are just revlogv1, add 'stream' cap
771 771 if not requiredformats - {'revlogv1'}:
772 772 caps.append('stream')
773 773 # otherwise, add 'streamreqs' detailing our local revlog format
774 774 else:
775 775 caps.append('streamreqs=%s' % ','.join(sorted(requiredformats)))
776 776 if repo.ui.configbool('experimental', 'bundle2-advertise'):
777 777 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role='server'))
778 778 caps.append('bundle2=' + urlreq.quote(capsblob))
779 779 caps.append('unbundle=%s' % ','.join(bundle2.bundlepriority))
780 780
781 781 if proto.name == 'http-v1':
782 782 caps.append('httpheader=%d' %
783 783 repo.ui.configint('server', 'maxhttpheaderlen'))
784 784 if repo.ui.configbool('experimental', 'httppostargs'):
785 785 caps.append('httppostargs')
786 786
787 787 # FUTURE advertise 0.2rx once support is implemented
788 788 # FUTURE advertise minrx and mintx after consulting config option
789 789 caps.append('httpmediatype=0.1rx,0.1tx,0.2tx')
790 790
791 791 compengines = supportedcompengines(repo.ui, util.SERVERROLE)
792 792 if compengines:
793 793 comptypes = ','.join(urlreq.quote(e.wireprotosupport().name)
794 794 for e in compengines)
795 795 caps.append('compression=%s' % comptypes)
796 796
797 797 return caps
798 798
799 799 # If you are writing an extension and consider wrapping this function. Wrap
800 800 # `_capabilities` instead.
801 801 @wireprotocommand('capabilities')
802 802 def capabilities(repo, proto):
803 803 return bytesresponse(' '.join(_capabilities(repo, proto)))
804 804
805 805 @wireprotocommand('changegroup', 'roots')
806 806 def changegroup(repo, proto, roots):
807 807 nodes = decodelist(roots)
808 808 outgoing = discovery.outgoing(repo, missingroots=nodes,
809 809 missingheads=repo.heads())
810 810 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
811 811 gen = iter(lambda: cg.read(32768), '')
812 812 return streamres(gen=gen)
813 813
814 814 @wireprotocommand('changegroupsubset', 'bases heads')
815 815 def changegroupsubset(repo, proto, bases, heads):
816 816 bases = decodelist(bases)
817 817 heads = decodelist(heads)
818 818 outgoing = discovery.outgoing(repo, missingroots=bases,
819 819 missingheads=heads)
820 820 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
821 821 gen = iter(lambda: cg.read(32768), '')
822 822 return streamres(gen=gen)
823 823
824 824 @wireprotocommand('debugwireargs', 'one two *')
825 825 def debugwireargs(repo, proto, one, two, others):
826 826 # only accept optional args from the known set
827 827 opts = options('debugwireargs', ['three', 'four'], others)
828 828 return bytesresponse(repo.debugwireargs(one, two,
829 829 **pycompat.strkwargs(opts)))
830 830
831 831 @wireprotocommand('getbundle', '*')
832 832 def getbundle(repo, proto, others):
833 833 opts = options('getbundle', gboptsmap.keys(), others)
834 834 for k, v in opts.iteritems():
835 835 keytype = gboptsmap[k]
836 836 if keytype == 'nodes':
837 837 opts[k] = decodelist(v)
838 838 elif keytype == 'csv':
839 839 opts[k] = list(v.split(','))
840 840 elif keytype == 'scsv':
841 841 opts[k] = set(v.split(','))
842 842 elif keytype == 'boolean':
843 843 # Client should serialize False as '0', which is a non-empty string
844 844 # so it evaluates as a True bool.
845 845 if v == '0':
846 846 opts[k] = False
847 847 else:
848 848 opts[k] = bool(v)
849 849 elif keytype != 'plain':
850 850 raise KeyError('unknown getbundle option type %s'
851 851 % keytype)
852 852
853 853 if not bundle1allowed(repo, 'pull'):
854 854 if not exchange.bundle2requested(opts.get('bundlecaps')):
855 855 if proto.name == 'http-v1':
856 856 return ooberror(bundle2required)
857 857 raise error.Abort(bundle2requiredmain,
858 858 hint=bundle2requiredhint)
859 859
860 860 prefercompressed = True
861 861
862 862 try:
863 863 if repo.ui.configbool('server', 'disablefullbundle'):
864 864 # Check to see if this is a full clone.
865 865 clheads = set(repo.changelog.heads())
866 866 changegroup = opts.get('cg', True)
867 867 heads = set(opts.get('heads', set()))
868 868 common = set(opts.get('common', set()))
869 869 common.discard(nullid)
870 870 if changegroup and not common and clheads == heads:
871 871 raise error.Abort(
872 872 _('server has pull-based clones disabled'),
873 873 hint=_('remove --pull if specified or upgrade Mercurial'))
874 874
875 875 info, chunks = exchange.getbundlechunks(repo, 'serve',
876 876 **pycompat.strkwargs(opts))
877 877 prefercompressed = info.get('prefercompressed', True)
878 878 except error.Abort as exc:
879 879 # cleanly forward Abort error to the client
880 880 if not exchange.bundle2requested(opts.get('bundlecaps')):
881 881 if proto.name == 'http-v1':
882 return ooberror(str(exc) + '\n')
882 return ooberror(pycompat.bytestr(exc) + '\n')
883 883 raise # cannot do better for bundle1 + ssh
884 884 # bundle2 request expect a bundle2 reply
885 885 bundler = bundle2.bundle20(repo.ui)
886 manargs = [('message', str(exc))]
886 manargs = [('message', pycompat.bytestr(exc))]
887 887 advargs = []
888 888 if exc.hint is not None:
889 889 advargs.append(('hint', exc.hint))
890 890 bundler.addpart(bundle2.bundlepart('error:abort',
891 891 manargs, advargs))
892 892 chunks = bundler.getchunks()
893 893 prefercompressed = False
894 894
895 895 return streamres(gen=chunks, prefer_uncompressed=not prefercompressed)
896 896
897 897 @wireprotocommand('heads')
898 898 def heads(repo, proto):
899 899 h = repo.heads()
900 900 return bytesresponse(encodelist(h) + '\n')
901 901
902 902 @wireprotocommand('hello')
903 903 def hello(repo, proto):
904 904 """Called as part of SSH handshake to obtain server info.
905 905
906 906 Returns a list of lines describing interesting things about the
907 907 server, in an RFC822-like format.
908 908
909 909 Currently, the only one defined is ``capabilities``, which consists of a
910 910 line of space separated tokens describing server abilities:
911 911
912 912 capabilities: <token0> <token1> <token2>
913 913 """
914 914 caps = capabilities(repo, proto).data
915 915 return bytesresponse('capabilities: %s\n' % caps)
916 916
917 917 @wireprotocommand('listkeys', 'namespace')
918 918 def listkeys(repo, proto, namespace):
919 919 d = repo.listkeys(encoding.tolocal(namespace)).items()
920 920 return bytesresponse(pushkeymod.encodekeys(d))
921 921
922 922 @wireprotocommand('lookup', 'key')
923 923 def lookup(repo, proto, key):
924 924 try:
925 925 k = encoding.tolocal(key)
926 926 c = repo[k]
927 927 r = c.hex()
928 928 success = 1
929 929 except Exception as inst:
930 930 r = str(inst)
931 931 success = 0
932 932 return bytesresponse('%d %s\n' % (success, r))
933 933
934 934 @wireprotocommand('known', 'nodes *')
935 935 def known(repo, proto, nodes, others):
936 936 v = ''.join(b and '1' or '0' for b in repo.known(decodelist(nodes)))
937 937 return bytesresponse(v)
938 938
939 939 @wireprotocommand('pushkey', 'namespace key old new')
940 940 def pushkey(repo, proto, namespace, key, old, new):
941 941 # compatibility with pre-1.8 clients which were accidentally
942 942 # sending raw binary nodes rather than utf-8-encoded hex
943 943 if len(new) == 20 and util.escapestr(new) != new:
944 944 # looks like it could be a binary node
945 945 try:
946 946 new.decode('utf-8')
947 947 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
948 948 except UnicodeDecodeError:
949 949 pass # binary, leave unmodified
950 950 else:
951 951 new = encoding.tolocal(new) # normal path
952 952
953 953 with proto.mayberedirectstdio() as output:
954 954 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
955 955 encoding.tolocal(old), new) or False
956 956
957 957 output = output.getvalue() if output else ''
958 958 return bytesresponse('%s\n%s' % (int(r), output))
959 959
960 960 @wireprotocommand('stream_out')
961 961 def stream(repo, proto):
962 962 '''If the server supports streaming clone, it advertises the "stream"
963 963 capability with a value representing the version and flags of the repo
964 964 it is serving. Client checks to see if it understands the format.
965 965 '''
966 966 return streamres_legacy(streamclone.generatev1wireproto(repo))
967 967
968 968 @wireprotocommand('unbundle', 'heads')
969 969 def unbundle(repo, proto, heads):
970 970 their_heads = decodelist(heads)
971 971
972 972 with proto.mayberedirectstdio() as output:
973 973 try:
974 974 exchange.check_heads(repo, their_heads, 'preparing changes')
975 975
976 976 # write bundle data to temporary file because it can be big
977 977 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
978 978 fp = os.fdopen(fd, pycompat.sysstr('wb+'))
979 979 r = 0
980 980 try:
981 981 proto.forwardpayload(fp)
982 982 fp.seek(0)
983 983 gen = exchange.readbundle(repo.ui, fp, None)
984 984 if (isinstance(gen, changegroupmod.cg1unpacker)
985 985 and not bundle1allowed(repo, 'push')):
986 986 if proto.name == 'http-v1':
987 987 # need to special case http because stderr do not get to
988 988 # the http client on failed push so we need to abuse
989 989 # some other error type to make sure the message get to
990 990 # the user.
991 991 return ooberror(bundle2required)
992 992 raise error.Abort(bundle2requiredmain,
993 993 hint=bundle2requiredhint)
994 994
995 995 r = exchange.unbundle(repo, gen, their_heads, 'serve',
996 996 proto.client())
997 997 if util.safehasattr(r, 'addpart'):
998 998 # The return looks streamable, we are in the bundle2 case
999 999 # and should return a stream.
1000 1000 return streamres_legacy(gen=r.getchunks())
1001 1001 return pushres(r, output.getvalue() if output else '')
1002 1002
1003 1003 finally:
1004 1004 fp.close()
1005 1005 os.unlink(tempname)
1006 1006
1007 1007 except (error.BundleValueError, error.Abort, error.PushRaced) as exc:
1008 1008 # handle non-bundle2 case first
1009 1009 if not getattr(exc, 'duringunbundle2', False):
1010 1010 try:
1011 1011 raise
1012 1012 except error.Abort:
1013 1013 # The old code we moved used util.stderr directly.
1014 1014 # We did not change it to minimise code change.
1015 1015 # This need to be moved to something proper.
1016 1016 # Feel free to do it.
1017 1017 util.stderr.write("abort: %s\n" % exc)
1018 1018 if exc.hint is not None:
1019 1019 util.stderr.write("(%s)\n" % exc.hint)
1020 1020 return pushres(0, output.getvalue() if output else '')
1021 1021 except error.PushRaced:
1022 1022 return pusherr(str(exc),
1023 1023 output.getvalue() if output else '')
1024 1024
1025 1025 bundler = bundle2.bundle20(repo.ui)
1026 1026 for out in getattr(exc, '_bundle2salvagedoutput', ()):
1027 1027 bundler.addpart(out)
1028 1028 try:
1029 1029 try:
1030 1030 raise
1031 1031 except error.PushkeyFailed as exc:
1032 1032 # check client caps
1033 1033 remotecaps = getattr(exc, '_replycaps', None)
1034 1034 if (remotecaps is not None
1035 1035 and 'pushkey' not in remotecaps.get('error', ())):
1036 1036 # no support remote side, fallback to Abort handler.
1037 1037 raise
1038 1038 part = bundler.newpart('error:pushkey')
1039 1039 part.addparam('in-reply-to', exc.partid)
1040 1040 if exc.namespace is not None:
1041 1041 part.addparam('namespace', exc.namespace,
1042 1042 mandatory=False)
1043 1043 if exc.key is not None:
1044 1044 part.addparam('key', exc.key, mandatory=False)
1045 1045 if exc.new is not None:
1046 1046 part.addparam('new', exc.new, mandatory=False)
1047 1047 if exc.old is not None:
1048 1048 part.addparam('old', exc.old, mandatory=False)
1049 1049 if exc.ret is not None:
1050 1050 part.addparam('ret', exc.ret, mandatory=False)
1051 1051 except error.BundleValueError as exc:
1052 1052 errpart = bundler.newpart('error:unsupportedcontent')
1053 1053 if exc.parttype is not None:
1054 1054 errpart.addparam('parttype', exc.parttype)
1055 1055 if exc.params:
1056 1056 errpart.addparam('params', '\0'.join(exc.params))
1057 1057 except error.Abort as exc:
1058 1058 manargs = [('message', str(exc))]
1059 1059 advargs = []
1060 1060 if exc.hint is not None:
1061 1061 advargs.append(('hint', exc.hint))
1062 1062 bundler.addpart(bundle2.bundlepart('error:abort',
1063 1063 manargs, advargs))
1064 1064 except error.PushRaced as exc:
1065 1065 bundler.newpart('error:pushraced', [('message', str(exc))])
1066 1066 return streamres_legacy(gen=bundler.getchunks())
@@ -1,648 +1,648 b''
1 1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 3 #
4 4 # This software may be used and distributed according to the terms of the
5 5 # GNU General Public License version 2 or any later version.
6 6
7 7 from __future__ import absolute_import
8 8
9 9 import abc
10 10 import contextlib
11 11 import struct
12 12 import sys
13 13
14 14 from .i18n import _
15 15 from . import (
16 16 encoding,
17 17 error,
18 18 hook,
19 19 pycompat,
20 20 util,
21 21 wireproto,
22 22 wireprototypes,
23 23 )
24 24
25 25 stringio = util.stringio
26 26
27 27 urlerr = util.urlerr
28 28 urlreq = util.urlreq
29 29
30 30 HTTP_OK = 200
31 31
32 32 HGTYPE = 'application/mercurial-0.1'
33 33 HGTYPE2 = 'application/mercurial-0.2'
34 34 HGERRTYPE = 'application/hg-error'
35 35
36 36 # Names of the SSH protocol implementations.
37 37 SSHV1 = 'ssh-v1'
38 38 # This is advertised over the wire. Incremental the counter at the end
39 39 # to reflect BC breakages.
40 40 SSHV2 = 'exp-ssh-v2-0001'
41 41
42 42 class baseprotocolhandler(object):
43 43 """Abstract base class for wire protocol handlers.
44 44
45 45 A wire protocol handler serves as an interface between protocol command
46 46 handlers and the wire protocol transport layer. Protocol handlers provide
47 47 methods to read command arguments, redirect stdio for the duration of
48 48 the request, handle response types, etc.
49 49 """
50 50
51 51 __metaclass__ = abc.ABCMeta
52 52
53 53 @abc.abstractproperty
54 54 def name(self):
55 55 """The name of the protocol implementation.
56 56
57 57 Used for uniquely identifying the transport type.
58 58 """
59 59
60 60 @abc.abstractmethod
61 61 def getargs(self, args):
62 62 """return the value for arguments in <args>
63 63
64 64 returns a list of values (same order as <args>)"""
65 65
66 66 @abc.abstractmethod
67 67 def forwardpayload(self, fp):
68 68 """Read the raw payload and forward to a file.
69 69
70 70 The payload is read in full before the function returns.
71 71 """
72 72
73 73 @abc.abstractmethod
74 74 def mayberedirectstdio(self):
75 75 """Context manager to possibly redirect stdio.
76 76
77 77 The context manager yields a file-object like object that receives
78 78 stdout and stderr output when the context manager is active. Or it
79 79 yields ``None`` if no I/O redirection occurs.
80 80
81 81 The intent of this context manager is to capture stdio output
82 82 so it may be sent in the response. Some transports support streaming
83 83 stdio to the client in real time. For these transports, stdio output
84 84 won't be captured.
85 85 """
86 86
87 87 @abc.abstractmethod
88 88 def client(self):
89 89 """Returns a string representation of this client (as bytes)."""
90 90
91 91 def decodevaluefromheaders(req, headerprefix):
92 92 """Decode a long value from multiple HTTP request headers.
93 93
94 94 Returns the value as a bytes, not a str.
95 95 """
96 96 chunks = []
97 97 i = 1
98 98 prefix = headerprefix.upper().replace(r'-', r'_')
99 99 while True:
100 100 v = req.env.get(r'HTTP_%s_%d' % (prefix, i))
101 101 if v is None:
102 102 break
103 103 chunks.append(pycompat.bytesurl(v))
104 104 i += 1
105 105
106 106 return ''.join(chunks)
107 107
108 108 class httpv1protocolhandler(baseprotocolhandler):
109 109 def __init__(self, req, ui):
110 110 self._req = req
111 111 self._ui = ui
112 112
113 113 @property
114 114 def name(self):
115 115 return 'http-v1'
116 116
117 117 def getargs(self, args):
118 118 knownargs = self._args()
119 119 data = {}
120 120 keys = args.split()
121 121 for k in keys:
122 122 if k == '*':
123 123 star = {}
124 124 for key in knownargs.keys():
125 125 if key != 'cmd' and key not in keys:
126 126 star[key] = knownargs[key][0]
127 127 data['*'] = star
128 128 else:
129 129 data[k] = knownargs[k][0]
130 130 return [data[k] for k in keys]
131 131
132 132 def _args(self):
133 133 args = util.rapply(pycompat.bytesurl, self._req.form.copy())
134 134 postlen = int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
135 135 if postlen:
136 136 args.update(urlreq.parseqs(
137 137 self._req.read(postlen), keep_blank_values=True))
138 138 return args
139 139
140 140 argvalue = decodevaluefromheaders(self._req, r'X-HgArg')
141 141 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
142 142 return args
143 143
144 144 def forwardpayload(self, fp):
145 145 length = int(self._req.env[r'CONTENT_LENGTH'])
146 146 # If httppostargs is used, we need to read Content-Length
147 147 # minus the amount that was consumed by args.
148 148 length -= int(self._req.env.get(r'HTTP_X_HGARGS_POST', 0))
149 149 for s in util.filechunkiter(self._req, limit=length):
150 150 fp.write(s)
151 151
152 152 @contextlib.contextmanager
153 153 def mayberedirectstdio(self):
154 154 oldout = self._ui.fout
155 155 olderr = self._ui.ferr
156 156
157 157 out = util.stringio()
158 158
159 159 try:
160 160 self._ui.fout = out
161 161 self._ui.ferr = out
162 162 yield out
163 163 finally:
164 164 self._ui.fout = oldout
165 165 self._ui.ferr = olderr
166 166
167 167 def client(self):
168 168 return 'remote:%s:%s:%s' % (
169 169 self._req.env.get('wsgi.url_scheme') or 'http',
170 170 urlreq.quote(self._req.env.get('REMOTE_HOST', '')),
171 171 urlreq.quote(self._req.env.get('REMOTE_USER', '')))
172 172
173 173 # This method exists mostly so that extensions like remotefilelog can
174 174 # disable a kludgey legacy method only over http. As of early 2018,
175 175 # there are no other known users, so with any luck we can discard this
176 176 # hook if remotefilelog becomes a first-party extension.
177 177 def iscmd(cmd):
178 178 return cmd in wireproto.commands
179 179
180 180 def parsehttprequest(repo, req, query):
181 181 """Parse the HTTP request for a wire protocol request.
182 182
183 183 If the current request appears to be a wire protocol request, this
184 184 function returns a dict with details about that request, including
185 185 an ``abstractprotocolserver`` instance suitable for handling the
186 186 request. Otherwise, ``None`` is returned.
187 187
188 188 ``req`` is a ``wsgirequest`` instance.
189 189 """
190 190 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
191 191 # string parameter. If it isn't present, this isn't a wire protocol
192 192 # request.
193 193 if r'cmd' not in req.form:
194 194 return None
195 195
196 196 cmd = pycompat.sysbytes(req.form[r'cmd'][0])
197 197
198 198 # The "cmd" request parameter is used by both the wire protocol and hgweb.
199 199 # While not all wire protocol commands are available for all transports,
200 200 # if we see a "cmd" value that resembles a known wire protocol command, we
201 201 # route it to a protocol handler. This is better than routing possible
202 202 # wire protocol requests to hgweb because it prevents hgweb from using
203 203 # known wire protocol commands and it is less confusing for machine
204 204 # clients.
205 205 if not iscmd(cmd):
206 206 return None
207 207
208 208 proto = httpv1protocolhandler(req, repo.ui)
209 209
210 210 return {
211 211 'cmd': cmd,
212 212 'proto': proto,
213 213 'dispatch': lambda: _callhttp(repo, req, proto, cmd),
214 214 'handleerror': lambda ex: _handlehttperror(ex, req, cmd),
215 215 }
216 216
217 217 def _httpresponsetype(ui, req, prefer_uncompressed):
218 218 """Determine the appropriate response type and compression settings.
219 219
220 220 Returns a tuple of (mediatype, compengine, engineopts).
221 221 """
222 222 # Determine the response media type and compression engine based
223 223 # on the request parameters.
224 224 protocaps = decodevaluefromheaders(req, r'X-HgProto').split(' ')
225 225
226 226 if '0.2' in protocaps:
227 227 # All clients are expected to support uncompressed data.
228 228 if prefer_uncompressed:
229 229 return HGTYPE2, util._noopengine(), {}
230 230
231 231 # Default as defined by wire protocol spec.
232 232 compformats = ['zlib', 'none']
233 233 for cap in protocaps:
234 234 if cap.startswith('comp='):
235 235 compformats = cap[5:].split(',')
236 236 break
237 237
238 238 # Now find an agreed upon compression format.
239 239 for engine in wireproto.supportedcompengines(ui, util.SERVERROLE):
240 240 if engine.wireprotosupport().name in compformats:
241 241 opts = {}
242 242 level = ui.configint('server', '%slevel' % engine.name())
243 243 if level is not None:
244 244 opts['level'] = level
245 245
246 246 return HGTYPE2, engine, opts
247 247
248 248 # No mutually supported compression format. Fall back to the
249 249 # legacy protocol.
250 250
251 251 # Don't allow untrusted settings because disabling compression or
252 252 # setting a very high compression level could lead to flooding
253 253 # the server's network or CPU.
254 254 opts = {'level': ui.configint('server', 'zliblevel')}
255 255 return HGTYPE, util.compengines['zlib'], opts
256 256
257 257 def _callhttp(repo, req, proto, cmd):
258 258 def genversion2(gen, engine, engineopts):
259 259 # application/mercurial-0.2 always sends a payload header
260 260 # identifying the compression engine.
261 261 name = engine.wireprotosupport().name
262 262 assert 0 < len(name) < 256
263 263 yield struct.pack('B', len(name))
264 264 yield name
265 265
266 266 for chunk in gen:
267 267 yield chunk
268 268
269 269 rsp = wireproto.dispatch(repo, proto, cmd)
270 270
271 271 if not wireproto.commands.commandavailable(cmd, proto):
272 272 req.respond(HTTP_OK, HGERRTYPE,
273 273 body=_('requested wire protocol command is not available '
274 274 'over HTTP'))
275 275 return []
276 276
277 277 if isinstance(rsp, bytes):
278 278 req.respond(HTTP_OK, HGTYPE, body=rsp)
279 279 return []
280 280 elif isinstance(rsp, wireprototypes.bytesresponse):
281 281 req.respond(HTTP_OK, HGTYPE, body=rsp.data)
282 282 return []
283 283 elif isinstance(rsp, wireprototypes.streamreslegacy):
284 284 gen = rsp.gen
285 285 req.respond(HTTP_OK, HGTYPE)
286 286 return gen
287 287 elif isinstance(rsp, wireprototypes.streamres):
288 288 gen = rsp.gen
289 289
290 290 # This code for compression should not be streamres specific. It
291 291 # is here because we only compress streamres at the moment.
292 292 mediatype, engine, engineopts = _httpresponsetype(
293 293 repo.ui, req, rsp.prefer_uncompressed)
294 294 gen = engine.compressstream(gen, engineopts)
295 295
296 296 if mediatype == HGTYPE2:
297 297 gen = genversion2(gen, engine, engineopts)
298 298
299 299 req.respond(HTTP_OK, mediatype)
300 300 return gen
301 301 elif isinstance(rsp, wireprototypes.pushres):
302 302 rsp = '%d\n%s' % (rsp.res, rsp.output)
303 303 req.respond(HTTP_OK, HGTYPE, body=rsp)
304 304 return []
305 305 elif isinstance(rsp, wireprototypes.pusherr):
306 306 # This is the httplib workaround documented in _handlehttperror().
307 307 req.drain()
308 308
309 309 rsp = '0\n%s\n' % rsp.res
310 310 req.respond(HTTP_OK, HGTYPE, body=rsp)
311 311 return []
312 312 elif isinstance(rsp, wireprototypes.ooberror):
313 313 rsp = rsp.message
314 314 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
315 315 return []
316 316 raise error.ProgrammingError('hgweb.protocol internal failure', rsp)
317 317
318 318 def _handlehttperror(e, req, cmd):
319 319 """Called when an ErrorResponse is raised during HTTP request processing."""
320 320
321 321 # Clients using Python's httplib are stateful: the HTTP client
322 322 # won't process an HTTP response until all request data is
323 323 # sent to the server. The intent of this code is to ensure
324 324 # we always read HTTP request data from the client, thus
325 325 # ensuring httplib transitions to a state that allows it to read
326 326 # the HTTP response. In other words, it helps prevent deadlocks
327 327 # on clients using httplib.
328 328
329 329 if (req.env[r'REQUEST_METHOD'] == r'POST' and
330 330 # But not if Expect: 100-continue is being used.
331 331 (req.env.get('HTTP_EXPECT',
332 332 '').lower() != '100-continue') or
333 333 # Or the non-httplib HTTP library is being advertised by
334 334 # the client.
335 335 req.env.get('X-HgHttp2', '')):
336 336 req.drain()
337 337 else:
338 338 req.headers.append((r'Connection', r'Close'))
339 339
340 340 # TODO This response body assumes the failed command was
341 341 # "unbundle." That assumption is not always valid.
342 req.respond(e, HGTYPE, body='0\n%s\n' % e)
342 req.respond(e, HGTYPE, body='0\n%s\n' % pycompat.bytestr(e))
343 343
344 344 return ''
345 345
346 346 def _sshv1respondbytes(fout, value):
347 347 """Send a bytes response for protocol version 1."""
348 348 fout.write('%d\n' % len(value))
349 349 fout.write(value)
350 350 fout.flush()
351 351
352 352 def _sshv1respondstream(fout, source):
353 353 write = fout.write
354 354 for chunk in source.gen:
355 355 write(chunk)
356 356 fout.flush()
357 357
358 358 def _sshv1respondooberror(fout, ferr, rsp):
359 359 ferr.write(b'%s\n-\n' % rsp)
360 360 ferr.flush()
361 361 fout.write(b'\n')
362 362 fout.flush()
363 363
364 364 class sshv1protocolhandler(baseprotocolhandler):
365 365 """Handler for requests services via version 1 of SSH protocol."""
366 366 def __init__(self, ui, fin, fout):
367 367 self._ui = ui
368 368 self._fin = fin
369 369 self._fout = fout
370 370
371 371 @property
372 372 def name(self):
373 373 return SSHV1
374 374
375 375 def getargs(self, args):
376 376 data = {}
377 377 keys = args.split()
378 378 for n in xrange(len(keys)):
379 379 argline = self._fin.readline()[:-1]
380 380 arg, l = argline.split()
381 381 if arg not in keys:
382 382 raise error.Abort(_("unexpected parameter %r") % arg)
383 383 if arg == '*':
384 384 star = {}
385 385 for k in xrange(int(l)):
386 386 argline = self._fin.readline()[:-1]
387 387 arg, l = argline.split()
388 388 val = self._fin.read(int(l))
389 389 star[arg] = val
390 390 data['*'] = star
391 391 else:
392 392 val = self._fin.read(int(l))
393 393 data[arg] = val
394 394 return [data[k] for k in keys]
395 395
396 396 def forwardpayload(self, fpout):
397 397 # The file is in the form:
398 398 #
399 399 # <chunk size>\n<chunk>
400 400 # ...
401 401 # 0\n
402 402 _sshv1respondbytes(self._fout, b'')
403 403 count = int(self._fin.readline())
404 404 while count:
405 405 fpout.write(self._fin.read(count))
406 406 count = int(self._fin.readline())
407 407
408 408 @contextlib.contextmanager
409 409 def mayberedirectstdio(self):
410 410 yield None
411 411
412 412 def client(self):
413 413 client = encoding.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
414 414 return 'remote:ssh:' + client
415 415
416 416 class sshv2protocolhandler(sshv1protocolhandler):
417 417 """Protocol handler for version 2 of the SSH protocol."""
418 418
419 419 def _runsshserver(ui, repo, fin, fout):
420 420 # This function operates like a state machine of sorts. The following
421 421 # states are defined:
422 422 #
423 423 # protov1-serving
424 424 # Server is in protocol version 1 serving mode. Commands arrive on
425 425 # new lines. These commands are processed in this state, one command
426 426 # after the other.
427 427 #
428 428 # protov2-serving
429 429 # Server is in protocol version 2 serving mode.
430 430 #
431 431 # upgrade-initial
432 432 # The server is going to process an upgrade request.
433 433 #
434 434 # upgrade-v2-filter-legacy-handshake
435 435 # The protocol is being upgraded to version 2. The server is expecting
436 436 # the legacy handshake from version 1.
437 437 #
438 438 # upgrade-v2-finish
439 439 # The upgrade to version 2 of the protocol is imminent.
440 440 #
441 441 # shutdown
442 442 # The server is shutting down, possibly in reaction to a client event.
443 443 #
444 444 # And here are their transitions:
445 445 #
446 446 # protov1-serving -> shutdown
447 447 # When server receives an empty request or encounters another
448 448 # error.
449 449 #
450 450 # protov1-serving -> upgrade-initial
451 451 # An upgrade request line was seen.
452 452 #
453 453 # upgrade-initial -> upgrade-v2-filter-legacy-handshake
454 454 # Upgrade to version 2 in progress. Server is expecting to
455 455 # process a legacy handshake.
456 456 #
457 457 # upgrade-v2-filter-legacy-handshake -> shutdown
458 458 # Client did not fulfill upgrade handshake requirements.
459 459 #
460 460 # upgrade-v2-filter-legacy-handshake -> upgrade-v2-finish
461 461 # Client fulfilled version 2 upgrade requirements. Finishing that
462 462 # upgrade.
463 463 #
464 464 # upgrade-v2-finish -> protov2-serving
465 465 # Protocol upgrade to version 2 complete. Server can now speak protocol
466 466 # version 2.
467 467 #
468 468 # protov2-serving -> protov1-serving
469 469 # Ths happens by default since protocol version 2 is the same as
470 470 # version 1 except for the handshake.
471 471
472 472 state = 'protov1-serving'
473 473 proto = sshv1protocolhandler(ui, fin, fout)
474 474 protoswitched = False
475 475
476 476 while True:
477 477 if state == 'protov1-serving':
478 478 # Commands are issued on new lines.
479 479 request = fin.readline()[:-1]
480 480
481 481 # Empty lines signal to terminate the connection.
482 482 if not request:
483 483 state = 'shutdown'
484 484 continue
485 485
486 486 # It looks like a protocol upgrade request. Transition state to
487 487 # handle it.
488 488 if request.startswith(b'upgrade '):
489 489 if protoswitched:
490 490 _sshv1respondooberror(fout, ui.ferr,
491 491 b'cannot upgrade protocols multiple '
492 492 b'times')
493 493 state = 'shutdown'
494 494 continue
495 495
496 496 state = 'upgrade-initial'
497 497 continue
498 498
499 499 available = wireproto.commands.commandavailable(request, proto)
500 500
501 501 # This command isn't available. Send an empty response and go
502 502 # back to waiting for a new command.
503 503 if not available:
504 504 _sshv1respondbytes(fout, b'')
505 505 continue
506 506
507 507 rsp = wireproto.dispatch(repo, proto, request)
508 508
509 509 if isinstance(rsp, bytes):
510 510 _sshv1respondbytes(fout, rsp)
511 511 elif isinstance(rsp, wireprototypes.bytesresponse):
512 512 _sshv1respondbytes(fout, rsp.data)
513 513 elif isinstance(rsp, wireprototypes.streamres):
514 514 _sshv1respondstream(fout, rsp)
515 515 elif isinstance(rsp, wireprototypes.streamreslegacy):
516 516 _sshv1respondstream(fout, rsp)
517 517 elif isinstance(rsp, wireprototypes.pushres):
518 518 _sshv1respondbytes(fout, b'')
519 519 _sshv1respondbytes(fout, b'%d' % rsp.res)
520 520 elif isinstance(rsp, wireprototypes.pusherr):
521 521 _sshv1respondbytes(fout, rsp.res)
522 522 elif isinstance(rsp, wireprototypes.ooberror):
523 523 _sshv1respondooberror(fout, ui.ferr, rsp.message)
524 524 else:
525 525 raise error.ProgrammingError('unhandled response type from '
526 526 'wire protocol command: %s' % rsp)
527 527
528 528 # For now, protocol version 2 serving just goes back to version 1.
529 529 elif state == 'protov2-serving':
530 530 state = 'protov1-serving'
531 531 continue
532 532
533 533 elif state == 'upgrade-initial':
534 534 # We should never transition into this state if we've switched
535 535 # protocols.
536 536 assert not protoswitched
537 537 assert proto.name == SSHV1
538 538
539 539 # Expected: upgrade <token> <capabilities>
540 540 # If we get something else, the request is malformed. It could be
541 541 # from a future client that has altered the upgrade line content.
542 542 # We treat this as an unknown command.
543 543 try:
544 544 token, caps = request.split(b' ')[1:]
545 545 except ValueError:
546 546 _sshv1respondbytes(fout, b'')
547 547 state = 'protov1-serving'
548 548 continue
549 549
550 550 # Send empty response if we don't support upgrading protocols.
551 551 if not ui.configbool('experimental', 'sshserver.support-v2'):
552 552 _sshv1respondbytes(fout, b'')
553 553 state = 'protov1-serving'
554 554 continue
555 555
556 556 try:
557 557 caps = urlreq.parseqs(caps)
558 558 except ValueError:
559 559 _sshv1respondbytes(fout, b'')
560 560 state = 'protov1-serving'
561 561 continue
562 562
563 563 # We don't see an upgrade request to protocol version 2. Ignore
564 564 # the upgrade request.
565 565 wantedprotos = caps.get(b'proto', [b''])[0]
566 566 if SSHV2 not in wantedprotos:
567 567 _sshv1respondbytes(fout, b'')
568 568 state = 'protov1-serving'
569 569 continue
570 570
571 571 # It looks like we can honor this upgrade request to protocol 2.
572 572 # Filter the rest of the handshake protocol request lines.
573 573 state = 'upgrade-v2-filter-legacy-handshake'
574 574 continue
575 575
576 576 elif state == 'upgrade-v2-filter-legacy-handshake':
577 577 # Client should have sent legacy handshake after an ``upgrade``
578 578 # request. Expected lines:
579 579 #
580 580 # hello
581 581 # between
582 582 # pairs 81
583 583 # 0000...-0000...
584 584
585 585 ok = True
586 586 for line in (b'hello', b'between', b'pairs 81'):
587 587 request = fin.readline()[:-1]
588 588
589 589 if request != line:
590 590 _sshv1respondooberror(fout, ui.ferr,
591 591 b'malformed handshake protocol: '
592 592 b'missing %s' % line)
593 593 ok = False
594 594 state = 'shutdown'
595 595 break
596 596
597 597 if not ok:
598 598 continue
599 599
600 600 request = fin.read(81)
601 601 if request != b'%s-%s' % (b'0' * 40, b'0' * 40):
602 602 _sshv1respondooberror(fout, ui.ferr,
603 603 b'malformed handshake protocol: '
604 604 b'missing between argument value')
605 605 state = 'shutdown'
606 606 continue
607 607
608 608 state = 'upgrade-v2-finish'
609 609 continue
610 610
611 611 elif state == 'upgrade-v2-finish':
612 612 # Send the upgrade response.
613 613 fout.write(b'upgraded %s %s\n' % (token, SSHV2))
614 614 servercaps = wireproto.capabilities(repo, proto)
615 615 rsp = b'capabilities: %s' % servercaps.data
616 616 fout.write(b'%d\n%s\n' % (len(rsp), rsp))
617 617 fout.flush()
618 618
619 619 proto = sshv2protocolhandler(ui, fin, fout)
620 620 protoswitched = True
621 621
622 622 state = 'protov2-serving'
623 623 continue
624 624
625 625 elif state == 'shutdown':
626 626 break
627 627
628 628 else:
629 629 raise error.ProgrammingError('unhandled ssh server state: %s' %
630 630 state)
631 631
632 632 class sshserver(object):
633 633 def __init__(self, ui, repo):
634 634 self._ui = ui
635 635 self._repo = repo
636 636 self._fin = ui.fin
637 637 self._fout = ui.fout
638 638
639 639 hook.redirect(True)
640 640 ui.fout = repo.ui.fout = ui.ferr
641 641
642 642 # Prevent insertion/deletion of CRs
643 643 util.setbinary(self._fin)
644 644 util.setbinary(self._fout)
645 645
646 646 def serve_forever(self):
647 647 _runsshserver(self._ui, self._repo, self._fin, self._fout)
648 648 sys.exit(0)
General Comments 0
You need to be logged in to leave comments. Login now