Show More
@@ -203,6 +203,28 b' an HTTP 406.' | |||
|
203 | 203 | Servers receiving requests with an invalid ``Content-Type`` header SHOULD |
|
204 | 204 | respond with an HTTP 415. |
|
205 | 205 | |
|
206 | The command to run is specified in the POST payload as defined by the | |
|
207 | *Unified Frame-Based Protocol*. This is redundant with data already | |
|
208 | encoded in the URL. This is by design, so server operators can have | |
|
209 | better understanding about server activity from looking merely at | |
|
210 | HTTP access logs. | |
|
211 | ||
|
212 | In most circumstances, the command specified in the URL MUST match | |
|
213 | the command specified in the frame-based payload or the server will | |
|
214 | respond with an error. The exception to this is the special | |
|
215 | ``multirequest`` URL. (See below.) In addition, HTTP requests | |
|
216 | are limited to one command invocation. The exception is the special | |
|
217 | ``multirequest`` URL. | |
|
218 | ||
|
219 | The ``multirequest`` command endpoints (``ro/multirequest`` and | |
|
220 | ``rw/multirequest``) are special in that they allow the execution of | |
|
221 | *any* command and allow the execution of multiple commands. If the | |
|
222 | HTTP request issues multiple commands across multiple frames, all | |
|
223 | issued commands will be processed by the server. Per the defined | |
|
224 | behavior of the *Unified Frame-Based Protocol*, commands may be | |
|
225 | issued interleaved and responses may come back in a different order | |
|
226 | than they were issued. Clients MUST be able to deal with this. | |
|
227 | ||
|
206 | 228 | SSH Protocol |
|
207 | 229 | ============ |
|
208 | 230 |
@@ -327,7 +327,12 b' def _handlehttpv2request(rctx, req, res,' | |||
|
327 | 327 | _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res) |
|
328 | 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 | 336 | res.status = b'404 Not Found' |
|
332 | 337 | res.headers[b'Content-Type'] = b'text/plain' |
|
333 | 338 | res.setbodybytes(_('unknown wire protocol command: %s\n') % command) |
@@ -338,7 +343,8 b' def _handlehttpv2request(rctx, req, res,' | |||
|
338 | 343 | |
|
339 | 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 | 348 | res.status = b'404 Not Found' |
|
343 | 349 | res.headers[b'Content-Type'] = b'text/plain' |
|
344 | 350 | res.setbodybytes(_('invalid wire protocol command: %s') % command) |
@@ -434,18 +440,14 b' def _processhttpv2request(ui, repo, req,' | |||
|
434 | 440 | # Need more data before we can do anything. |
|
435 | 441 | continue |
|
436 | 442 | elif action == 'runcommand': |
|
437 | # We currently only support running a single command per | |
|
438 | # HTTP request. | |
|
439 | if seencommand: | |
|
440 | # TODO define proper error mechanism. | |
|
441 | res.status = b'200 OK' | |
|
442 | res.headers[b'Content-Type'] = b'text/plain' | |
|
443 | res.setbodybytes(_('support for multiple commands per request ' | |
|
444 | 'not yet implemented')) | |
|
443 | sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm, | |
|
444 | reqcommand, reactor, meta, | |
|
445 | issubsequent=seencommand) | |
|
446 | ||
|
447 | if sentoutput: | |
|
445 | 448 | return |
|
446 | 449 | |
|
447 | _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, | |
|
448 | reactor, meta) | |
|
450 | seencommand = True | |
|
449 | 451 | |
|
450 | 452 | elif action == 'error': |
|
451 | 453 | # TODO define proper error mechanism. |
@@ -471,7 +473,7 b' def _processhttpv2request(ui, repo, req,' | |||
|
471 | 473 | % action) |
|
472 | 474 | |
|
473 | 475 | def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor, |
|
474 | command): | |
|
476 | command, issubsequent): | |
|
475 | 477 | """Dispatch a wire protocol command made from HTTPv2 requests. |
|
476 | 478 | |
|
477 | 479 | The authenticated permission (``authedperm``) along with the original |
@@ -484,34 +486,57 b' def _httpv2runcommand(ui, repo, req, res' | |||
|
484 | 486 | # run doesn't have permissions requirements greater than what was granted |
|
485 | 487 | # by ``authedperm``. |
|
486 | 488 | # |
|
487 |
# |
|
|
488 |
# |
|
|
489 | # things change, we need to watch out... | |
|
490 | if reqcommand != command['command']: | |
|
491 | # TODO define proper error mechanism | |
|
492 | res.status = b'200 OK' | |
|
493 | res.headers[b'Content-Type'] = b'text/plain' | |
|
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. | |
|
489 | # Our rule for this is we only allow one command per HTTP request and | |
|
490 | # that command must match the command in the URL. However, we make | |
|
491 | # an exception for the ``multirequest`` URL. This URL is allowed to | |
|
492 | # execute multiple commands. We double check permissions of each command | |
|
493 | # as it is invoked to ensure there is no privilege escalation. | |
|
494 | # TODO consider allowing multiple commands to regular command URLs | |
|
495 | # iff each command is the same. | |
|
500 | 496 | |
|
501 | 497 | proto = httpv2protocolhandler(req, ui, args=command['args']) |
|
502 | assert wireproto.commands.commandavailable(command['command'], proto) | |
|
503 | wirecommand = wireproto.commands[command['command']] | |
|
504 | 498 | |
|
505 | assert authedperm in (b'ro', b'rw') | |
|
506 | assert wirecommand.permission in ('push', 'pull') | |
|
499 | if reqcommand == b'multirequest': | |
|
500 | if not wireproto.commands.commandavailable(command['command'], proto): | |
|
501 | # TODO proper error mechanism | |
|
502 | res.status = b'200 OK' | |
|
503 | res.headers[b'Content-Type'] = b'text/plain' | |
|
504 | res.setbodybytes(_('wire protocol command not available: %s') % | |
|
505 | command['command']) | |
|
506 | return True | |
|
507 | ||
|
508 | assert authedperm in (b'ro', b'rw') | |
|
509 | wirecommand = wireproto.commands[command['command']] | |
|
510 | assert wirecommand.permission in ('push', 'pull') | |
|
507 | 511 | |
|
508 | # We already checked this as part of the URL==command check, but | |
|
509 | # permissions are important, so do it again. | |
|
510 | if authedperm == b'ro': | |
|
511 | assert wirecommand.permission == 'pull' | |
|
512 | elif authedperm == b'rw': | |
|
513 | # We are allowed to access read-only commands under the rw URL. | |
|
514 | assert wirecommand.permission in ('push', 'pull') | |
|
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 | 541 | rsp = wireproto.dispatch(repo, proto, command['command']) |
|
517 | 542 | |
@@ -527,6 +552,7 b' def _httpv2runcommand(ui, repo, req, res' | |||
|
527 | 552 | |
|
528 | 553 | if action == 'sendframes': |
|
529 | 554 | res.setbodygen(meta['framegen']) |
|
555 | return True | |
|
530 | 556 | elif action == 'noop': |
|
531 | 557 | pass |
|
532 | 558 | else: |
@@ -412,4 +412,153 b' Command frames can be reflected via debu' | |||
|
412 | 412 | s> received: <no frame>\n |
|
413 | 413 | s> {"action": "noop"} |
|
414 | 414 | |
|
415 | Multiple requests to regular command URL are not allowed | |
|
416 | ||
|
417 | $ send << EOF | |
|
418 | > httprequest POST api/$HTTPV2/ro/customreadonly | |
|
419 | > accept: $MEDIATYPE | |
|
420 | > content-type: $MEDIATYPE | |
|
421 | > user-agent: test | |
|
422 | > frame 1 command-name eos customreadonly | |
|
423 | > frame 3 command-name eos customreadonly | |
|
424 | > EOF | |
|
425 | using raw connection to peer | |
|
426 | s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n | |
|
427 | s> Accept-Encoding: identity\r\n | |
|
428 | s> accept: application/mercurial-exp-framing-0002\r\n | |
|
429 | s> content-type: application/mercurial-exp-framing-0002\r\n | |
|
430 | s> user-agent: test\r\n | |
|
431 | s> content-length: 40\r\n | |
|
432 | s> host: $LOCALIP:$HGPORT\r\n (glob) | |
|
433 | s> \r\n | |
|
434 | s> \x0e\x00\x00\x01\x00\x11customreadonly\x0e\x00\x00\x03\x00\x11customreadonly | |
|
435 | s> makefile('rb', None) | |
|
436 | s> HTTP/1.1 200 OK\r\n | |
|
437 | s> Server: testing stub value\r\n | |
|
438 | s> Date: $HTTP_DATE$\r\n | |
|
439 | s> Content-Type: text/plain\r\n | |
|
440 | s> Content-Length: 46\r\n | |
|
441 | s> \r\n | |
|
442 | s> multiple commands cannot be issued to this URL | |
|
443 | ||
|
444 | Multiple requests to "multirequest" URL are allowed | |
|
445 | ||
|
446 | $ send << EOF | |
|
447 | > httprequest POST api/$HTTPV2/ro/multirequest | |
|
448 | > accept: $MEDIATYPE | |
|
449 | > content-type: $MEDIATYPE | |
|
450 | > user-agent: test | |
|
451 | > frame 1 command-name eos customreadonly | |
|
452 | > frame 3 command-name eos customreadonly | |
|
453 | > EOF | |
|
454 | using raw connection to peer | |
|
455 | s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n | |
|
456 | s> Accept-Encoding: identity\r\n | |
|
457 | s> accept: application/mercurial-exp-framing-0002\r\n | |
|
458 | s> content-type: application/mercurial-exp-framing-0002\r\n | |
|
459 | s> user-agent: test\r\n | |
|
460 | s> *\r\n (glob) | |
|
461 | s> host: $LOCALIP:$HGPORT\r\n (glob) | |
|
462 | s> \r\n | |
|
463 | s> \x0e\x00\x00\x01\x00\x11customreadonly\x0e\x00\x00\x03\x00\x11customreadonly | |
|
464 | s> makefile('rb', None) | |
|
465 | s> HTTP/1.1 200 OK\r\n | |
|
466 | s> Server: testing stub value\r\n | |
|
467 | s> Date: $HTTP_DATE$\r\n | |
|
468 | s> Content-Type: application/mercurial-exp-framing-0002\r\n | |
|
469 | s> Transfer-Encoding: chunked\r\n | |
|
470 | s> \r\n | |
|
471 | s> *\r\n (glob) | |
|
472 | s> \x1d\x00\x00\x01\x00Bcustomreadonly bytes response | |
|
473 | s> \r\n | |
|
474 | s> 23\r\n | |
|
475 | s> \x1d\x00\x00\x03\x00Bcustomreadonly bytes response | |
|
476 | s> \r\n | |
|
477 | s> 0\r\n | |
|
478 | s> \r\n | |
|
479 | ||
|
480 | Interleaved requests to "multirequest" are processed | |
|
481 | ||
|
482 | $ send << EOF | |
|
483 | > httprequest POST api/$HTTPV2/ro/multirequest | |
|
484 | > accept: $MEDIATYPE | |
|
485 | > content-type: $MEDIATYPE | |
|
486 | > user-agent: test | |
|
487 | > frame 1 command-name have-args listkeys | |
|
488 | > frame 3 command-name have-args listkeys | |
|
489 | > frame 3 command-argument eoa \x09\x00\x09\x00namespacebookmarks | |
|
490 | > frame 1 command-argument eoa \x09\x00\x0a\x00namespacenamespaces | |
|
491 | > EOF | |
|
492 | using raw connection to peer | |
|
493 | s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n | |
|
494 | s> Accept-Encoding: identity\r\n | |
|
495 | s> accept: application/mercurial-exp-framing-0002\r\n | |
|
496 | s> content-type: application/mercurial-exp-framing-0002\r\n | |
|
497 | s> user-agent: test\r\n | |
|
498 | s> content-length: 85\r\n | |
|
499 | s> host: $LOCALIP:$HGPORT\r\n (glob) | |
|
500 | s> \r\n | |
|
501 | s> \x08\x00\x00\x01\x00\x12listkeys\x08\x00\x00\x03\x00\x12listkeys\x16\x00\x00\x03\x00" \x00 \x00namespacebookmarks\x17\x00\x00\x01\x00" \x00\n | |
|
502 | s> \x00namespacenamespaces | |
|
503 | s> makefile('rb', None) | |
|
504 | s> HTTP/1.1 200 OK\r\n | |
|
505 | s> Server: testing stub value\r\n | |
|
506 | s> Date: $HTTP_DATE$\r\n | |
|
507 | s> Content-Type: application/mercurial-exp-framing-0002\r\n | |
|
508 | s> Transfer-Encoding: chunked\r\n | |
|
509 | s> \r\n | |
|
510 | s> 6\r\n | |
|
511 | s> \x00\x00\x00\x03\x00B | |
|
512 | s> \r\n | |
|
513 | s> 24\r\n | |
|
514 | s> \x1e\x00\x00\x01\x00Bbookmarks \n | |
|
515 | s> namespaces \n | |
|
516 | s> phases | |
|
517 | s> \r\n | |
|
518 | s> 0\r\n | |
|
519 | s> \r\n | |
|
520 | ||
|
521 | Restart server to disable read-write access | |
|
522 | ||
|
523 | $ killdaemons.py | |
|
524 | $ cat > server/.hg/hgrc << EOF | |
|
525 | > [experimental] | |
|
526 | > web.apiserver = true | |
|
527 | > web.api.debugreflect = true | |
|
528 | > web.api.http-v2 = true | |
|
529 | > [web] | |
|
530 | > push_ssl = false | |
|
531 | > EOF | |
|
532 | ||
|
533 | $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log | |
|
534 | $ cat hg.pid > $DAEMON_PIDS | |
|
535 | ||
|
536 | Attempting to run a read-write command via multirequest on read-only URL is not allowed | |
|
537 | ||
|
538 | $ send << EOF | |
|
539 | > httprequest POST api/$HTTPV2/ro/multirequest | |
|
540 | > accept: $MEDIATYPE | |
|
541 | > content-type: $MEDIATYPE | |
|
542 | > user-agent: test | |
|
543 | > frame 1 command-name eos unbundle | |
|
544 | > EOF | |
|
545 | using raw connection to peer | |
|
546 | s> POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n | |
|
547 | s> Accept-Encoding: identity\r\n | |
|
548 | s> accept: application/mercurial-exp-framing-0002\r\n | |
|
549 | s> content-type: application/mercurial-exp-framing-0002\r\n | |
|
550 | s> user-agent: test\r\n | |
|
551 | s> content-length: 14\r\n | |
|
552 | s> host: $LOCALIP:$HGPORT\r\n (glob) | |
|
553 | s> \r\n | |
|
554 | s> \x08\x00\x00\x01\x00\x11unbundle | |
|
555 | s> makefile('rb', None) | |
|
556 | s> HTTP/1.1 403 Forbidden\r\n | |
|
557 | s> Server: testing stub value\r\n | |
|
558 | s> Date: $HTTP_DATE$\r\n | |
|
559 | s> Content-Type: text/plain\r\n | |
|
560 | s> Content-Length: 53\r\n | |
|
561 | s> \r\n | |
|
562 | s> insufficient permissions to execute command: unbundle | |
|
563 | ||
|
415 | 564 | $ cat error.log |
General Comments 0
You need to be logged in to leave comments.
Login now