mercurial/wireprotov2peer.py
changeset 40025 86b22a4cfab1
parent 40020 f5a05bb48116
child 40027 7e807b8a9e56
--- a/mercurial/wireprotov2peer.py	Wed Sep 26 17:46:48 2018 -0700
+++ b/mercurial/wireprotov2peer.py	Wed Sep 26 15:02:19 2018 -0700
@@ -13,6 +13,7 @@
 from . import (
     encoding,
     error,
+    sslutil,
     wireprotoframing,
 )
 from .utils import (
@@ -34,6 +35,74 @@
 
     return b''.join(chunks)
 
+SUPPORTED_REDIRECT_PROTOCOLS = {
+    b'http',
+    b'https',
+}
+
+SUPPORTED_CONTENT_HASHES = {
+    b'sha1',
+    b'sha256',
+}
+
+def redirecttargetsupported(ui, target):
+    """Determine whether a redirect target entry is supported.
+
+    ``target`` should come from the capabilities data structure emitted by
+    the server.
+    """
+    if target.get(b'protocol') not in SUPPORTED_REDIRECT_PROTOCOLS:
+        ui.note(_('(remote redirect target %s uses unsupported protocol: %s)\n')
+                % (target[b'name'], target.get(b'protocol', b'')))
+        return False
+
+    if target.get(b'snirequired') and not sslutil.hassni:
+        ui.note(_('(redirect target %s requires SNI, which is unsupported)\n') %
+                target[b'name'])
+        return False
+
+    if b'tlsversions' in target:
+        tlsversions = set(target[b'tlsversions'])
+        supported = set()
+
+        for v in sslutil.supportedprotocols:
+            assert v.startswith(b'tls')
+            supported.add(v[3:])
+
+        if not tlsversions & supported:
+            ui.note(_('(remote redirect target %s requires unsupported TLS '
+                      'versions: %s)\n') % (
+                target[b'name'], b', '.join(sorted(tlsversions))))
+            return False
+
+    ui.note(_('(remote redirect target %s is compatible)\n') % target[b'name'])
+
+    return True
+
+def supportedredirects(ui, apidescriptor):
+    """Resolve the "redirect" command request key given an API descriptor.
+
+    Given an API descriptor returned by the server, returns a data structure
+    that can be used in hte "redirect" field of command requests to advertise
+    support for compatible redirect targets.
+
+    Returns None if no redirect targets are remotely advertised or if none are
+    supported.
+    """
+    if not apidescriptor or b'redirect' not in apidescriptor:
+        return None
+
+    targets = [t[b'name'] for t in apidescriptor[b'redirect'][b'targets']
+               if redirecttargetsupported(ui, t)]
+
+    hashes = [h for h in apidescriptor[b'redirect'][b'hashes']
+              if h in SUPPORTED_CONTENT_HASHES]
+
+    return {
+        b'targets': targets,
+        b'hashes': hashes,
+    }
+
 class commandresponse(object):
     """Represents the response to a command request.
 
@@ -87,9 +156,12 @@
 
     def _handleinitial(self, o):
         self._seeninitial = True
-        if o[b'status'] == 'ok':
+        if o[b'status'] == b'ok':
             return
 
+        elif o[b'status'] == b'redirect':
+            raise error.Abort(_('redirect responses not yet supported'))
+
         atoms = [{'msg': o[b'error'][b'message']}]
         if b'args' in o[b'error']:
             atoms[0]['args'] = o[b'error'][b'args']
@@ -150,12 +222,13 @@
         self._responses = {}
         self._frameseof = False
 
-    def callcommand(self, command, args, f):
+    def callcommand(self, command, args, f, redirect=None):
         """Register a request to call a command.
 
         Returns an iterable of frames that should be sent over the wire.
         """
-        request, action, meta = self._reactor.callcommand(command, args)
+        request, action, meta = self._reactor.callcommand(command, args,
+                                                          redirect=redirect)
 
         if action != 'noop':
             raise error.ProgrammingError('%s not yet supported' % action)