comparison mercurial/wireprotoserver.py @ 37050:fddcb51b5084

wireproto: define permissions-based routing of HTTPv2 wire protocol Now that we have a scaffolding for serving version 2 of the HTTP protocol, let's start implementing it. A good place to start is URL routing and basic request processing semantics. We can focus on content types, capabilities detect, etc later. Version 2 of the HTTP wire protocol encodes the needed permissions of the request in the URL path. The reasons for this are documented in the added documentation. In short, a) it makes it really easy and fail proof for server administrators to implement path-based authentication and b) it will enable clients to realize very early in a server exchange that authentication will be required to complete the operation. This latter point avoids all kinds of complexity and problems, like dealing with Expect: 100-continue and clients finding out later during `hg push` that they need to provide authentication. This will avoid the current badness where clients send a full bundle, get an HTTP 403, provide authentication, then retransmit the bundle. In order to implement command checking, we needed to implement a protocol handler for the new wire protocol. Our handler is just small enough to run the code we've implemented. Tests for the defined functionality have been added. I very much want to refactor the permissions checking code and define a better response format. But this can be done later. Nothing is covered by backwards compatibility at this point. Differential Revision: https://phab.mercurial-scm.org/D2836
author Gregory Szorc <gregory.szorc@gmail.com>
date Mon, 19 Mar 2018 16:43:47 -0700
parents 1cfef5693203
children fc5e261915b9
comparison
equal deleted inserted replaced
37049:1cfef5693203 37050:fddcb51b5084
270 270
271 API_HANDLERS[proto]['handler'](rctx, req, res, checkperm, 271 API_HANDLERS[proto]['handler'](rctx, req, res, checkperm,
272 req.dispatchparts[2:]) 272 req.dispatchparts[2:])
273 273
274 def _handlehttpv2request(rctx, req, res, checkperm, urlparts): 274 def _handlehttpv2request(rctx, req, res, checkperm, urlparts):
275 from .hgweb import common as hgwebcommon
276
277 # URL space looks like: <permissions>/<command>, where <permission> can
278 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
279
280 # Root URL does nothing meaningful... yet.
281 if not urlparts:
282 res.status = b'200 OK'
283 res.headers[b'Content-Type'] = b'text/plain'
284 res.setbodybytes(_('HTTP version 2 API handler'))
285 return
286
287 if len(urlparts) == 1:
288 res.status = b'404 Not Found'
289 res.headers[b'Content-Type'] = b'text/plain'
290 res.setbodybytes(_('do not know how to process %s\n') %
291 req.dispatchpath)
292 return
293
294 permission, command = urlparts[0:2]
295
296 if permission not in (b'ro', b'rw'):
297 res.status = b'404 Not Found'
298 res.headers[b'Content-Type'] = b'text/plain'
299 res.setbodybytes(_('unknown permission: %s') % permission)
300 return
301
302 # At some point we'll want to use our own API instead of recycling the
303 # behavior of version 1 of the wire protocol...
304 # TODO return reasonable responses - not responses that overload the
305 # HTTP status line message for error reporting.
306 try:
307 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
308 except hgwebcommon.ErrorResponse as e:
309 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
310 for k, v in e.headers:
311 res.headers[k] = v
312 res.setbodybytes('permission denied')
313 return
314
315 if command not in wireproto.commands:
316 res.status = b'404 Not Found'
317 res.headers[b'Content-Type'] = b'text/plain'
318 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
319 return
320
321 repo = rctx.repo
322 ui = repo.ui
323
324 proto = httpv2protocolhandler(req, ui)
325
326 if not wireproto.commands.commandavailable(command, proto):
327 res.status = b'404 Not Found'
328 res.headers[b'Content-Type'] = b'text/plain'
329 res.setbodybytes(_('invalid wire protocol command: %s') % command)
330 return
331
332 # We don't do anything meaningful yet.
275 res.status = b'200 OK' 333 res.status = b'200 OK'
276 res.headers[b'Content-Type'] = b'text/plain' 334 res.headers[b'Content-Type'] = b'text/plain'
277 res.setbodybytes(b'/'.join(urlparts) + b'\n') 335 res.setbodybytes(b'/'.join(urlparts) + b'\n')
278 336
279 # Maps API name to metadata so custom API can be registered. 337 # Maps API name to metadata so custom API can be registered.
281 HTTPV2: { 339 HTTPV2: {
282 'config': ('experimental', 'web.api.http-v2'), 340 'config': ('experimental', 'web.api.http-v2'),
283 'handler': _handlehttpv2request, 341 'handler': _handlehttpv2request,
284 }, 342 },
285 } 343 }
344
345 class httpv2protocolhandler(wireprototypes.baseprotocolhandler):
346 def __init__(self, req, ui):
347 self._req = req
348 self._ui = ui
349
350 @property
351 def name(self):
352 return HTTPV2
353
354 def getargs(self, args):
355 raise NotImplementedError
356
357 def forwardpayload(self, fp):
358 raise NotImplementedError
359
360 @contextlib.contextmanager
361 def mayberedirectstdio(self):
362 raise NotImplementedError
363
364 def client(self):
365 raise NotImplementedError
366
367 def addcapabilities(self, repo, caps):
368 raise NotImplementedError
369
370 def checkperm(self, perm):
371 raise NotImplementedError
286 372
287 def _httpresponsetype(ui, req, prefer_uncompressed): 373 def _httpresponsetype(ui, req, prefer_uncompressed):
288 """Determine the appropriate response type and compression settings. 374 """Determine the appropriate response type and compression settings.
289 375
290 Returns a tuple of (mediatype, compengine, engineopts). 376 Returns a tuple of (mediatype, compengine, engineopts).