changeset 964:27fec8bf9273

svnwrap: Implement handling of get_ssl_server_trust_prompt_provider If the server certificate is untrusted when connected to a subversion repository using the connection SSL, respond with a message similar to svn client. Here, we can choose either a permanent accept, temporary accept, rejection.
author Mitsuhiro Koga <shiena.jp@gmail.com>
date Wed, 10 Oct 2012 00:28:38 +0900
parents 64d961130a07
children b748a0eed09a
files hgsubversion/svnrepo.py hgsubversion/svnwrap/subvertpy_wrapper.py hgsubversion/svnwrap/svn_swig_wrapper.py
diffstat 3 files changed, 128 insertions(+), 16 deletions(-) [+]
line wrap: on
line diff
--- a/hgsubversion/svnrepo.py
+++ b/hgsubversion/svnrepo.py
@@ -195,4 +195,52 @@ def instance(ui, url, create):
     if create:
         raise hgutil.Abort('cannot create new remote Subversion repository')
 
+    svnwrap.ssl_server_trust_prompt_callback(svn_auth_ssl_server_trust_prompt(ui))
     return svnremoterepo(ui, url)
+
+def svn_auth_ssl_server_trust_prompt(ui):
+    def callback(realm, failures, cert_info, may_save, pool=None):
+        msg = 'Error validating server certificate for \'%s\':\n' % (realm,)
+        if failures & svnwrap.SSL_UNKNOWNCA:
+            msg += (
+                    ' - The certificate is not issued by a trusted authority. Use the\n'
+                    '   fingerprint to validate the certificate manually!\n'
+                    )
+        if failures & svnwrap.SSL_CNMISMATCH:
+            msg += ' - The certificate hostname does not match.\n'
+        if failures & svnwrap.SSL_NOTYETVALID:
+            msg += ' - The certificate is not yet valid.\n'
+        if failures & svnwrap.SSL_EXPIRED:
+            msg += ' - The certificate has expired.\n'
+        if failures & svnwrap.SSL_OTHER:
+            msg += ' - The certificate has an unknown error.\n'
+        msg += (
+                'Certificate information:\n'
+                '- Hostname: %s\n'
+                '- Valid: from %s until %s\n'
+                '- Issuer: %s\n'
+                '- Fingerprint: %s\n'
+                ) % (
+                        cert_info[0], # hostname
+                        cert_info[2], # valid_from
+                        cert_info[3], # valid_until
+                        cert_info[4], # issuer_dname
+                        cert_info[1], # fingerprint
+                        )
+        if may_save:
+            msg += '(R)eject, accept (t)emporarily or accept (p)ermanently? '
+            choices = (('&Reject'), ('&Temporarily'), ('&Permanently'))
+        else:
+            msg += '(R)eject or accept (t)emporarily? '
+            choices = (('&Reject'), ('&Temporarily'))
+        choice = ui.promptchoice(msg, choices, default=0)
+        if choice == 1:
+            creds = (failures, False)
+        elif may_save and choice == 2:
+            creds = (failures, True)
+        else:
+            creds = None
+
+        return creds
+    return callback
+
--- a/hgsubversion/svnwrap/subvertpy_wrapper.py
+++ b/hgsubversion/svnwrap/subvertpy_wrapper.py
@@ -52,6 +52,11 @@ def version():
         svnvers += '-' + subversion_version[3]
     return (svnvers, 'Subvertpy ' + _versionstr(subvertpy.__version__))
 
+_ssl_server_trust_prompt_callback = None
+def ssl_server_trust_prompt_callback(callback):
+    global _ssl_server_trust_prompt_callback
+    _ssl_server_trust_prompt_callback = callback
+
 # exported values
 ERR_FS_CONFLICT = subvertpy.ERR_FS_CONFLICT
 ERR_FS_NOT_FOUND = subvertpy.ERR_FS_NOT_FOUND
@@ -59,6 +64,11 @@ ERR_FS_TXN_OUT_OF_DATE = subvertpy.ERR_F
 ERR_INCOMPLETE_DATA = subvertpy.ERR_INCOMPLETE_DATA
 ERR_RA_DAV_PATH_NOT_FOUND = subvertpy.ERR_RA_DAV_PATH_NOT_FOUND
 ERR_RA_DAV_REQUEST_FAILED = subvertpy.ERR_RA_DAV_REQUEST_FAILED
+SSL_UNKNOWNCA = subvertpy.SSL_UNKNOWNCA
+SSL_CNMISMATCH = subvertpy.SSL_CNMISMATCH
+SSL_NOTYETVALID = subvertpy.SSL_NOTYETVALID
+SSL_EXPIRED = subvertpy.SSL_EXPIRED
+SSL_OTHER = subvertpy.SSL_OTHER
 SubversionException = subvertpy.SubversionException
 apply_txdelta = delta.apply_txdelta_handler
 # superclass for editor.HgEditor
@@ -202,6 +212,22 @@ class SubversionRepo(object):
             return self.username or username, self.password or '', False
         def getuser(realm, may_save):
             return self.username or '', False
