##// END OF EJS Templates
httppeer: support protocol upgrade...
Gregory Szorc -
r37576:8a731322 default
parent child Browse files
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(ui, path, opener)
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 version = proto.split('-', 1)[1]
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, version))
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/api/%s/%s/%s' % (self.url, wireprotov2server.HTTPV2,
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 rawcaps = resp.read()
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, caps = performhandshake(ui, url, opener, requestbuilder)
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