##// END OF EJS Templates
wireproto: define and implement HTTP handshake to upgrade protocol...
Gregory Szorc -
r37575:734515ac default
parent child Browse files
Show More
@@ -33,6 +33,9 b' from .node import ('
33 33 nullrev,
34 34 short,
35 35 )
36 from .thirdparty import (
37 cbor,
38 )
36 39 from . import (
37 40 bundle2,
38 41 changegroup,
@@ -3045,9 +3048,14 b' def debugwireproto(ui, repo, path=None, '
3045 3048 req.get_method = lambda: method
3046 3049
3047 3050 try:
3048 opener.open(req).read()
3051 res = opener.open(req)
3052 body = res.read()
3049 3053 except util.urlerr.urlerror as e:
3050 3054 e.read()
3055 continue
3056
3057 if res.headers.get('Content-Type') == 'application/mercurial-cbor':
3058 ui.write(_('cbor> %s\n') % stringutil.pprint(cbor.loads(body)))
3051 3059
3052 3060 elif action == 'close':
3053 3061 peer.close()
@@ -42,8 +42,44 b' Handshake'
42 42 The client sends a ``capabilities`` command request (``?cmd=capabilities``)
43 43 as soon as HTTP requests may be issued.
44 44
45 The server responds with a capabilities string, which the client parses to
46 learn about the server's abilities.
45 By default, the server responds with a version 1 capabilities string, which
46 the client parses to learn about the server's abilities. The ``Content-Type``
47 for this response is ``application/mercurial-0.1`` or
48 ``application/mercurial-0.2`` depending on whether the client advertised
49 support for version ``0.2`` in its request. (Clients aren't supposed to
50 advertise support for ``0.2`` until the capabilities response indicates
51 the server's support for that media type. However, a client could
52 conceivably cache this metadata and issue the capabilities request in such
53 a way to elicit an ``application/mercurial-0.2`` response.)
54
55 Clients wishing to switch to a newer API service may send an
56 ``X-HgUpgrade-<X>`` header containing a space-delimited list of API service
57 names the client is capable of speaking. The request MUST also include an
58 ``X-HgProto-<X>`` header advertising a known serialization format for the
59 response. ``cbor`` is currently the only defined serialization format.
60
61 If the request contains these headers, the response ``Content-Type`` MAY
62 be for a different media type. e.g. ``application/mercurial-cbor`` if the
63 client advertises support for CBOR.
64
65 The response MUST be deserializable to a map with the following keys:
66
67 apibase
68 URL path to API services, relative to the repository root. e.g. ``api/``.
69
70 apis
71 A map of API service names to API descriptors. An API descriptor contains
72 more details about that API. In the case of the HTTP Version 2 Transport,
73 it will be the normal response to a ``capabilities`` command.
74
75 Only the services advertised by the client that are also available on
76 the server are advertised.
77
78 v1capabilities
79 The capabilities string that would be returned by a version 1 response.
80
81 The client can then inspect the server-advertised APIs and decide which
82 API to use, including continuing to use the HTTP Version 1 Transport.
47 83
48 84 HTTP Version 1 Transport
49 85 ------------------------
@@ -123,6 +159,9 b' The ``application/hg-error`` media type '
123 159 The content of the HTTP response body typically holds text describing the
124 160 error.
125 161
162 The ``application/mercurial-cbor`` media type indicates a CBOR payload
163 and should be interpreted as identical to ``application/cbor``.
164
126 165 Behavior of media types is further described in the ``Content Negotiation``
127 166 section below.
128 167
@@ -1252,6 +1291,12 b' 0.2'
1252 1291 Indicates the client supports receiving ``application/mercurial-0.2``
1253 1292 responses.
1254 1293
1294 cbor
1295 Indicates the client supports receiving ``application/mercurial-cbor``
1296 responses.
1297
1298 (Only intended to be used with version 2 transports.)
1299
1255 1300 comp
1256 1301 Indicates compression formats the client can decode. Value is a list of
1257 1302 comma delimited strings identifying compression formats ordered from
@@ -12,6 +12,9 b' import sys'
12 12 import threading
13 13
14 14 from .i18n import _
15 from .thirdparty import (
16 cbor,
17 )
15 18 from .thirdparty.zope import (
16 19 interface as zi,
17 20 )
@@ -230,6 +233,18 b' def handlewsgirequest(rctx, req, res, ch'
230 233
231 234 return True
232 235
236 def _availableapis(repo):
237 apis = set()
238
239 # Registered APIs are made available via config options of the name of
240 # the protocol.
241 for k, v in API_HANDLERS.items():
242 section, option = v['config']
243 if repo.ui.configbool(section, option):
244 apis.add(k)
245
246 return apis
247
233 248 def handlewsgiapirequest(rctx, req, res, checkperm):
234 249 """Handle requests to /api/*."""
235 250 assert req.dispatchparts[0] == b'api'
@@ -247,13 +262,7 b' def handlewsgiapirequest(rctx, req, res,'
247 262 # The URL space is /api/<protocol>/*. The structure of URLs under varies
248 263 # by <protocol>.
249 264
250 # Registered APIs are made available via config options of the name of
251 # the protocol.
252 availableapis = set()
253 for k, v in API_HANDLERS.items():
254 section, option = v['config']
255 if repo.ui.configbool(section, option):
256 availableapis.add(k)
265 availableapis = _availableapis(repo)
257 266
258 267 # Requests to /api/ list available APIs.
259 268 if req.dispatchparts == [b'api']:
@@ -287,10 +296,21 b' def handlewsgiapirequest(rctx, req, res,'
287 296 req.dispatchparts[2:])
288 297
289 298 # Maps API name to metadata so custom API can be registered.
299 # Keys are:
300 #
301 # config
302 # Config option that controls whether service is enabled.
303 # handler
304 # Callable receiving (rctx, req, res, checkperm, urlparts) that is called
305 # when a request to this API is received.
306 # apidescriptor
307 # Callable receiving (req, repo) that is called to obtain an API
308 # descriptor for this service. The response must be serializable to CBOR.
290 309 API_HANDLERS = {
291 310 wireprotov2server.HTTPV2: {
292 311 'config': ('experimental', 'web.api.http-v2'),
293 312 'handler': wireprotov2server.handlehttpv2request,
313 'apidescriptor': wireprotov2server.httpv2apidescriptor,
294 314 },
295 315 }
296 316
@@ -327,6 +347,54 b' def _httpresponsetype(ui, proto, prefer_'
327 347 opts = {'level': ui.configint('server', 'zliblevel')}
328 348 return HGTYPE, util.compengines['zlib'], opts
329 349
350 def processcapabilitieshandshake(repo, req, res, proto):
351 """Called during a ?cmd=capabilities request.
352
353 If the client is advertising support for a newer protocol, we send
354 a CBOR response with information about available services. If no
355 advertised services are available, we don't handle the request.
356 """
357 # Fall back to old behavior unless the API server is enabled.
358 if not repo.ui.configbool('experimental', 'web.apiserver'):
359 return False
360
361 clientapis = decodevaluefromheaders(req, b'X-HgUpgrade')
362 protocaps = decodevaluefromheaders(req, b'X-HgProto')
363 if not clientapis or not protocaps:
364 return False
365
366 # We currently only support CBOR responses.
367 protocaps = set(protocaps.split(' '))
368 if b'cbor' not in protocaps:
369 return False
370
371 descriptors = {}
372
373 for api in sorted(set(clientapis.split()) & _availableapis(repo)):
374 handler = API_HANDLERS[api]
375
376 descriptorfn = handler.get('apidescriptor')
377 if not descriptorfn:
378 continue
379
380 descriptors[api] = descriptorfn(req, repo)
381
382 v1caps = wireproto.dispatch(repo, proto, 'capabilities')
383 assert isinstance(v1caps, wireprototypes.bytesresponse)
384
385 m = {
386 # TODO allow this to be configurable.
387 'apibase': 'api/',
388 'apis': descriptors,
389 'v1capabilities': v1caps.data,
390 }
391
392 res.status = b'200 OK'
393 res.headers[b'Content-Type'] = b'application/mercurial-cbor'
394 res.setbodybytes(cbor.dumps(m, canonical=True))
395
396 return True
397
330 398 def _callhttp(repo, req, res, proto, cmd):
331 399 # Avoid cycle involving hg module.
332 400 from .hgweb import common as hgwebcommon
@@ -363,6 +431,12 b' def _callhttp(repo, req, res, proto, cmd'
363 431
364 432 proto.checkperm(wireproto.commands[cmd].permission)
365 433
434 # Possibly handle a modern client wanting to switch protocols.
435 if (cmd == 'capabilities' and
436 processcapabilitieshandshake(repo, req, res, proto)):
437
438 return
439
366 440 rsp = wireproto.dispatch(repo, proto, cmd)
367 441
368 442 if isinstance(rsp, bytes):
@@ -365,6 +365,11 b' class httpv2protocolhandler(object):'
365 365 def checkperm(self, perm):
366 366 raise NotImplementedError
367 367
368 def httpv2apidescriptor(req, repo):
369 proto = httpv2protocolhandler(req, repo.ui)
370
371 return _capabilitiesv2(repo, proto)
372
368 373 def _capabilitiesv2(repo, proto):
369 374 """Obtain the set of capabilities for version 2 transports.
370 375
@@ -1,11 +1,201 b''
1 1 $ . $TESTDIR/wireprotohelpers.sh
2 2
3 3 $ hg init server
4 $ enablehttpv2 server
4 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
5 $ cat hg.pid > $DAEMON_PIDS
6
7 A normal capabilities request is serviced for version 1
8
9 $ sendhttpraw << EOF
10 > httprequest GET ?cmd=capabilities
11 > user-agent: test
12 > EOF
13 using raw connection to peer
14 s> GET /?cmd=capabilities HTTP/1.1\r\n
15 s> Accept-Encoding: identity\r\n
16 s> user-agent: test\r\n
17 s> host: $LOCALIP:$HGPORT\r\n (glob)
18 s> \r\n
19 s> makefile('rb', None)
20 s> HTTP/1.1 200 Script output follows\r\n
21 s> Server: testing stub value\r\n
22 s> Date: $HTTP_DATE$\r\n
23 s> Content-Type: application/mercurial-0.1\r\n
24 s> Content-Length: 458\r\n
25 s> \r\n
26 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
27
28 A proper request without the API server enabled returns the legacy response
29
30 $ sendhttpraw << EOF
31 > httprequest GET ?cmd=capabilities
32 > user-agent: test
33 > x-hgupgrade-1: foo
34 > x-hgproto-1: cbor
35 > EOF
36 using raw connection to peer
37 s> GET /?cmd=capabilities HTTP/1.1\r\n
38 s> Accept-Encoding: identity\r\n
39 s> user-agent: test\r\n
40 s> x-hgproto-1: cbor\r\n
41 s> x-hgupgrade-1: foo\r\n
42 s> host: $LOCALIP:$HGPORT\r\n (glob)
43 s> \r\n
44 s> makefile('rb', None)
45 s> HTTP/1.1 200 Script output follows\r\n
46 s> Server: testing stub value\r\n
47 s> Date: $HTTP_DATE$\r\n
48 s> Content-Type: application/mercurial-0.1\r\n
49 s> Content-Length: 458\r\n
50 s> \r\n
51 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
52
53 Restart with just API server enabled. This enables serving the new format.
54
55 $ killdaemons.py
56 $ cat error.log
57
58 $ cat >> server/.hg/hgrc << EOF
59 > [experimental]
60 > web.apiserver = true
61 > EOF
62
5 63 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
6 64 $ cat hg.pid > $DAEMON_PIDS
7 65
8 capabilities request returns an array of capability strings
66 X-HgUpgrade-<N> without CBOR advertisement uses legacy response
67
68 $ sendhttpraw << EOF
69 > httprequest GET ?cmd=capabilities
70 > user-agent: test
71 > x-hgupgrade-1: foo bar
72 > EOF
73 using raw connection to peer
74 s> GET /?cmd=capabilities HTTP/1.1\r\n
75 s> Accept-Encoding: identity\r\n
76 s> user-agent: test\r\n
77 s> x-hgupgrade-1: foo bar\r\n
78 s> host: $LOCALIP:$HGPORT\r\n (glob)
79 s> \r\n
80 s> makefile('rb', None)
81 s> HTTP/1.1 200 Script output follows\r\n
82 s> Server: testing stub value\r\n
83 s> Date: $HTTP_DATE$\r\n
84 s> Content-Type: application/mercurial-0.1\r\n
85 s> Content-Length: 458\r\n
86 s> \r\n
87 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
88
89 X-HgUpgrade-<N> without known serialization in X-HgProto-<N> uses legacy response
90
91 $ sendhttpraw << EOF
92 > httprequest GET ?cmd=capabilities
93 > user-agent: test
94 > x-hgupgrade-1: foo bar
95 > x-hgproto-1: some value
96 > EOF
97 using raw connection to peer
98 s> GET /?cmd=capabilities HTTP/1.1\r\n
99 s> Accept-Encoding: identity\r\n
100 s> user-agent: test\r\n
101 s> x-hgproto-1: some value\r\n
102 s> x-hgupgrade-1: foo bar\r\n
103 s> host: $LOCALIP:$HGPORT\r\n (glob)
104 s> \r\n
105 s> makefile('rb', None)
106 s> HTTP/1.1 200 Script output follows\r\n
107 s> Server: testing stub value\r\n
108 s> Date: $HTTP_DATE$\r\n
109 s> Content-Type: application/mercurial-0.1\r\n
110 s> Content-Length: 458\r\n
111 s> \r\n
112 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
113
114 X-HgUpgrade-<N> + X-HgProto-<N> headers trigger new response format
115
116 $ sendhttpraw << EOF
117 > httprequest GET ?cmd=capabilities
118 > user-agent: test
119 > x-hgupgrade-1: foo bar
120 > x-hgproto-1: cbor
121 > EOF
122 using raw connection to peer
123 s> GET /?cmd=capabilities HTTP/1.1\r\n
124 s> Accept-Encoding: identity\r\n
125 s> user-agent: test\r\n
126 s> x-hgproto-1: cbor\r\n
127 s> x-hgupgrade-1: foo bar\r\n
128 s> host: $LOCALIP:$HGPORT\r\n (glob)
129 s> \r\n
130 s> makefile('rb', None)
131 s> HTTP/1.1 200 OK\r\n
132 s> Server: testing stub value\r\n
133 s> Date: $HTTP_DATE$\r\n
134 s> Content-Type: application/mercurial-cbor\r\n
135 s> Content-Length: 496\r\n
136 s> \r\n
137 s> \xa3Dapis\xa0GapibaseDapi/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
138 cbor> {b'apibase': b'api/', b'apis': {}, b'v1capabilities': b'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'}
139
140 Restart server to enable HTTPv2
141
142 $ killdaemons.py
143 $ enablehttpv2 server
144 $ hg -R server serve -p $HGPORT -d --pid-file hg.pid -E error.log
145
146 Only requested API services are returned
147
148 $ sendhttpraw << EOF
149 > httprequest GET ?cmd=capabilities
150 > user-agent: test
151 > x-hgupgrade-1: foo bar
152 > x-hgproto-1: cbor
153 > EOF
154 using raw connection to peer
155 s> GET /?cmd=capabilities HTTP/1.1\r\n
156 s> Accept-Encoding: identity\r\n
157 s> user-agent: test\r\n
158 s> x-hgproto-1: cbor\r\n
159 s> x-hgupgrade-1: foo bar\r\n
160 s> host: $LOCALIP:$HGPORT\r\n (glob)
161 s> \r\n
162 s> makefile('rb', None)
163 s> HTTP/1.1 200 OK\r\n
164 s> Server: testing stub value\r\n
165 s> Date: $HTTP_DATE$\r\n
166 s> Content-Type: application/mercurial-cbor\r\n
167 s> Content-Length: 496\r\n
168 s> \r\n
169 s> \xa3Dapis\xa0GapibaseDapi/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
170 cbor> {b'apibase': b'api/', b'apis': {}, b'v1capabilities': b'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'}
171
172 Request for HTTPv2 service returns information about it
173
174 $ sendhttpraw << EOF
175 > httprequest GET ?cmd=capabilities
176 > user-agent: test
177 > x-hgupgrade-1: exp-http-v2-0001 foo bar
178 > x-hgproto-1: cbor
179 > EOF
180 using raw connection to peer
181 s> GET /?cmd=capabilities HTTP/1.1\r\n
182 s> Accept-Encoding: identity\r\n
183 s> user-agent: test\r\n
184 s> x-hgproto-1: cbor\r\n
185 s> x-hgupgrade-1: exp-http-v2-0001 foo bar\r\n
186 s> host: $LOCALIP:$HGPORT\r\n (glob)
187 s> \r\n
188 s> makefile('rb', None)
189 s> HTTP/1.1 200 OK\r\n
190 s> Server: testing stub value\r\n
191 s> Date: $HTTP_DATE$\r\n
192 s> Content-Type: application/mercurial-cbor\r\n
193 s> Content-Length: 879\r\n
194 s> \r\n
195 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
196 cbor> {b'apibase': b'api/', b'apis': {b'exp-http-v2-0001': {b'commands': {b'branchmap': {b'args': {}, b'permissions': [b'pull']}, b'capabilities': {b'args': {}, b'permissions': [b'pull']}, b'heads': {b'args': {b'publiconly': False}, b'permissions': [b'pull']}, b'known': {b'args': {b'nodes': [b'deadbeef']}, b'permissions': [b'pull']}, b'listkeys': {b'args': {b'namespace': b'ns'}, b'permissions': [b'pull']}, b'lookup': {b'args': {b'key': b'foo'}, b'permissions': [b'pull']}, b'pushkey': {b'args': {b'key': b'key', b'namespace': b'ns', b'new': b'new', b'old': b'old'}, b'permissions': [b'push']}}, b'compression': [{b'name': b'zstd'}, {b'name': b'zlib'}]}}, b'v1capabilities': b'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'}
197
198 capabilities command returns expected info
9 199
10 200 $ sendhttpv2peer << EOF
11 201 > command capabilities
General Comments 0
You need to be logged in to leave comments. Login now