+        def svn_auth_ssl_server_trust_prompt(realm, failures, cert_info, may_save):
+            global _ssl_server_trust_prompt_callback
+            if _ssl_server_trust_prompt_callback:
+                ret = _ssl_server_trust_prompt_callback(realm, failures, cert_info, may_save)
+                if ret:
+                    creds = ret
+                else:
+                    # We need to reject the certificate, but subvertpy doesn't
+                    # handle None as a return value here, and requires
+                    # we instead return a tuple of (int, bool). Because of that,
+                    # we return (0, False) instead.
+                    creds = (0, False)
+            else:
+                creds = (0, False)
+
+            return creds
 
         providers = ra.get_platform_specific_client_providers()
         providers += [
@@ -212,6 +238,7 @@ class SubversionRepo(object):
             ra.get_ssl_server_trust_file_provider(),
             ra.get_username_prompt_provider(getuser, 0),
             ra.get_simple_prompt_provider(getpass, 0),
+            ra.get_ssl_server_trust_prompt_provider(svn_auth_ssl_server_trust_prompt),
         ]
 
         auth = ra.Auth(providers)
@@ -220,9 +247,20 @@ class SubversionRepo(object):
         if self.password:
             auth.set_parameter(subvertpy.AUTH_PARAM_DEFAULT_PASSWORD, self.password)
 
-        self.remote = ra.RemoteAccess(url=self.svn_url,
-                                      client_string_func=getclientstring,
-                                      auth=auth)
+        try:
+            self.remote = ra.RemoteAccess(url=self.svn_url,
+                                          client_string_func=getclientstring,
+                                          auth=auth)
+        except SubversionException, e:
+            # e.child contains a detailed error messages
+            msglist = []
+            svn_exc = e
+            while svn_exc:
+                if svn_exc.args[0]:
+                    msglist.append(svn_exc.args[0])
+                svn_exc = svn_exc.child
+            msg = '\n'.join(msglist)
+            raise common.SubversionConnectionException(msg)
 
         self.client = client.Client()
         self.client.auth = auth
--- a/hgsubversion/svnwrap/svn_swig_wrapper.py
+++ b/hgsubversion/svnwrap/svn_swig_wrapper.py
@@ -43,6 +43,11 @@ ERR_FS_NOT_FOUND = core.SVN_ERR_FS_NOT_F
 ERR_FS_TXN_OUT_OF_DATE = core.SVN_ERR_FS_TXN_OUT_OF_DATE
 ERR_INCOMPLETE_DATA = core.SVN_ERR_INCOMPLETE_DATA
 ERR_RA_DAV_REQUEST_FAILED = core.SVN_ERR_RA_DAV_REQUEST_FAILED
+SSL_UNKNOWNCA = core.SVN_AUTH_SSL_UNKNOWNCA
+SSL_CNMISMATCH = core.SVN_AUTH_SSL_CNMISMATCH
+SSL_NOTYETVALID = core.SVN_AUTH_SSL_NOTYETVALID
+SSL_EXPIRED = core.SVN_AUTH_SSL_EXPIRED
+SSL_OTHER = core.SVN_AUTH_SSL_OTHER
 SubversionException = core.SubversionException
 Editor = delta.Editor
 
@@ -103,6 +108,31 @@ def user_pass_prompt(realm, default_user
     creds.password = getpass.getpass('Password for %s: ' % creds.username)
     return creds
 
+_ssl_server_trust_prompt_callback = None
+def ssl_server_trust_prompt_callback(callback):
+    global _ssl_server_trust_prompt_callback
+    _ssl_server_trust_prompt_callback = callback
+
+def _ssl_server_trust_prompt(realm, failures, cert_info, may_save, pool):
+    global _ssl_server_trust_prompt_callback
+    if _ssl_server_trust_prompt_callback:
+        cert = [
+                cert_info.hostname,
+                cert_info.fingerprint,
+                cert_info.valid_from,
+                cert_info.valid_until,
+                cert_info.issuer_dname,
+                ]
+        ret = _ssl_server_trust_prompt_callback(realm, failures, cert, may_save, pool)
+        if ret:
+            creds = core.svn_auth_cred_ssl_server_trust_t()
+            (creds.accepted_failures, creds.may_save) = ret
+        else:
+            creds = None
+    else:
+        creds = None
+    return creds
+
 def _create_auth_baton(pool, password_stores):
     """Create a Subversion authentication baton. """
     # Give the client context baton a suite of authentication
@@ -146,6 +176,7 @@ def _create_auth_baton(pool, password_st
         client.get_ssl_client_cert_pw_file_provider(),
         client.get_ssl_server_trust_file_provider(),
         client.get_simple_prompt_provider(user_pass_prompt, 2),
+        client.get_ssl_server_trust_prompt_provider(_ssl_server_trust_prompt),
         ]
 
     return core.svn_auth_open(providers, pool)
@@ -211,19 +242,14 @@ class SubversionRepo(object):
             self.ra = ra.open2(self.svn_url, callbacks,
                                svn_config, self.pool)
         except SubversionException, e:
-            if e.apr_err == core.SVN_ERR_RA_SERF_SSL_CERT_UNTRUSTED:
-                msg = ('Subversion does not trust the SSL certificate for this '
-                       'site; please try running \'svn ls %s\' first.'
-                       % self.svn_url)
-            elif e.apr_err == core.SVN_ERR_RA_DAV_REQUEST_FAILED:
-                msg = ('Failed to open Subversion repository; please try '
-                       'running \'svn ls %s\' for details.' % self.svn_url)
-            else:
-                msg = e.args[0]
-                for k, v in vars(core).iteritems():
-                    if k.startswith('SVN_ERR_') and v == e.apr_err:
-                        msg = '%s (%s)' % (msg, k)
-                        break
+            # e.child contains a detailed error messages
+            msglist = []
+            svn_exc = e
+            while svn_exc:
+                if svn_exc.args[0]:
+                    msglist.append(svn_exc.args[0])
+                svn_exc = svn_exc.child
+            msg = '\n'.join(msglist)
             raise common.SubversionConnectionException(msg)
 
     @property