##// 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 513 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
514 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 520 url = '%s/%s' % (apiurl, permission)
519 521
@@ -13,8 +13,12 from .i18n import _
13 13 from . import (
14 14 encoding,
15 15 error,
16 pycompat,
16 17 sslutil,
18 url as urlmod,
19 util,
17 20 wireprotoframing,
21 wireprototypes,
18 22 )
19 23 from .utils import (
20 24 cborutil,
@@ -112,9 +116,10 class commandresponse(object):
112 116 events occur.
113 117 """
114 118
115 def __init__(self, requestid, command):
119 def __init__(self, requestid, command, fromredirect=False):
116 120 self.requestid = requestid
117 121 self.command = command
122 self.fromredirect = fromredirect
118 123
119 124 # Whether all remote input related to this command has been
120 125 # received.
@@ -132,6 +137,7 class commandresponse(object):
132 137 self._pendingevents = []
133 138 self._decoder = cborutil.bufferingdecoder()
134 139 self._seeninitial = False
140 self._redirect = None
135 141
136 142 def _oninputcomplete(self):
137 143 with self._lock:
@@ -146,10 +152,19 class commandresponse(object):
146 152
147 153 with self._lock:
148 154 for o in self._decoder.getavailable():
149 if not self._seeninitial:
155 if not self._seeninitial and not self.fromredirect:
150 156 self._handleinitial(o)
151 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 168 self._pendingevents.append(o)
154 169
155 170 self._serviceable.set()
@@ -160,7 +175,16 class commandresponse(object):
160 175 return
161 176
162 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 189 atoms = [{'msg': o[b'error'][b'message']}]
166 190 if b'args' in o[b'error']:
@@ -214,13 +238,17 class clienthandler(object):
214 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 243 self._ui = ui
219 244 self._reactor = clientreactor
220 245 self._requests = {}
221 246 self._futures = {}
222 247 self._responses = {}
248 self._redirects = []
223 249 self._frameseof = False
250 self._opener = opener or urlmod.opener(ui)
251 self._requestbuilder = requestbuilder
224 252
225 253 def callcommand(self, command, args, f, redirect=None):
226 254 """Register a request to call a command.
@@ -269,7 +297,12 class clienthandler(object):
269 297 self._ui.note(_('received %r\n') % frame)
270 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 306 return None
274 307
275 308 return True
@@ -318,10 +351,27 class clienthandler(object):
318 351 # This can raise. The caller can handle it.
319 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 364 if meta['eos']:
322 365 response._oninputcomplete()
323 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 375 # If the command has a decoder, we wait until all input has been
326 376 # received before resolving the future. Otherwise we resolve the
327 377 # future immediately.
@@ -336,6 +386,82 class clienthandler(object):
336 386 self._futures[frame.requestid].set_result(decoded)
337 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 465 def decodebranchmap(objs):
340 466 # Response should be a single CBOR map of branch name to array of nodes.
341 467 bm = next(objs)
@@ -1354,8 +1354,33 2nd request should result in content red
1354 1354 s> 0\r\n
1355 1355 s> \r\n
1356 1356 received frame(size=0; request=1; stream=2; streamflags=; type=command-response; flags=eos)
1357 abort: redirect responses not yet supported
1358 [255]
1357 (following redirect to http://*:$HGPORT/api/simplecache/c045a581599d58608efd3d93d8129841f2af04a0) (glob)
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 1385 $ cat error.log
1361 1386 $ killdaemons.py
General Comments 0
You need to be logged in to leave comments. Login now