Mercurial > public > mercurial-scm > hg
comparison mercurial/wireprotoserver.py @ 37059:bbea991635d0
wireproto: service multiple command requests per HTTP request
Now that our new frame-based protocol server can understand how
to ingest multiple, possibly interleaved, command requests, let's
hook it up to the HTTP server.
The code on the HTTP side of things is still a bit hacky. We need
a bit of work around error handling, content types, etc. But it's
a start.
Among the added tests, we demonstrate that a client can send frames
for multiple commands iterleaved with each other and that a later
issued command can respond before the first one has finished
sending. This makes our multi-request model technically superior
to the previous "batch" command.
Differential Revision: https://phab.mercurial-scm.org/D2871
author | Gregory Szorc <gregory.szorc@gmail.com> |
---|---|
date | Mon, 19 Mar 2018 16:55:07 -0700 |
parents | 2ec1fb9de638 |
children | 884a0c1604ad |
comparison
equal
deleted
inserted
replaced
37058:c5e9c3b47366 | 37059:bbea991635d0 |
---|---|
325 # We have a special endpoint to reflect the request back at the client. | 325 # We have a special endpoint to reflect the request back at the client. |
326 if command == b'debugreflect': | 326 if command == b'debugreflect': |
327 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res) | 327 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res) |
328 return | 328 return |
329 | 329 |
330 if command not in wireproto.commands: | 330 # Extra commands that we handle that aren't really wire protocol |
331 # commands. Think extra hard before making this hackery available to | |
332 # extension. | |
333 extracommands = {'multirequest'} | |
334 | |
335 if command not in wireproto.commands and command not in extracommands: | |
331 res.status = b'404 Not Found' | 336 res.status = b'404 Not Found' |
332 res.headers[b'Content-Type'] = b'text/plain' | 337 res.headers[b'Content-Type'] = b'text/plain' |
333 res.setbodybytes(_('unknown wire protocol command: %s\n') % command) | 338 res.setbodybytes(_('unknown wire protocol command: %s\n') % command) |
334 return | 339 return |
335 | 340 |
336 repo = rctx.repo | 341 repo = rctx.repo |
337 ui = repo.ui | 342 ui = repo.ui |
338 | 343 |
339 proto = httpv2protocolhandler(req, ui) | 344 proto = httpv2protocolhandler(req, ui) |
340 | 345 |
341 if not wireproto.commands.commandavailable(command, proto): | 346 if (not wireproto.commands.commandavailable(command, proto) |
347 and command not in extracommands): | |
342 res.status = b'404 Not Found' | 348 res.status = b'404 Not Found' |
343 res.headers[b'Content-Type'] = b'text/plain' | 349 res.headers[b'Content-Type'] = b'text/plain' |
344 res.setbodybytes(_('invalid wire protocol command: %s') % command) | 350 res.setbodybytes(_('invalid wire protocol command: %s') % command) |
345 return | 351 return |
346 | 352 |
432 | 438 |
433 if action == 'wantframe': | 439 if action == 'wantframe': |
434 # Need more data before we can do anything. | 440 # Need more data before we can do anything. |
435 continue | 441 continue |
436 elif action == 'runcommand': | 442 elif action == 'runcommand': |
437 # We currently only support running a single command per | 443 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm, |
438 # HTTP request. | 444 reqcommand, reactor, meta, |
439 if seencommand: | 445 issubsequent=seencommand) |
440 # TODO define proper error mechanism. | 446 |
441 res.status = b'200 OK' | 447 if sentoutput: |
442 res.headers[b'Content-Type'] = b'text/plain' | |
443 res.setbodybytes(_('support for multiple commands per request ' | |
444 'not yet implemented')) | |
445 return | 448 return |
446 | 449 |
447 _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, | 450 seencommand = True |
448 reactor, meta) | |
449 | 451 |
450 elif action == 'error': | 452 elif action == 'error': |
451 # TODO define proper error mechanism. | 453 # TODO define proper error mechanism. |
452 res.status = b'200 OK' | 454 res.status = b'200 OK' |
453 res.headers[b'Content-Type'] = b'text/plain' | 455 res.headers[b'Content-Type'] = b'text/plain' |
469 else: | 471 else: |
470 raise error.ProgrammingError('unhandled action from frame processor: %s' | 472 raise error.ProgrammingError('unhandled action from frame processor: %s' |
471 % action) | 473 % action) |
472 | 474 |
473 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor, | 475 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor, |
474 command): | 476 command, issubsequent): |
475 """Dispatch a wire protocol command made from HTTPv2 requests. | 477 """Dispatch a wire protocol command made from HTTPv2 requests. |
476 | 478 |
477 The authenticated permission (``authedperm``) along with the original | 479 The authenticated permission (``authedperm``) along with the original |
478 command from the URL (``reqcommand``) are passed in. | 480 command from the URL (``reqcommand``) are passed in. |
479 """ | 481 """ |
482 # command to run is expressed in a frame. However, the URL also requested | 484 # command to run is expressed in a frame. However, the URL also requested |
483 # to run a specific command. We need to be careful that the command we | 485 # to run a specific command. We need to be careful that the command we |
484 # run doesn't have permissions requirements greater than what was granted | 486 # run doesn't have permissions requirements greater than what was granted |
485 # by ``authedperm``. | 487 # by ``authedperm``. |
486 # | 488 # |
487 # For now, this is no big deal, as we only allow a single command per | 489 # Our rule for this is we only allow one command per HTTP request and |
488 # request and that command must match the command in the URL. But when | 490 # that command must match the command in the URL. However, we make |
489 # things change, we need to watch out... | 491 # an exception for the ``multirequest`` URL. This URL is allowed to |
490 if reqcommand != command['command']: | 492 # execute multiple commands. We double check permissions of each command |
491 # TODO define proper error mechanism | 493 # as it is invoked to ensure there is no privilege escalation. |
492 res.status = b'200 OK' | 494 # TODO consider allowing multiple commands to regular command URLs |
493 res.headers[b'Content-Type'] = b'text/plain' | 495 # iff each command is the same. |
494 res.setbodybytes(_('command in frame must match command in URL')) | |
495 return | |
496 | |
497 # TODO once we get rid of the command==URL restriction, we'll need to | |
498 # revalidate command validity and auth here. checkperm, | |
499 # wireproto.commands.commandavailable(), etc. | |
500 | 496 |
501 proto = httpv2protocolhandler(req, ui, args=command['args']) | 497 proto = httpv2protocolhandler(req, ui, args=command['args']) |
502 assert wireproto.commands.commandavailable(command['command'], proto) | 498 |
503 wirecommand = wireproto.commands[command['command']] | 499 if reqcommand == b'multirequest': |
504 | 500 if not wireproto.commands.commandavailable(command['command'], proto): |
505 assert authedperm in (b'ro', b'rw') | 501 # TODO proper error mechanism |
506 assert wirecommand.permission in ('push', 'pull') | 502 res.status = b'200 OK' |
507 | 503 res.headers[b'Content-Type'] = b'text/plain' |
508 # We already checked this as part of the URL==command check, but | 504 res.setbodybytes(_('wire protocol command not available: %s') % |
509 # permissions are important, so do it again. | 505 command['command']) |
510 if authedperm == b'ro': | 506 return True |
511 assert wirecommand.permission == 'pull' | 507 |
512 elif authedperm == b'rw': | 508 assert authedperm in (b'ro', b'rw') |
513 # We are allowed to access read-only commands under the rw URL. | 509 wirecommand = wireproto.commands[command['command']] |
514 assert wirecommand.permission in ('push', 'pull') | 510 assert wirecommand.permission in ('push', 'pull') |
511 | |
512 if authedperm == b'ro' and wirecommand.permission != 'pull': | |
513 # TODO proper error mechanism | |
514 res.status = b'403 Forbidden' | |
515 res.headers[b'Content-Type'] = b'text/plain' | |
516 res.setbodybytes(_('insufficient permissions to execute ' | |
517 'command: %s') % command['command']) | |
518 return True | |
519 | |
520 # TODO should we also call checkperm() here? Maybe not if we're going | |
521 # to overhaul that API. The granted scope from the URL check should | |
522 # be good enough. | |
523 | |
524 else: | |
525 # Don't allow multiple commands outside of ``multirequest`` URL. | |
526 if issubsequent: | |
527 # TODO proper error mechanism | |
528 res.status = b'200 OK' | |
529 res.headers[b'Content-Type'] = b'text/plain' | |
530 res.setbodybytes(_('multiple commands cannot be issued to this ' | |
531 'URL')) | |
532 return True | |
533 | |
534 if reqcommand != command['command']: | |
535 # TODO define proper error mechanism | |
536 res.status = b'200 OK' | |
537 res.headers[b'Content-Type'] = b'text/plain' | |
538 res.setbodybytes(_('command in frame must match command in URL')) | |
539 return True | |
515 | 540 |
516 rsp = wireproto.dispatch(repo, proto, command['command']) | 541 rsp = wireproto.dispatch(repo, proto, command['command']) |
517 | 542 |
518 res.status = b'200 OK' | 543 res.status = b'200 OK' |
519 res.headers[b'Content-Type'] = FRAMINGTYPE | 544 res.headers[b'Content-Type'] = FRAMINGTYPE |
525 action, meta = reactor.onapplicationerror( | 550 action, meta = reactor.onapplicationerror( |
526 _('unhandled response type from wire proto command')) | 551 _('unhandled response type from wire proto command')) |
527 | 552 |
528 if action == 'sendframes': | 553 if action == 'sendframes': |
529 res.setbodygen(meta['framegen']) | 554 res.setbodygen(meta['framegen']) |
555 return True | |
530 elif action == 'noop': | 556 elif action == 'noop': |
531 pass | 557 pass |
532 else: | 558 else: |
533 raise error.ProgrammingError('unhandled event from reactor: %s' % | 559 raise error.ProgrammingError('unhandled event from reactor: %s' % |
534 action) | 560 action) |