##// 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 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 # 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)
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