##// END OF EJS Templates
wireprotov2: client support for following content redirects...
Gregory Szorc -
r40062:7e807b8a default
parent child Browse files
Show More
@@ -513,7 +513,9 def sendv2request(ui, opener, requestbui
513 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
513 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
514 buffersends=True)
514 buffersends=True)
515
515
516 handler = wireprotov2peer.clienthandler(ui, reactor)
516 handler = wireprotov2peer.clienthandler(ui, reactor,
517 opener=opener,
518 requestbuilder=requestbuilder)
517
519
518 url = '%s/%s' % (apiurl, permission)
520 url = '%s/%s' % (apiurl, permission)
519
521
@@ -13,8 +13,12 from .i18n import _
13 from . import (
13 from . import (
14 encoding,
14 encoding,
15 error,
15 error,
16 pycompat,
16 sslutil,
17 sslutil,
18 url as urlmod,
19 util,
17 wireprotoframing,
20 wireprotoframing,
21 wireprototypes,
18 )
22 )
19 from .utils import (
23 from .utils import (
20 cborutil,
24 cborutil,
@@ -112,9 +116,10 class commandresponse(object):
112 events occur.
116 events occur.
113 """
117 """
114
118
115 def __init__(self, requestid, command):
119 def __init__(self, requestid, command, fromredirect=False):
116 self.requestid = requestid
120 self.requestid = requestid
117 self.command = command
121 self.command = command
122 self.fromredirect = fromredirect
118
123
119 # Whether all remote input related to this command has been
124 # Whether all remote input related to this command has been
120 # received.
125 # received.
@@ -132,6 +137,7 class commandresponse(object):
132 self._pendingevents = []
137 self._pendingevents = []
133 self._decoder = cborutil.bufferingdecoder()
138 self._decoder = cborutil.bufferingdecoder()
134 self._seeninitial = False
139 self._seeninitial = False
140 self._redirect = None
135
141
136 def _oninputcomplete(self):
142 def _oninputcomplete(self):
137 with self._lock:
143 with self._lock:
@@ -146,10 +152,19 class commandresponse(object):
146
152
147 with self._lock:
153 with self._lock:
148 for o in self._decoder.getavailable():
154 for o in self._decoder.getavailable():
149 if not self._seeninitial:
155 if not self._seeninitial and not self.fromredirect:
150 self._handleinitial(o)
156 self._handleinitial(o)
151 continue
157 continue
152
158
159 # We should never see an object after a content redirect,
160 # as the spec says the main status object containing the
161 # content redirect is the only object in the stream. Fail
162 # if we see a misbehaving server.
163 if self._redirect:
164 raise error.Abort(_('received unexpected response data '
165 'after content redirect; the remote is '
166 'buggy'))
167
153 self._pendingevents.append(o)
168 self._pendingevents.append(o)
154
169
155 self._serviceable.set()
170 self._serviceable.set()
@@ -160,7 +175,16 class commandresponse(object):
160 return
175 return
161
176
162 elif o[b'status'] == b'redirect':
177 elif o[b'status'] == b'redirect':
163 raise error.Abort(_('redirect responses not yet supported'))
178 l = o[b'location']
179 self._redirect = wireprototypes.alternatelocationresponse(
180 url=l[b'url'],
181 mediatype=l[b'mediatype'],
182 size=l.get(b'size'),
183 fullhashes=l.get(b'fullhashes'),
184 fullhashseed=l.get(b'fullhashseed'),
185 serverdercerts=l.get(b'serverdercerts'),
186 servercadercerts=l.get(b'servercadercerts'))
187 return
164
188
165 atoms = [{'msg': o[b'error'][b'message']}]
189 atoms = [{'msg': o[b'error'][b'message']}]
166 if b'args' in o[b'error']:
190 if b'args' in o[b'error']:
@@ -214,13 +238,17 class clienthandler(object):
214 with the higher-level peer API.
238 with the higher-level peer API.
215 """
239 """
216
240
217 def __init__(self, ui, clientreactor):
241 def __init__(self, ui, clientreactor, opener=None,
242 requestbuilder=util.urlreq.request):
218 self._ui = ui
243 self._ui = ui
219 self._reactor = clientreactor
244 self._reactor = clientreactor
220 self._requests = {}
245 self._requests = {}
221 self._futures = {}
246 self._futures = {}
222 self._responses = {}
247 self._responses = {}
248 self._redirects = []
223 self._frameseof = False
249 self._frameseof = False
250 self._opener = opener or urlmod.opener(ui)
251 self._requestbuilder = requestbuilder
224
252
225 def callcommand(self, command, args, f, redirect=None):
253 def callcommand(self, command, args, f, redirect=None):
226 """Register a request to call a command.
254 """Register a request to call a command.
@@ -269,7 +297,12 class clienthandler(object):
269 self._ui.note(_('received %r\n') % frame)
297 self._ui.note(_('received %r\n') % frame)
270 self._processframe(frame)
298 self._processframe(frame)
271
299
272 if self._frameseof:
300 # Also try to read the first redirect.
301 if self._redirects:
302 if not self._processredirect(*self._redirects[0]):
303 self._redirects.pop(0)
304
305 if self._frameseof and not self._redirects:
273 return None
306 return None
274
307
275 return True
308 return True
@@ -318,10 +351,27 class clienthandler(object):
318 # This can raise. The caller can handle it.
351 # This can raise. The caller can handle it.
319 response._onresponsedata(meta['data'])
352 response._onresponsedata(meta['data'])
320
353
354 # If we got a content redirect response, we want to fetch it and
355 # expose the data as if we received it inline. But we also want to
356 # keep our internal request accounting in order. Our strategy is to
357 # basically put meaningful response handling on pause until EOS occurs
358 # and the stream accounting is in a good state. At that point, we follow
359 # the redirect and replace the response object with its data.
360
361 redirect = response._redirect
362 handlefuture = False if redirect else True
363
321 if meta['eos']:
364 if meta['eos']:
322 response._oninputcomplete()
365 response._oninputcomplete()
323 del self._requests[frame.requestid]
366 del self._requests[frame.requestid]
324
367
368 if redirect:
369 self._followredirect(frame.requestid, redirect)
370 return
371
372 if not handlefuture:
373 return
374
325 # If the command has a decoder, we wait until all input has been
375 # If the command has a decoder, we wait until all input has been
326 # received before resolving the future. Otherwise we resolve the
376 # received before resolving the future. Otherwise we resolve the
327 # future immediately.
377 # future immediately.
@@ -336,6 +386,82 class clienthandler(object):
336 self._futures[frame.requestid].set_result(decoded)
386 self._futures[frame.requestid].set_result(decoded)
337 del self._futures[frame.requestid]
387 del self._futures[frame.requestid]
338
388
389 def _followredirect(self, requestid, redirect):
390 """Called to initiate redirect following for a request."""
391 self._ui.note(_('(following redirect to %s)\n') % redirect.url)
392
393 # TODO handle framed responses.
394 if redirect.mediatype != b'application/mercurial-cbor':
395 raise error.Abort(_('cannot handle redirects for the %s media type')
396 % redirect.mediatype)
397
398 if redirect.fullhashes:
399 self._ui.warn(_('(support for validating hashes on content '
400 'redirects not supported)\n'))
401
402 if redirect.serverdercerts or redirect.servercadercerts:
403 self._ui.warn(_('(support for pinning server certificates on '
404 'content redirects not supported)\n'))
405
406 headers = {
407 r'Accept': redirect.mediatype,
408 }
409
410 req = self._requestbuilder(pycompat.strurl(redirect.url), None, headers)
411
412 try:
413 res = self._opener.open(req)
414 except util.urlerr.httperror as e:
415 if e.code == 401:
416 raise error.Abort(_('authorization failed'))
417 raise
418 except util.httplib.HTTPException as e:
419 self._ui.debug('http error requesting %s\n' % req.get_full_url())
420 self._ui.traceback()
421 raise IOError(None, e)
422
423 urlmod.wrapresponse(res)
424
425 # The existing response object is associated with frame data. Rather
426 # than try to normalize its state, just create a new object.
427 oldresponse = self._responses[requestid]
428 self._responses[requestid] = commandresponse(requestid,
429 oldresponse.command,
430 fromredirect=True)
431
432 self._redirects.append((requestid, res))
433
434 def _processredirect(self, rid, res):
435 """Called to continue processing a response from a redirect."""
436 response = self._responses[rid]
437
438 try:
439 data = res.read(32768)
440 response._onresponsedata(data)
441
442 # We're at end of stream.
443 if not data:
444 response._oninputcomplete()
445
446 if rid not in self._futures:
447 return
448
449 if response.command not in COMMAND_DECODERS:
450 self._futures[rid].set_result(response.objects())
451 del self._futures[rid]
452 elif response._inputcomplete:
453 decoded = COMMAND_DECODERS[response.command](response.objects())
454 self._futures[rid].set_result(decoded)
455 del self._futures[rid]
456
457 return bool(data)
458
459 except BaseException as e:
460 self._futures[rid].set_exception(e)
461 del self._futures[rid]
462 response._oninputcomplete()
463 return False
464
339 def decodebranchmap(objs):
465 def decodebranchmap(objs):
340 # Response should be a single CBOR map of branch name to array of nodes.
466 # Response should be a single CBOR map of branch name to array of nodes.
341 bm = next(objs)
467 bm = next(objs)
@@ -1354,8 +1354,33 2nd request should result in content red
1354 s> 0\r\n
1354 s> 0\r\n
1355 s> \r\n
1355 s> \r\n
1356 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
1356 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
1357 abort: redirect responses not yet supported
1357 (following redirect to http://*:$HGPORT/api/simplecache/c045a581599d58608efd3d93d8129841f2af04a0) (glob)
1358 [255]
1358 s> GET /api/simplecache/c045a581599d58608efd3d93d8129841f2af04a0 HTTP/1.1\r\n
1359 s> Accept-Encoding: identity\r\n
1360 s> accept: application/mercurial-cbor\r\n
1361 s> host: *:$HGPORT\r\n (glob)
1362 s> user-agent: Mercurial debugwireproto\r\n
1363 s> \r\n
1364 s> makefile('rb', None)
1365 s> HTTP/1.1 200 OK\r\n
1366 s> Server: testing stub value\r\n
1367 s> Date: $HTTP_DATE$\r\n
1368 s> Content-Type: application/mercurial-cbor\r\n
1369 s> Content-Length: 91\r\n
1370 s> \r\n
1371 s> \xa1Jtotalitems\x01\xa2DnodeT\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&AGparents\x82T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
1372 response: gen[
1373 {
1374 b'totalitems': 1
1375 },
1376 {
1377 b'node': b'\x99/Gy\x02\x9a=\xf8\xd0fm\x00\xbb\x92OicN&A',
1378 b'parents': [
1379 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
1380 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
1381 ]
1382 }
1383 ]
1359
1384
1360 $ cat error.log
1385 $ cat error.log
1361 $ killdaemons.py
1386 $ killdaemons.py
General Comments 0
You need to be logged in to leave comments. Login now