# HG changeset patch # User Mitsuhiro Koga # Date 1349796518 -32400 # Node ID 27fec8bf9273c5fe0384b67ef487d8d8686806d8 # Parent 64d961130a07a652fc03d2406b84935f8d2e689e 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. diff --git a/hgsubversion/svnrepo.py b/hgsubversion/svnrepo.py --- 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 + diff --git a/hgsubversion/svnwrap/subvertpy_wrapper.py b/hgsubversion/svnwrap/subvertpy_wrapper.py --- 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 diff --git a/hgsubversion/svnwrap/svn_swig_wrapper.py b/hgsubversion/svnwrap/svn_swig_wrapper.py --- 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