##// END OF EJS Templates
wireproto: service multiple command requests per HTTP request...
Gregory Szorc -
r37077:bbea9916 default
parent child Browse files
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 # For now, this is no big deal, as we only allow a single command per
488 # request and that command must match the command in the URL. But when
489 # things change, we need to watch out...
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.
496
497 proto = httpv2protocolhandler(req, ui, args=command['args'])
498
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')
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
490 534 if reqcommand != command['command']:
491 535 # TODO define proper error mechanism
492 536 res.status = b'200 OK'
493 537 res.headers[b'Content-Type'] = b'text/plain'
494 538 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
501 proto = httpv2protocolhandler(req, ui, args=command['args'])
502 assert wireproto.commands.commandavailable(command['command'], proto)
503 wirecommand = wireproto.commands[command['command']]
504
505 assert authedperm in (b'ro', b'rw')
506 assert wirecommand.permission in ('push', 'pull')
507
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')
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