Show More
@@ -203,6 +203,28 b' an HTTP 406.' | |||||
203 | Servers receiving requests with an invalid ``Content-Type`` header SHOULD |
|
203 | Servers receiving requests with an invalid ``Content-Type`` header SHOULD | |
204 | respond with an HTTP 415. |
|
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 | SSH Protocol |
|
228 | SSH Protocol | |
207 | ============ |
|
229 | ============ | |
208 |
|
230 |
@@ -327,7 +327,12 b' def _handlehttpv2request(rctx, req, res,' | |||||
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) | |
@@ -338,7 +343,8 b' def _handlehttpv2request(rctx, req, res,' | |||||
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) | |
@@ -434,18 +440,14 b' def _processhttpv2request(ui, repo, req,' | |||||
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. | |
@@ -471,7 +473,7 b' def _processhttpv2request(ui, repo, req,' | |||||
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 | |
@@ -484,34 +486,57 b' def _httpv2runcommand(ui, repo, req, res' | |||||
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 |
# |
|
489 | # Our rule for this is we only allow one command per HTTP request and | |
488 |
# |
|
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) |
|
|||
503 | wirecommand = wireproto.commands[command['command']] |
|
|||
504 |
|
498 | |||
505 | assert authedperm in (b'ro', b'rw') |
|
499 | if reqcommand == b'multirequest': | |
506 | assert wirecommand.permission in ('push', 'pull') |
|
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 |
|
512 | if authedperm == b'ro' and wirecommand.permission != 'pull': | |
509 | # permissions are important, so do it again. |
|
513 | # TODO proper error mechanism | |
510 | if authedperm == b'ro': |
|
514 | res.status = b'403 Forbidden' | |
511 | assert wirecommand.permission == 'pull' |
|
515 | res.headers[b'Content-Type'] = b'text/plain' | |
512 | elif authedperm == b'rw': |
|
516 | res.setbodybytes(_('insufficient permissions to execute ' | |
513 | # We are allowed to access read-only commands under the rw URL. |
|
517 | 'command: %s') % command['command']) | |
514 | assert wirecommand.permission in ('push', 'pull') |
|
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 | |||
@@ -527,6 +552,7 b' def _httpv2runcommand(ui, repo, req, res' | |||||
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: |
@@ -412,4 +412,153 b' Command frames can be reflected via debu' | |||||
412 | s> received: <no frame>\n |
|
412 | s> received: <no frame>\n | |
413 | s> {"action": "noop"} |
|
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 | $ cat error.log |
|
564 | $ cat error.log |
General Comments 0
You need to be logged in to leave comments.
Login now