56 ``target`` should come from the capabilities data structure emitted by |
56 ``target`` should come from the capabilities data structure emitted by |
57 the server. |
57 the server. |
58 """ |
58 """ |
59 if target.get(b'protocol') not in SUPPORTED_REDIRECT_PROTOCOLS: |
59 if target.get(b'protocol') not in SUPPORTED_REDIRECT_PROTOCOLS: |
60 ui.note( |
60 ui.note( |
61 _('(remote redirect target %s uses unsupported protocol: %s)\n') |
61 _(b'(remote redirect target %s uses unsupported protocol: %s)\n') |
62 % (target[b'name'], target.get(b'protocol', b'')) |
62 % (target[b'name'], target.get(b'protocol', b'')) |
63 ) |
63 ) |
64 return False |
64 return False |
65 |
65 |
66 if target.get(b'snirequired') and not sslutil.hassni: |
66 if target.get(b'snirequired') and not sslutil.hassni: |
67 ui.note( |
67 ui.note( |
68 _('(redirect target %s requires SNI, which is unsupported)\n') |
68 _(b'(redirect target %s requires SNI, which is unsupported)\n') |
69 % target[b'name'] |
69 % target[b'name'] |
70 ) |
70 ) |
71 return False |
71 return False |
72 |
72 |
73 if b'tlsversions' in target: |
73 if b'tlsversions' in target: |
79 supported.add(v[3:]) |
79 supported.add(v[3:]) |
80 |
80 |
81 if not tlsversions & supported: |
81 if not tlsversions & supported: |
82 ui.note( |
82 ui.note( |
83 _( |
83 _( |
84 '(remote redirect target %s requires unsupported TLS ' |
84 b'(remote redirect target %s requires unsupported TLS ' |
85 'versions: %s)\n' |
85 b'versions: %s)\n' |
86 ) |
86 ) |
87 % (target[b'name'], b', '.join(sorted(tlsversions))) |
87 % (target[b'name'], b', '.join(sorted(tlsversions))) |
88 ) |
88 ) |
89 return False |
89 return False |
90 |
90 |
91 ui.note(_('(remote redirect target %s is compatible)\n') % target[b'name']) |
91 ui.note(_(b'(remote redirect target %s is compatible)\n') % target[b'name']) |
92 |
92 |
93 return True |
93 return True |
94 |
94 |
95 |
95 |
96 def supportedredirects(ui, apidescriptor): |
96 def supportedredirects(ui, apidescriptor): |
179 # content redirect is the only object in the stream. Fail |
179 # content redirect is the only object in the stream. Fail |
180 # if we see a misbehaving server. |
180 # if we see a misbehaving server. |
181 if self._redirect: |
181 if self._redirect: |
182 raise error.Abort( |
182 raise error.Abort( |
183 _( |
183 _( |
184 'received unexpected response data ' |
184 b'received unexpected response data ' |
185 'after content redirect; the remote is ' |
185 b'after content redirect; the remote is ' |
186 'buggy' |
186 b'buggy' |
187 ) |
187 ) |
188 ) |
188 ) |
189 |
189 |
190 self._pendingevents.append(o) |
190 self._pendingevents.append(o) |
191 |
191 |
213 serverdercerts=l.get(b'serverdercerts'), |
213 serverdercerts=l.get(b'serverdercerts'), |
214 servercadercerts=l.get(b'servercadercerts'), |
214 servercadercerts=l.get(b'servercadercerts'), |
215 ) |
215 ) |
216 return |
216 return |
217 |
217 |
218 atoms = [{'msg': o[b'error'][b'message']}] |
218 atoms = [{b'msg': o[b'error'][b'message']}] |
219 if b'args' in o[b'error']: |
219 if b'args' in o[b'error']: |
220 atoms[0]['args'] = o[b'error'][b'args'] |
220 atoms[0][b'args'] = o[b'error'][b'args'] |
221 |
221 |
222 raise error.RepoError(formatrichmessage(atoms)) |
222 raise error.RepoError(formatrichmessage(atoms)) |
223 |
223 |
224 def objects(self): |
224 def objects(self): |
225 """Obtained decoded objects from this response. |
225 """Obtained decoded objects from this response. |
291 """ |
291 """ |
292 request, action, meta = self._reactor.callcommand( |
292 request, action, meta = self._reactor.callcommand( |
293 command, args, redirect=redirect |
293 command, args, redirect=redirect |
294 ) |
294 ) |
295 |
295 |
296 if action != 'noop': |
296 if action != b'noop': |
297 raise error.ProgrammingError('%s not yet supported' % action) |
297 raise error.ProgrammingError(b'%s not yet supported' % action) |
298 |
298 |
299 rid = request.requestid |
299 rid = request.requestid |
300 self._requests[rid] = request |
300 self._requests[rid] = request |
301 self._futures[rid] = f |
301 self._futures[rid] = f |
302 # TODO we need some kind of lifetime on response instances otherwise |
302 # TODO we need some kind of lifetime on response instances otherwise |
310 |
310 |
311 Returns an iterable of frames that should be sent over the wire. |
311 Returns an iterable of frames that should be sent over the wire. |
312 """ |
312 """ |
313 action, meta = self._reactor.flushcommands() |
313 action, meta = self._reactor.flushcommands() |
314 |
314 |
315 if action != 'sendframes': |
315 if action != b'sendframes': |
316 raise error.ProgrammingError('%s not yet supported' % action) |
316 raise error.ProgrammingError(b'%s not yet supported' % action) |
317 |
317 |
318 return meta['framegen'] |
318 return meta[b'framegen'] |
319 |
319 |
320 def readdata(self, framefh): |
320 def readdata(self, framefh): |
321 """Attempt to read data and do work. |
321 """Attempt to read data and do work. |
322 |
322 |
323 Returns None if no data was read. Presumably this means we're |
323 Returns None if no data was read. Presumably this means we're |
327 frame = wireprotoframing.readframe(framefh) |
327 frame = wireprotoframing.readframe(framefh) |
328 if frame is None: |
328 if frame is None: |
329 # TODO tell reactor? |
329 # TODO tell reactor? |
330 self._frameseof = True |
330 self._frameseof = True |
331 else: |
331 else: |
332 self._ui.debug('received %r\n' % frame) |
332 self._ui.debug(b'received %r\n' % frame) |
333 self._processframe(frame) |
333 self._processframe(frame) |
334 |
334 |
335 # Also try to read the first redirect. |
335 # Also try to read the first redirect. |
336 if self._redirects: |
336 if self._redirects: |
337 if not self._processredirect(*self._redirects[0]): |
337 if not self._processredirect(*self._redirects[0]): |
345 def _processframe(self, frame): |
345 def _processframe(self, frame): |
346 """Process a single read frame.""" |
346 """Process a single read frame.""" |
347 |
347 |
348 action, meta = self._reactor.onframerecv(frame) |
348 action, meta = self._reactor.onframerecv(frame) |
349 |
349 |
350 if action == 'error': |
350 if action == b'error': |
351 e = error.RepoError(meta['message']) |
351 e = error.RepoError(meta[b'message']) |
352 |
352 |
353 if frame.requestid in self._responses: |
353 if frame.requestid in self._responses: |
354 self._responses[frame.requestid]._oninputcomplete() |
354 self._responses[frame.requestid]._oninputcomplete() |
355 |
355 |
356 if frame.requestid in self._futures: |
356 if frame.requestid in self._futures: |
358 del self._futures[frame.requestid] |
358 del self._futures[frame.requestid] |
359 else: |
359 else: |
360 raise e |
360 raise e |
361 |
361 |
362 return |
362 return |
363 elif action == 'noop': |
363 elif action == b'noop': |
364 return |
364 return |
365 elif action == 'responsedata': |
365 elif action == b'responsedata': |
366 # Handled below. |
366 # Handled below. |
367 pass |
367 pass |
368 else: |
368 else: |
369 raise error.ProgrammingError('action not handled: %s' % action) |
369 raise error.ProgrammingError(b'action not handled: %s' % action) |
370 |
370 |
371 if frame.requestid not in self._requests: |
371 if frame.requestid not in self._requests: |
372 raise error.ProgrammingError( |
372 raise error.ProgrammingError( |
373 'received frame for unknown request; this is either a bug in ' |
373 b'received frame for unknown request; this is either a bug in ' |
374 'the clientreactor not screening for this or this instance was ' |
374 b'the clientreactor not screening for this or this instance was ' |
375 'never told about this request: %r' % frame |
375 b'never told about this request: %r' % frame |
376 ) |
376 ) |
377 |
377 |
378 response = self._responses[frame.requestid] |
378 response = self._responses[frame.requestid] |
379 |
379 |
380 if action == 'responsedata': |
380 if action == b'responsedata': |
381 # Any failures processing this frame should bubble up to the |
381 # Any failures processing this frame should bubble up to the |
382 # future tracking the request. |
382 # future tracking the request. |
383 try: |
383 try: |
384 self._processresponsedata(frame, meta, response) |
384 self._processresponsedata(frame, meta, response) |
385 except BaseException as e: |
385 except BaseException as e: |
395 response._oninputcomplete() |
395 response._oninputcomplete() |
396 else: |
396 else: |
397 response._onerror(e) |
397 response._onerror(e) |
398 else: |
398 else: |
399 raise error.ProgrammingError( |
399 raise error.ProgrammingError( |
400 'unhandled action from clientreactor: %s' % action |
400 b'unhandled action from clientreactor: %s' % action |
401 ) |
401 ) |
402 |
402 |
403 def _processresponsedata(self, frame, meta, response): |
403 def _processresponsedata(self, frame, meta, response): |
404 # This can raise. The caller can handle it. |
404 # This can raise. The caller can handle it. |
405 response._onresponsedata(meta['data']) |
405 response._onresponsedata(meta[b'data']) |
406 |
406 |
407 # We need to be careful about resolving futures prematurely. If a |
407 # We need to be careful about resolving futures prematurely. If a |
408 # response is a redirect response, resolving the future before the |
408 # response is a redirect response, resolving the future before the |
409 # redirect is processed would result in the consumer seeing an |
409 # redirect is processed would result in the consumer seeing an |
410 # empty stream of objects, since they'd be consuming our |
410 # empty stream of objects, since they'd be consuming our |
412 # |
412 # |
413 # Our strategy is to not resolve/finish the request until either |
413 # Our strategy is to not resolve/finish the request until either |
414 # EOS occurs or until the initial response object is fully received. |
414 # EOS occurs or until the initial response object is fully received. |
415 |
415 |
416 # Always react to eos. |
416 # Always react to eos. |
417 if meta['eos']: |
417 if meta[b'eos']: |
418 response._oninputcomplete() |
418 response._oninputcomplete() |
419 del self._requests[frame.requestid] |
419 del self._requests[frame.requestid] |
420 |
420 |
421 # Not EOS but we haven't decoded the initial response object yet. |
421 # Not EOS but we haven't decoded the initial response object yet. |
422 # Return and wait for more data. |
422 # Return and wait for more data. |
444 self._futures[frame.requestid].set_result(decoded) |
444 self._futures[frame.requestid].set_result(decoded) |
445 del self._futures[frame.requestid] |
445 del self._futures[frame.requestid] |
446 |
446 |
447 def _followredirect(self, requestid, redirect): |
447 def _followredirect(self, requestid, redirect): |
448 """Called to initiate redirect following for a request.""" |
448 """Called to initiate redirect following for a request.""" |
449 self._ui.note(_('(following redirect to %s)\n') % redirect.url) |
449 self._ui.note(_(b'(following redirect to %s)\n') % redirect.url) |
450 |
450 |
451 # TODO handle framed responses. |
451 # TODO handle framed responses. |
452 if redirect.mediatype != b'application/mercurial-cbor': |
452 if redirect.mediatype != b'application/mercurial-cbor': |
453 raise error.Abort( |
453 raise error.Abort( |
454 _('cannot handle redirects for the %s media type') |
454 _(b'cannot handle redirects for the %s media type') |
455 % redirect.mediatype |
455 % redirect.mediatype |
456 ) |
456 ) |
457 |
457 |
458 if redirect.fullhashes: |
458 if redirect.fullhashes: |
459 self._ui.warn( |
459 self._ui.warn( |
460 _( |
460 _( |
461 '(support for validating hashes on content ' |
461 b'(support for validating hashes on content ' |
462 'redirects not supported)\n' |
462 b'redirects not supported)\n' |
463 ) |
463 ) |
464 ) |
464 ) |
465 |
465 |
466 if redirect.serverdercerts or redirect.servercadercerts: |
466 if redirect.serverdercerts or redirect.servercadercerts: |
467 self._ui.warn( |
467 self._ui.warn( |
468 _( |
468 _( |
469 '(support for pinning server certificates on ' |
469 b'(support for pinning server certificates on ' |
470 'content redirects not supported)\n' |
470 b'content redirects not supported)\n' |
471 ) |
471 ) |
472 ) |
472 ) |
473 |
473 |
474 headers = { |
474 headers = { |
475 r'Accept': redirect.mediatype, |
475 r'Accept': redirect.mediatype, |
479 |
479 |
480 try: |
480 try: |
481 res = self._opener.open(req) |
481 res = self._opener.open(req) |
482 except util.urlerr.httperror as e: |
482 except util.urlerr.httperror as e: |
483 if e.code == 401: |
483 if e.code == 401: |
484 raise error.Abort(_('authorization failed')) |
484 raise error.Abort(_(b'authorization failed')) |
485 raise |
485 raise |
486 except util.httplib.HTTPException as e: |
486 except util.httplib.HTTPException as e: |
487 self._ui.debug('http error requesting %s\n' % req.get_full_url()) |
487 self._ui.debug(b'http error requesting %s\n' % req.get_full_url()) |
488 self._ui.traceback() |
488 self._ui.traceback() |
489 raise IOError(None, e) |
489 raise IOError(None, e) |
490 |
490 |
491 urlmod.wrapresponse(res) |
491 urlmod.wrapresponse(res) |
492 |
492 |
565 def decodepushkey(objs): |
565 def decodepushkey(objs): |
566 return next(objs) |
566 return next(objs) |
567 |
567 |
568 |
568 |
569 COMMAND_DECODERS = { |
569 COMMAND_DECODERS = { |
570 'branchmap': decodebranchmap, |
570 b'branchmap': decodebranchmap, |
571 'heads': decodeheads, |
571 b'heads': decodeheads, |
572 'known': decodeknown, |
572 b'known': decodeknown, |
573 'listkeys': decodelistkeys, |
573 b'listkeys': decodelistkeys, |
574 'lookup': decodelookup, |
574 b'lookup': decodelookup, |
575 'pushkey': decodepushkey, |
575 b'pushkey': decodepushkey, |
576 } |
576 } |