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):