# HG changeset patch # User Thomas De Schampheleire # Date 1595447757 -7200 # Node ID d727e81e00975c88faf2a2f438391c7acd551341 # Parent d1521d421610e64b51e344fb32711b8a585df29b vcs: fix cloning remote repository with HTTP authentication (Issue #379) Using a remote clone URI of http://user:pass@host/... triggered an exception: ... E File ".../kallithea/lib/utils.py", line 256, in is_valid_repo_uri E GitRepository._check_url(url) E File ".../kallithea/lib/vcs/backends/git/repository.py", line 183, in _check_url E passmgr.add_password(*authinfo) E File "/usr/lib/python3.7/urllib/request.py", line 848, in add_password E self.reduce_uri(u, default_port) for u in uri) E File "/usr/lib/python3.7/urllib/request.py", line 848, in E self.reduce_uri(u, default_port) for u in uri) E File "/usr/lib/python3.7/urllib/request.py", line 875, in reduce_uri E host, port = splitport(authority) E File "/usr/lib/python3.7/urllib/parse.py", line 1022, in splitport E match = _portprog.fullmatch(host) E TypeError: cannot use a string pattern on a bytes-like object The authinfo tuple is obtained via mercurial.util.url, which unfortunately returns a tuple of bytes whereas urllib expects strings. It seems that mercurial internally has some more hacking around urllib as urllibcompat.py, which we don't use. Therefore, transform the bytes into strings before passing authinfo to urllib. As the realm can be None, we need to check it specifically otherwise safe_str would return a string 'None'. A basic test that catches the mentioned problem is added, even though it does not actually test that cloning with auth info will actually work (it only tests that it fails cleanly if the URI is not reachable). Additionally, one use of 'test_uri' in hg/repository.py still needed to be transformed from bytes to string. For git this was already ok. diff -r d1521d421610 -r d727e81e0097 kallithea/lib/vcs/backends/hg/repository.py --- a/kallithea/lib/vcs/backends/hg/repository.py Wed Jul 22 21:48:53 2020 +0200 +++ b/kallithea/lib/vcs/backends/hg/repository.py Wed Jul 22 21:55:57 2020 +0200 @@ -320,7 +320,7 @@ req = urllib.request.Request( "%s?%s" % ( - test_uri, + safe_str(test_uri), urllib.parse.urlencode({ 'cmd': 'between', 'pairs': "%s-%s" % ('0' * 40, '0' * 40), diff -r d1521d421610 -r d727e81e0097 kallithea/lib/vcs/utils/helpers.py --- a/kallithea/lib/vcs/utils/helpers.py Wed Jul 22 21:48:53 2020 +0200 +++ b/kallithea/lib/vcs/utils/helpers.py Wed Jul 22 21:55:57 2020 +0200 @@ -11,6 +11,7 @@ import mercurial.url from kallithea.lib.vcs.exceptions import RepositoryError, VCSError +from kallithea.lib.vcs.utils import safe_str from kallithea.lib.vcs.utils.paths import abspath @@ -226,9 +227,21 @@ test_uri, authinfo = url_obj.authinfo() if authinfo: + # authinfo is a tuple (realm, uris, user, password) where 'uris' itself + # is a tuple of URIs. + # If url_obj is obtained via mercurial.util.url, the obtained authinfo + # values will be bytes, e.g. + # (None, (b'http://127.0.0.1/repo', b'127.0.0.1'), b'user', b'pass') + # However, urllib expects strings, not bytes, so we must convert them. + # create a password manager passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() - passmgr.add_password(*authinfo) + passmgr.add_password( + safe_str(authinfo[0]) if authinfo[0] else None, # realm + tuple(safe_str(x) for x in authinfo[1]), # uris + safe_str(authinfo[2]), # user + safe_str(authinfo[3]), # password + ) handlers.extend((mercurial.url.httpbasicauthhandler(passmgr), mercurial.url.httpdigestauthhandler(passmgr))) diff -r d1521d421610 -r d727e81e0097 kallithea/tests/functional/test_admin_repos.py --- a/kallithea/tests/functional/test_admin_repos.py Wed Jul 22 21:48:53 2020 +0200 +++ b/kallithea/tests/functional/test_admin_repos.py Wed Jul 22 21:55:57 2020 +0200 @@ -344,6 +344,19 @@ _session_csrf_secret_token=self.session_csrf_secret_token())) response.mustcontain('Invalid repository URL') + def test_create_remote_repo_wrong_clone_uri_http_auth(self): + self.log_user() + repo_name = self.NEW_REPO + description = 'description for newly created repo' + response = self.app.post(base.url('repos'), + fixture._get_repo_create_params(repo_private=False, + repo_name=repo_name, + repo_type=self.REPO_TYPE, + repo_description=description, + clone_uri='http://user:pass@127.0.0.1/repo', + _session_csrf_secret_token=self.session_csrf_secret_token())) + response.mustcontain('Invalid repository URL') + def test_delete(self): self.log_user() repo_name = 'vcs_test_new_to_delete_%s' % self.REPO_TYPE