Mercurial > hg
comparison mercurial/wireprotoserver.py @ 37054:e7a012b60d6e
wireproto: implement basic command dispatching for HTTPv2
Now that we can ingest frames and decode them to requests to run
commands, we are able to actually run those commands. So this
commit starts to implement that.
There are numerous shortcomings. We can't operate on commands
with "*" arguments. We can only emit bytesresponse results. We
don't yet issue a response in the unified framing protocol.
But it's a start.
Differential Revision: https://phab.mercurial-scm.org/D2857
author | Gregory Szorc <gregory.szorc@gmail.com> |
---|---|
date | Wed, 14 Mar 2018 13:32:31 -0700 |
parents | cd0ca979a8b8 |
children | 61393f888dfe |
comparison
equal
deleted
inserted
replaced
37053:cd0ca979a8b8 | 37054:e7a012b60d6e |
---|---|
358 res.headers[b'Content-Type'] = b'text/plain' | 358 res.headers[b'Content-Type'] = b'text/plain' |
359 res.setbodybytes(_('client MUST send Content-Type header with ' | 359 res.setbodybytes(_('client MUST send Content-Type header with ' |
360 'value: %s\n') % FRAMINGTYPE) | 360 'value: %s\n') % FRAMINGTYPE) |
361 return | 361 return |
362 | 362 |
363 # We don't do anything meaningful yet. | 363 _processhttpv2request(ui, repo, req, res, permission, command, proto) |
364 res.status = b'200 OK' | |
365 res.headers[b'Content-Type'] = b'text/plain' | |
366 res.setbodybytes(b'/'.join(urlparts) + b'\n') | |
367 | 364 |
368 def _processhttpv2reflectrequest(ui, repo, req, res): | 365 def _processhttpv2reflectrequest(ui, repo, req, res): |
369 """Reads unified frame protocol request and dumps out state to client. | 366 """Reads unified frame protocol request and dumps out state to client. |
370 | 367 |
371 This special endpoint can be used to help debug the wire protocol. | 368 This special endpoint can be used to help debug the wire protocol. |
405 separators=(', ', ': '))) | 402 separators=(', ', ': '))) |
406 | 403 |
407 res.status = b'200 OK' | 404 res.status = b'200 OK' |
408 res.headers[b'Content-Type'] = b'text/plain' | 405 res.headers[b'Content-Type'] = b'text/plain' |
409 res.setbodybytes(b'\n'.join(states)) | 406 res.setbodybytes(b'\n'.join(states)) |
407 | |
408 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto): | |
409 """Post-validation handler for HTTPv2 requests. | |
410 | |
411 Called when the HTTP request contains unified frame-based protocol | |
412 frames for evaluation. | |
413 """ | |
414 reactor = wireprotoframing.serverreactor() | |
415 seencommand = False | |
416 | |
417 while True: | |
418 frame = wireprotoframing.readframe(req.bodyfh) | |
419 if not frame: | |
420 break | |
421 | |
422 action, meta = reactor.onframerecv(*frame) | |
423 | |
424 if action == 'wantframe': | |
425 # Need more data before we can do anything. | |
426 continue | |
427 elif action == 'runcommand': | |
428 # We currently only support running a single command per | |
429 # HTTP request. | |
430 if seencommand: | |
431 # TODO define proper error mechanism. | |
432 res.status = b'200 OK' | |
433 res.headers[b'Content-Type'] = b'text/plain' | |
434 res.setbodybytes(_('support for multiple commands per request ' | |
435 'not yet implemented')) | |
436 return | |
437 | |
438 _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, | |
439 reactor, meta) | |
440 | |
441 elif action == 'error': | |
442 # TODO define proper error mechanism. | |
443 res.status = b'200 OK' | |
444 res.headers[b'Content-Type'] = b'text/plain' | |
445 res.setbodybytes(meta['message'] + b'\n') | |
446 return | |
447 else: | |
448 raise error.ProgrammingError( | |
449 'unhandled action from frame processor: %s' % action) | |
450 | |
451 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor, | |
452 command): | |
453 """Dispatch a wire protocol command made from HTTPv2 requests. | |
454 | |
455 The authenticated permission (``authedperm``) along with the original | |
456 command from the URL (``reqcommand``) are passed in. | |
457 """ | |
458 # We already validated that the session has permissions to perform the | |
459 # actions in ``authedperm``. In the unified frame protocol, the canonical | |
460 # command to run is expressed in a frame. However, the URL also requested | |
461 # to run a specific command. We need to be careful that the command we | |
462 # run doesn't have permissions requirements greater than what was granted | |
463 # by ``authedperm``. | |
464 # | |
465 # For now, this is no big deal, as we only allow a single command per | |
466 # request and that command must match the command in the URL. But when | |
467 # things change, we need to watch out... | |
468 if reqcommand != command['command']: | |
469 # TODO define proper error mechanism | |
470 res.status = b'200 OK' | |
471 res.headers[b'Content-Type'] = b'text/plain' | |
472 res.setbodybytes(_('command in frame must match command in URL')) | |
473 return | |
474 | |
475 # TODO once we get rid of the command==URL restriction, we'll need to | |
476 # revalidate command validity and auth here. checkperm, | |
477 # wireproto.commands.commandavailable(), etc. | |
478 | |
479 proto = httpv2protocolhandler(req, ui, args=command['args']) | |
480 assert wireproto.commands.commandavailable(command['command'], proto) | |
481 wirecommand = wireproto.commands[command['command']] | |
482 | |
483 assert authedperm in (b'ro', b'rw') | |
484 assert wirecommand.permission in ('push', 'pull') | |
485 | |
486 # We already checked this as part of the URL==command check, but | |
487 # permissions are important, so do it again. | |
488 if authedperm == b'ro': | |
489 assert wirecommand.permission == 'pull' | |
490 elif authedperm == b'rw': | |
491 # We are allowed to access read-only commands under the rw URL. | |
492 assert wirecommand.permission in ('push', 'pull') | |
493 | |
494 rsp = wireproto.dispatch(repo, proto, command['command']) | |
495 | |
496 # TODO use proper response format. | |
497 res.status = b'200 OK' | |
498 res.headers[b'Content-Type'] = b'text/plain' | |
499 | |
500 if isinstance(rsp, wireprototypes.bytesresponse): | |
501 res.setbodybytes(rsp.data) | |
502 else: | |
503 res.setbodybytes(b'unhandled response type from wire proto ' | |
504 'command') | |
410 | 505 |
411 # Maps API name to metadata so custom API can be registered. | 506 # Maps API name to metadata so custom API can be registered. |
412 API_HANDLERS = { | 507 API_HANDLERS = { |
413 HTTPV2: { | 508 HTTPV2: { |
414 'config': ('experimental', 'web.api.http-v2'), | 509 'config': ('experimental', 'web.api.http-v2'), |
415 'handler': _handlehttpv2request, | 510 'handler': _handlehttpv2request, |
416 }, | 511 }, |
417 } | 512 } |
418 | 513 |
419 class httpv2protocolhandler(wireprototypes.baseprotocolhandler): | 514 class httpv2protocolhandler(wireprototypes.baseprotocolhandler): |
420 def __init__(self, req, ui): | 515 def __init__(self, req, ui, args=None): |
421 self._req = req | 516 self._req = req |
422 self._ui = ui | 517 self._ui = ui |
518 self._args = args | |
423 | 519 |
424 @property | 520 @property |
425 def name(self): | 521 def name(self): |
426 return HTTPV2 | 522 return HTTPV2 |
427 | 523 |
428 def getargs(self, args): | 524 def getargs(self, args): |
429 raise NotImplementedError | 525 data = {} |
526 for k in args.split(): | |
527 if k == '*': | |
528 raise NotImplementedError('do not support * args') | |
529 else: | |
530 data[k] = self._args[k] | |
531 | |
532 return [data[k] for k in args.split()] | |
430 | 533 |
431 def forwardpayload(self, fp): | 534 def forwardpayload(self, fp): |
432 raise NotImplementedError | 535 raise NotImplementedError |
433 | 536 |
434 @contextlib.contextmanager | 537 @contextlib.contextmanager |
437 | 540 |
438 def client(self): | 541 def client(self): |
439 raise NotImplementedError | 542 raise NotImplementedError |
440 | 543 |
441 def addcapabilities(self, repo, caps): | 544 def addcapabilities(self, repo, caps): |
442 raise NotImplementedError | 545 return caps |
443 | 546 |
444 def checkperm(self, perm): | 547 def checkperm(self, perm): |
445 raise NotImplementedError | 548 raise NotImplementedError |
446 | 549 |
447 def _httpresponsetype(ui, req, prefer_uncompressed): | 550 def _httpresponsetype(ui, req, prefer_uncompressed): |