Show More
@@ -538,6 +538,9 b" coreconfigitem('experimental', 'graphsty" | |||||
538 | coreconfigitem('experimental', 'hook-track-tags', |
|
538 | coreconfigitem('experimental', 'hook-track-tags', | |
539 | default=False, |
|
539 | default=False, | |
540 | ) |
|
540 | ) | |
|
541 | coreconfigitem('experimental', 'httppeer.advertise-v2', | |||
|
542 | default=False, | |||
|
543 | ) | |||
541 | coreconfigitem('experimental', 'httppostargs', |
|
544 | coreconfigitem('experimental', 'httppostargs', | |
542 | default=False, |
|
545 | default=False, | |
543 | ) |
|
546 | ) |
@@ -83,6 +83,7 b' from . import (' | |||||
83 | vfs as vfsmod, |
|
83 | vfs as vfsmod, | |
84 | wireprotoframing, |
|
84 | wireprotoframing, | |
85 | wireprotoserver, |
|
85 | wireprotoserver, | |
|
86 | wireprototypes, | |||
86 | ) |
|
87 | ) | |
87 | from .utils import ( |
|
88 | from .utils import ( | |
88 | dateutil, |
|
89 | dateutil, | |
@@ -2910,7 +2911,9 b' def debugwireproto(ui, repo, path=None, ' | |||||
2910 |
|
2911 | |||
2911 | if opts['peer'] == 'http2': |
|
2912 | if opts['peer'] == 'http2': | |
2912 | ui.write(_('creating http peer for wire protocol version 2\n')) |
|
2913 | ui.write(_('creating http peer for wire protocol version 2\n')) | |
2913 |
peer = httppeer.httpv2peer( |
|
2914 | peer = httppeer.httpv2peer( | |
|
2915 | ui, path, 'api/%s' % wireprototypes.HTTPV2, | |||
|
2916 | opener, httppeer.urlreq.request, {}) | |||
2914 | elif opts['peer'] == 'raw': |
|
2917 | elif opts['peer'] == 'raw': | |
2915 | ui.write(_('using raw connection to peer\n')) |
|
2918 | ui.write(_('using raw connection to peer\n')) | |
2916 | peer = None |
|
2919 | peer = None |
@@ -29,6 +29,7 b' from . import (' | |||||
29 | util, |
|
29 | util, | |
30 | wireproto, |
|
30 | wireproto, | |
31 | wireprotoframing, |
|
31 | wireprotoframing, | |
|
32 | wireprototypes, | |||
32 | wireprotov2server, |
|
33 | wireprotov2server, | |
33 | ) |
|
34 | ) | |
34 |
|
35 | |||
@@ -311,7 +312,8 b' def sendrequest(ui, opener, req):' | |||||
311 |
|
312 | |||
312 | return res |
|
313 | return res | |
313 |
|
314 | |||
314 |
def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible |
|
315 | def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible, | |
|
316 | allowcbor=False): | |||
315 | # record the url we got redirected to |
|
317 | # record the url we got redirected to | |
316 | respurl = pycompat.bytesurl(resp.geturl()) |
|
318 | respurl = pycompat.bytesurl(resp.geturl()) | |
317 | if respurl.endswith(qs): |
|
319 | if respurl.endswith(qs): | |
@@ -339,8 +341,19 b' def parsev1commandresponse(ui, baseurl, ' | |||||
339 | % (safeurl, proto or 'no content-type', resp.read(1024))) |
|
341 | % (safeurl, proto or 'no content-type', resp.read(1024))) | |
340 |
|
342 | |||
341 | try: |
|
343 | try: | |
342 |
|
|
344 | subtype = proto.split('-', 1)[1] | |
343 | version_info = tuple([int(n) for n in version.split('.')]) |
|
345 | ||
|
346 | # Unless we end up supporting CBOR in the legacy wire protocol, | |||
|
347 | # this should ONLY be encountered for the initial capabilities | |||
|
348 | # request during handshake. | |||
|
349 | if subtype == 'cbor': | |||
|
350 | if allowcbor: | |||
|
351 | return respurl, proto, resp | |||
|
352 | else: | |||
|
353 | raise error.RepoError(_('unexpected CBOR response from ' | |||
|
354 | 'server')) | |||
|
355 | ||||
|
356 | version_info = tuple([int(n) for n in subtype.split('.')]) | |||
344 | except ValueError: |
|
357 | except ValueError: | |
345 | raise error.RepoError(_("'%s' sent a broken Content-Type " |
|
358 | raise error.RepoError(_("'%s' sent a broken Content-Type " | |
346 | "header (%s)") % (safeurl, proto)) |
|
359 | "header (%s)") % (safeurl, proto)) | |
@@ -361,9 +374,9 b' def parsev1commandresponse(ui, baseurl, ' | |||||
361 | resp = engine.decompressorreader(resp) |
|
374 | resp = engine.decompressorreader(resp) | |
362 | else: |
|
375 | else: | |
363 | raise error.RepoError(_("'%s' uses newer protocol %s") % |
|
376 | raise error.RepoError(_("'%s' uses newer protocol %s") % | |
364 |
(safeurl, |
|
377 | (safeurl, subtype)) | |
365 |
|
378 | |||
366 | return respurl, resp |
|
379 | return respurl, proto, resp | |
367 |
|
380 | |||
368 | class httppeer(wireproto.wirepeer): |
|
381 | class httppeer(wireproto.wirepeer): | |
369 | def __init__(self, ui, path, url, opener, requestbuilder, caps): |
|
382 | def __init__(self, ui, path, url, opener, requestbuilder, caps): | |
@@ -416,8 +429,8 b' class httppeer(wireproto.wirepeer):' | |||||
416 |
|
429 | |||
417 | resp = sendrequest(self.ui, self._urlopener, req) |
|
430 | resp = sendrequest(self.ui, self._urlopener, req) | |
418 |
|
431 | |||
419 | self._url, resp = parsev1commandresponse(self.ui, self._url, cu, qs, |
|
432 | self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs, | |
420 | resp, _compressible) |
|
433 | resp, _compressible) | |
421 |
|
434 | |||
422 | return resp |
|
435 | return resp | |
423 |
|
436 | |||
@@ -501,17 +514,18 b' class httppeer(wireproto.wirepeer):' | |||||
501 |
|
514 | |||
502 | # TODO implement interface for version 2 peers |
|
515 | # TODO implement interface for version 2 peers | |
503 | class httpv2peer(object): |
|
516 | class httpv2peer(object): | |
504 |
def __init__(self, ui, repourl, opener |
|
517 | def __init__(self, ui, repourl, apipath, opener, requestbuilder, | |
|
518 | apidescriptor): | |||
505 | self.ui = ui |
|
519 | self.ui = ui | |
506 |
|
520 | |||
507 | if repourl.endswith('/'): |
|
521 | if repourl.endswith('/'): | |
508 | repourl = repourl[:-1] |
|
522 | repourl = repourl[:-1] | |
509 |
|
523 | |||
510 | self.url = repourl |
|
524 | self.url = repourl | |
|
525 | self._apipath = apipath | |||
511 | self._opener = opener |
|
526 | self._opener = opener | |
512 | # This is an its own attribute to facilitate extensions overriding |
|
527 | self._requestbuilder = requestbuilder | |
513 | # the default type. |
|
528 | self._descriptor = apidescriptor | |
514 | self._requestbuilder = urlreq.request |
|
|||
515 |
|
529 | |||
516 | def close(self): |
|
530 | def close(self): | |
517 | pass |
|
531 | pass | |
@@ -540,8 +554,7 b' class httpv2peer(object):' | |||||
540 | 'pull': 'ro', |
|
554 | 'pull': 'ro', | |
541 | }[permission] |
|
555 | }[permission] | |
542 |
|
556 | |||
543 |
url = '%s/ |
|
557 | url = '%s/%s/%s/%s' % (self.url, self._apipath, permission, name) | |
544 | permission, name) |
|
|||
545 |
|
558 | |||
546 | # TODO this should be part of a generic peer for the frame-based |
|
559 | # TODO this should be part of a generic peer for the frame-based | |
547 | # protocol. |
|
560 | # protocol. | |
@@ -597,6 +610,24 b' class httpv2peer(object):' | |||||
597 |
|
610 | |||
598 | return results |
|
611 | return results | |
599 |
|
612 | |||
|
613 | # Registry of API service names to metadata about peers that handle it. | |||
|
614 | # | |||
|
615 | # The following keys are meaningful: | |||
|
616 | # | |||
|
617 | # init | |||
|
618 | # Callable receiving (ui, repourl, servicepath, opener, requestbuilder, | |||
|
619 | # apidescriptor) to create a peer. | |||
|
620 | # | |||
|
621 | # priority | |||
|
622 | # Integer priority for the service. If we could choose from multiple | |||
|
623 | # services, we choose the one with the highest priority. | |||
|
624 | API_PEERS = { | |||
|
625 | wireprototypes.HTTPV2: { | |||
|
626 | 'init': httpv2peer, | |||
|
627 | 'priority': 50, | |||
|
628 | }, | |||
|
629 | } | |||
|
630 | ||||
600 | def performhandshake(ui, url, opener, requestbuilder): |
|
631 | def performhandshake(ui, url, opener, requestbuilder): | |
601 | # The handshake is a request to the capabilities command. |
|
632 | # The handshake is a request to the capabilities command. | |
602 |
|
633 | |||
@@ -604,21 +635,69 b' def performhandshake(ui, url, opener, re' | |||||
604 | def capable(x): |
|
635 | def capable(x): | |
605 | raise error.ProgrammingError('should not be called') |
|
636 | raise error.ProgrammingError('should not be called') | |
606 |
|
637 | |||
|
638 | args = {} | |||
|
639 | ||||
|
640 | # The client advertises support for newer protocols by adding an | |||
|
641 | # X-HgUpgrade-* header with a list of supported APIs and an | |||
|
642 | # X-HgProto-* header advertising which serializing formats it supports. | |||
|
643 | # We only support the HTTP version 2 transport and CBOR responses for | |||
|
644 | # now. | |||
|
645 | advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2') | |||
|
646 | ||||
|
647 | if advertisev2: | |||
|
648 | args['headers'] = { | |||
|
649 | r'X-HgProto-1': r'cbor', | |||
|
650 | } | |||
|
651 | ||||
|
652 | args['headers'].update( | |||
|
653 | encodevalueinheaders(' '.join(sorted(API_PEERS)), | |||
|
654 | 'X-HgUpgrade', | |||
|
655 | # We don't know the header limit this early. | |||
|
656 | # So make it small. | |||
|
657 | 1024)) | |||
|
658 | ||||
607 | req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps, |
|
659 | req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps, | |
608 | capable, url, 'capabilities', |
|
660 | capable, url, 'capabilities', | |
609 |
|
|
661 | args) | |
610 |
|
662 | |||
611 | resp = sendrequest(ui, opener, req) |
|
663 | resp = sendrequest(ui, opener, req) | |
612 |
|
664 | |||
613 | respurl, resp = parsev1commandresponse(ui, url, requrl, qs, resp, |
|
665 | respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp, | |
614 |
compressible=False |
|
666 | compressible=False, | |
|
667 | allowcbor=advertisev2) | |||
615 |
|
668 | |||
616 | try: |
|
669 | try: | |
617 |
raw |
|
670 | rawdata = resp.read() | |
618 | finally: |
|
671 | finally: | |
619 | resp.close() |
|
672 | resp.close() | |
620 |
|
673 | |||
621 | return respurl, set(rawcaps.split()) |
|
674 | if not ct.startswith('application/mercurial-'): | |
|
675 | raise error.ProgrammingError('unexpected content-type: %s' % ct) | |||
|
676 | ||||
|
677 | if advertisev2: | |||
|
678 | if ct == 'application/mercurial-cbor': | |||
|
679 | try: | |||
|
680 | info = cbor.loads(rawdata) | |||
|
681 | except cbor.CBORDecodeError: | |||
|
682 | raise error.Abort(_('error decoding CBOR from remote server'), | |||
|
683 | hint=_('try again and consider contacting ' | |||
|
684 | 'the server operator')) | |||
|
685 | ||||
|
686 | # We got a legacy response. That's fine. | |||
|
687 | elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'): | |||
|
688 | info = { | |||
|
689 | 'v1capabilities': set(rawdata.split()) | |||
|
690 | } | |||
|
691 | ||||
|
692 | else: | |||
|
693 | raise error.RepoError( | |||
|
694 | _('unexpected response type from server: %s') % ct) | |||
|
695 | else: | |||
|
696 | info = { | |||
|
697 | 'v1capabilities': set(rawdata.split()) | |||
|
698 | } | |||
|
699 | ||||
|
700 | return respurl, info | |||
622 |
|
701 | |||
623 | def makepeer(ui, path, opener=None, requestbuilder=urlreq.request): |
|
702 | def makepeer(ui, path, opener=None, requestbuilder=urlreq.request): | |
624 | """Construct an appropriate HTTP peer instance. |
|
703 | """Construct an appropriate HTTP peer instance. | |
@@ -640,9 +719,33 b' def makepeer(ui, path, opener=None, requ' | |||||
640 |
|
719 | |||
641 | opener = opener or urlmod.opener(ui, authinfo) |
|
720 | opener = opener or urlmod.opener(ui, authinfo) | |
642 |
|
721 | |||
643 |
respurl, |
|
722 | respurl, info = performhandshake(ui, url, opener, requestbuilder) | |
|
723 | ||||
|
724 | # Given the intersection of APIs that both we and the server support, | |||
|
725 | # sort by their advertised priority and pick the first one. | |||
|
726 | # | |||
|
727 | # TODO consider making this request-based and interface driven. For | |||
|
728 | # example, the caller could say "I want a peer that does X." It's quite | |||
|
729 | # possible that not all peers would do that. Since we know the service | |||
|
730 | # capabilities, we could filter out services not meeting the | |||
|
731 | # requirements. Possibly by consulting the interfaces defined by the | |||
|
732 | # peer type. | |||
|
733 | apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys()) | |||
644 |
|
734 | |||
645 | return httppeer(ui, path, respurl, opener, requestbuilder, caps) |
|
735 | preferredchoices = sorted(apipeerchoices, | |
|
736 | key=lambda x: API_PEERS[x]['priority'], | |||
|
737 | reverse=True) | |||
|
738 | ||||
|
739 | for service in preferredchoices: | |||
|
740 | apipath = '%s/%s' % (info['apibase'].rstrip('/'), service) | |||
|
741 | ||||
|
742 | return API_PEERS[service]['init'](ui, respurl, apipath, opener, | |||
|
743 | requestbuilder, | |||
|
744 | info['apis'][service]) | |||
|
745 | ||||
|
746 | # Failed to construct an API peer. Fall back to legacy. | |||
|
747 | return httppeer(ui, path, respurl, opener, requestbuilder, | |||
|
748 | info['v1capabilities']) | |||
646 |
|
749 | |||
647 | def instance(ui, path, create): |
|
750 | def instance(ui, path, create): | |
648 | if create: |
|
751 | if create: |
@@ -1,3 +1,5 b'' | |||||
|
1 | $ . $TESTDIR/wireprotohelpers.sh | |||
|
2 | ||||
1 | $ cat >> $HGRCPATH << EOF |
|
3 | $ cat >> $HGRCPATH << EOF | |
2 | > [web] |
|
4 | > [web] | |
3 | > push_ssl = false |
|
5 | > push_ssl = false | |
@@ -236,4 +238,98 b' Same thing, but with "httprequest" comma' | |||||
236 | s> namespaces\t\n |
|
238 | s> namespaces\t\n | |
237 | s> phases\t |
|
239 | s> phases\t | |
238 |
|
240 | |||
|
241 | Client with HTTPv2 enabled advertises that and gets old capabilities response from old server | |||
|
242 | ||||
|
243 | $ hg --config experimental.httppeer.advertise-v2=true --verbose debugwireproto http://$LOCALIP:$HGPORT << EOF | |||
|
244 | > command heads | |||
|
245 | > EOF | |||
|
246 | s> GET /?cmd=capabilities HTTP/1.1\r\n | |||
|
247 | s> Accept-Encoding: identity\r\n | |||
|
248 | s> vary: X-HgProto-1,X-HgUpgrade-1\r\n | |||
|
249 | s> x-hgproto-1: cbor\r\n | |||
|
250 | s> x-hgupgrade-1: exp-http-v2-0001\r\n | |||
|
251 | s> accept: application/mercurial-0.1\r\n | |||
|
252 | s> host: $LOCALIP:$HGPORT\r\n (glob) | |||
|
253 | s> user-agent: Mercurial debugwireproto\r\n | |||
|
254 | s> \r\n | |||
|
255 | s> makefile('rb', None) | |||
|
256 | s> HTTP/1.1 200 Script output follows\r\n | |||
|
257 | s> Server: testing stub value\r\n | |||
|
258 | s> Date: $HTTP_DATE$\r\n | |||
|
259 | s> Content-Type: application/mercurial-0.1\r\n | |||
|
260 | s> Content-Length: 458\r\n | |||
|
261 | s> \r\n | |||
|
262 | s> batch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ changegroupsubset compression=$BUNDLE2_COMPRESSIONS$ getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash | |||
|
263 | sending heads command | |||
|
264 | s> GET /?cmd=heads HTTP/1.1\r\n | |||
|
265 | s> Accept-Encoding: identity\r\n | |||
|
266 | s> vary: X-HgProto-1\r\n | |||
|
267 | s> x-hgproto-1: 0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull\r\n | |||
|
268 | s> accept: application/mercurial-0.1\r\n | |||
|
269 | s> host: $LOCALIP:$HGPORT\r\n (glob) | |||
|
270 | s> user-agent: Mercurial debugwireproto\r\n | |||
|
271 | s> \r\n | |||
|
272 | s> makefile('rb', None) | |||
|
273 | s> HTTP/1.1 200 Script output follows\r\n | |||
|
274 | s> Server: testing stub value\r\n | |||
|
275 | s> Date: $HTTP_DATE$\r\n | |||
|
276 | s> Content-Type: application/mercurial-0.1\r\n | |||
|
277 | s> Content-Length: 41\r\n | |||
|
278 | s> \r\n | |||
|
279 | s> 0000000000000000000000000000000000000000\n | |||
|
280 | response: b'0000000000000000000000000000000000000000\n' | |||
|
281 | ||||
239 | $ killdaemons.py |
|
282 | $ killdaemons.py | |
|
283 | $ enablehttpv2 empty | |||
|
284 | $ hg -R empty serve -p $HGPORT -d --pid-file hg.pid | |||
|
285 | $ cat hg.pid > $DAEMON_PIDS | |||
|
286 | ||||
|
287 | Client with HTTPv2 enabled automatically upgrades if the server supports it | |||
|
288 | ||||
|
289 | $ hg --config experimental.httppeer.advertise-v2=true --verbose debugwireproto http://$LOCALIP:$HGPORT << EOF | |||
|
290 | > command heads | |||
|
291 | > EOF | |||
|
292 | s> GET /?cmd=capabilities HTTP/1.1\r\n | |||
|
293 | s> Accept-Encoding: identity\r\n | |||
|
294 | s> vary: X-HgProto-1,X-HgUpgrade-1\r\n | |||
|
295 | s> x-hgproto-1: cbor\r\n | |||
|
296 | s> x-hgupgrade-1: exp-http-v2-0001\r\n | |||
|
297 | s> accept: application/mercurial-0.1\r\n | |||
|
298 | s> host: $LOCALIP:$HGPORT\r\n (glob) | |||
|
299 | s> user-agent: Mercurial debugwireproto\r\n | |||
|
300 | s> \r\n | |||
|
301 | s> makefile('rb', None) | |||
|
302 | s> HTTP/1.1 200 OK\r\n | |||
|
303 | s> Server: testing stub value\r\n | |||
|
304 | s> Date: $HTTP_DATE$\r\n | |||
|
305 | s> Content-Type: application/mercurial-cbor\r\n | |||
|
306 | s> Content-Length: 879\r\n | |||
|
307 | s> \r\n | |||
|
308 | s> \xa3Dapis\xa1Pexp-http-v2-0001\xa2Hcommands\xa7Eheads\xa2Dargs\xa1Jpubliconly\xf4Kpermissions\x81DpullEknown\xa2Dargs\xa1Enodes\x81HdeadbeefKpermissions\x81DpullFlookup\xa2Dargs\xa1CkeyCfooKpermissions\x81DpullGpushkey\xa2Dargs\xa4CkeyCkeyCnewCnewColdColdInamespaceBnsKpermissions\x81DpushHlistkeys\xa2Dargs\xa1InamespaceBnsKpermissions\x81DpullIbranchmap\xa2Dargs\xa0Kpermissions\x81DpullLcapabilities\xa2Dargs\xa0Kpermissions\x81DpullKcompression\x82\xa1DnameDzstd\xa1DnameDzlibGapibaseDapi/Nv1capabilitiesY\x01\xcabatch branchmap $USUAL_BUNDLE2_CAPS_SERVER$ changegroupsubset compression=$BUNDLE2_COMPRESSIONS$ getbundle httpheader=1024 httpmediatype=0.1rx,0.1tx,0.2tx known lookup pushkey streamreqs=generaldelta,revlogv1 unbundle=HG10GZ,HG10BZ,HG10UN unbundlehash | |||
|
309 | sending heads command | |||
|
310 | s> POST /api/exp-http-v2-0001/ro/heads HTTP/1.1\r\n | |||
|
311 | s> Accept-Encoding: identity\r\n | |||
|
312 | s> accept: application/mercurial-exp-framing-0003\r\n | |||
|
313 | s> content-type: application/mercurial-exp-framing-0003\r\n | |||
|
314 | s> content-length: 20\r\n | |||
|
315 | s> host: $LOCALIP:$HGPORT\r\n (glob) | |||
|
316 | s> user-agent: Mercurial debugwireproto\r\n | |||
|
317 | s> \r\n | |||
|
318 | s> \x0c\x00\x00\x01\x00\x01\x01\x11\xa1DnameEheads | |||
|
319 | s> makefile('rb', None) | |||
|
320 | s> HTTP/1.1 200 OK\r\n | |||
|
321 | s> Server: testing stub value\r\n | |||
|
322 | s> Date: $HTTP_DATE$\r\n | |||
|
323 | s> Content-Type: application/mercurial-exp-framing-0003\r\n | |||
|
324 | s> Transfer-Encoding: chunked\r\n | |||
|
325 | s> \r\n | |||
|
326 | s> 1e\r\n | |||
|
327 | s> \x16\x00\x00\x01\x00\x02\x01F | |||
|
328 | s> \x81T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 | |||
|
329 | s> \r\n | |||
|
330 | received frame(size=22; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos|cbor) | |||
|
331 | s> 0\r\n | |||
|
332 | s> \r\n | |||
|
333 | response: [[b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']] | |||
|
334 | ||||
|
335 | $ killdaemons.py |
General Comments 0
You need to be logged in to leave comments.
Login now