Mercurial > public > mercurial-scm > hg
comparison mercurial/httppeer.py @ 37483:61e405fb6372
wireproto: crude support for version 2 HTTP peer
As part of implementing the server-side bits of the wire protocol
command handlers for version 2, we want a way to easily test those
commands. Currently, we use the "httprequest" action of `hg
debugwireproto`. But this requires explicitly specifying the HTTP
request headers, low-level frame details, and the data structure
to encode with CBOR. That's a lot of boilerplate and a lot of it can
change as the wire protocol evolves.
`hg debugwireproto` has a mechanism to issue commands via the peer
interface. That is *much* easier to use and we prefer to test with
that going forward.
This commit implements enough parts of the peer API to send basic
requests via the HTTP version 2 transport.
The peer code is super hacky. Again, the goal is to facilitate
server testing, not robustly implement a client. The client code
will receive love at a later time.
Differential Revision: https://phab.mercurial-scm.org/D3177
author | Gregory Szorc <gregory.szorc@gmail.com> |
---|---|
date | Wed, 28 Mar 2018 15:09:34 -0700 |
parents | 3e1688711efd |
children | aacfca6f9767 |
comparison
equal
deleted
inserted
replaced
37482:fa9faf58959d | 37483:61e405fb6372 |
---|---|
14 import socket | 14 import socket |
15 import struct | 15 import struct |
16 import tempfile | 16 import tempfile |
17 | 17 |
18 from .i18n import _ | 18 from .i18n import _ |
19 from .thirdparty import ( | |
20 cbor, | |
21 ) | |
19 from . import ( | 22 from . import ( |
20 bundle2, | 23 bundle2, |
21 error, | 24 error, |
22 httpconnection, | 25 httpconnection, |
23 pycompat, | 26 pycompat, |
24 statichttprepo, | 27 statichttprepo, |
25 url as urlmod, | 28 url as urlmod, |
26 util, | 29 util, |
27 wireproto, | 30 wireproto, |
31 wireprotoframing, | |
32 wireprotoserver, | |
28 ) | 33 ) |
29 | 34 |
30 httplib = util.httplib | 35 httplib = util.httplib |
31 urlerr = util.urlerr | 36 urlerr = util.urlerr |
32 urlreq = util.urlreq | 37 urlreq = util.urlreq |
465 return self._callstream(cmd, _compressible=True, **args) | 470 return self._callstream(cmd, _compressible=True, **args) |
466 | 471 |
467 def _abort(self, exception): | 472 def _abort(self, exception): |
468 raise exception | 473 raise exception |
469 | 474 |
475 # TODO implement interface for version 2 peers | |
476 class httpv2peer(object): | |
477 def __init__(self, ui, repourl, opener): | |
478 self.ui = ui | |
479 | |
480 if repourl.endswith('/'): | |
481 repourl = repourl[:-1] | |
482 | |
483 self.url = repourl | |
484 self._opener = opener | |
485 # This is an its own attribute to facilitate extensions overriding | |
486 # the default type. | |
487 self._requestbuilder = urlreq.request | |
488 | |
489 def close(self): | |
490 pass | |
491 | |
492 # TODO require to be part of a batched primitive, use futures. | |
493 def _call(self, name, **args): | |
494 """Call a wire protocol command with arguments.""" | |
495 | |
496 # TODO permissions should come from capabilities results. | |
497 permission = wireproto.commandsv2[name].permission | |
498 if permission not in ('push', 'pull'): | |
499 raise error.ProgrammingError('unknown permission type: %s' % | |
500 permission) | |
501 | |
502 permission = { | |
503 'push': 'rw', | |
504 'pull': 'ro', | |
505 }[permission] | |
506 | |
507 url = '%s/api/%s/%s/%s' % (self.url, wireprotoserver.HTTPV2, permission, | |
508 name) | |
509 | |
510 # TODO modify user-agent to reflect v2. | |
511 headers = { | |
512 r'Accept': wireprotoserver.FRAMINGTYPE, | |
513 r'Content-Type': wireprotoserver.FRAMINGTYPE, | |
514 } | |
515 | |
516 # TODO this should be part of a generic peer for the frame-based | |
517 # protocol. | |
518 stream = wireprotoframing.stream(1) | |
519 frames = wireprotoframing.createcommandframes(stream, 1, | |
520 name, args) | |
521 | |
522 body = b''.join(map(bytes, frames)) | |
523 req = self._requestbuilder(pycompat.strurl(url), body, headers) | |
524 req.add_unredirected_header(r'Content-Length', r'%d' % len(body)) | |
525 | |
526 # TODO unify this code with httppeer. | |
527 try: | |
528 res = self._opener.open(req) | |
529 except urlerr.httperror as e: | |
530 if e.code == 401: | |
531 raise error.Abort(_('authorization failed')) | |
532 | |
533 raise | |
534 except httplib.HTTPException as e: | |
535 self.ui.traceback() | |
536 raise IOError(None, e) | |
537 | |
538 # TODO validate response type, wrap response to handle I/O errors. | |
539 # TODO more robust frame receiver. | |
540 results = [] | |
541 | |
542 while True: | |
543 frame = wireprotoframing.readframe(res) | |
544 if frame is None: | |
545 break | |
546 | |
547 self.ui.note(_('received %r\n') % frame) | |
548 | |
549 if frame.typeid == wireprotoframing.FRAME_TYPE_BYTES_RESPONSE: | |
550 if frame.flags & wireprotoframing.FLAG_BYTES_RESPONSE_CBOR: | |
551 payload = util.bytesio(frame.payload) | |
552 | |
553 decoder = cbor.CBORDecoder(payload) | |
554 while payload.tell() + 1 < len(frame.payload): | |
555 results.append(decoder.decode()) | |
556 else: | |
557 results.append(frame.payload) | |
558 else: | |
559 error.ProgrammingError('unhandled frame type: %d' % | |
560 frame.typeid) | |
561 | |
562 return results | |
563 | |
470 def makepeer(ui, path): | 564 def makepeer(ui, path): |
471 u = util.url(path) | 565 u = util.url(path) |
472 if u.query or u.fragment: | 566 if u.query or u.fragment: |
473 raise error.Abort(_('unsupported URL component: "%s"') % | 567 raise error.Abort(_('unsupported URL component: "%s"') % |
474 (u.query or u.fragment)) | 568 (u.query or u.fragment)) |