Repository: pypa/pip

Issue:
Failure to authenticate private repository when URL-encoded character in password
See https://github.com/pypa/pip/issues/51#issuecomment-128548720 

I have this issue with pip 7.1.2. There's a symbol in the password that needs url-encoding.

With the password url-encoded, pip fails to authenticate (gets a 401 response) and prompts for username/password interactively. It then works fine if I enter the user/pass interactively. Like @bcbrown I suspect it's not url-decoding the password.


Discussion:

Patch:
diff --git a/news/3236.bugfix b/news/3236.bugfix
new file mode 100644
index 000000000..51d358458
--- /dev/null
+++ b/news/3236.bugfix
@@ -0,0 +1,1 @@
+"Support URL-encoded characters in URL credentials."
diff --git a/pip/download.py b/pip/download.py
index 41709db5e..894d0ff9a 100644
--- a/pip/download.py
+++ b/pip/download.py
@@ -21,6 +21,7 @@ except ImportError:
 
 from pip._vendor.six.moves.urllib import parse as urllib_parse
 from pip._vendor.six.moves.urllib import request as urllib_request
+from pip._vendor.six.moves.urllib.parse import unquote as urllib_unquote
 
 import pip
 
@@ -207,8 +208,9 @@ class MultiDomainBasicAuth(AuthBase):
         if "@" in netloc:
             userinfo = netloc.rsplit("@", 1)[0]
             if ":" in userinfo:
-                return userinfo.split(":", 1)
-            return userinfo, None
+                user, pwd = userinfo.split(":", 1)
+                return (urllib_unquote(user), urllib_unquote(pwd))
+            return urllib_unquote(userinfo), None
         return None, None
 
 


Test patch:
diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py
index ee4b11c7d..1630393f0 100644
--- a/tests/unit/test_download.py
+++ b/tests/unit/test_download.py
@@ -13,7 +13,7 @@ import pip
 from pip.exceptions import HashMismatch
 from pip.download import (
     PipSession, SafeFileCache, path_to_url, unpack_http_url, url_to_path,
-    unpack_file_url,
+    unpack_file_url, MultiDomainBasicAuth
 )
 from pip.index import Link
 from pip.utils.hashes import Hashes
@@ -333,3 +333,14 @@ class TestPipSession:
         )
 
         assert not hasattr(session.adapters["https://example.com/"], "cache")
+
+
+def test_parse_credentials():
+    auth = MultiDomainBasicAuth()
+    assert auth.parse_credentials("foo:bar@example.com") == ('foo', 'bar')
+    assert auth.parse_credentials("foo@example.com") == ('foo', None)
+    assert auth.parse_credentials("example.com") == (None, None)
+
+    # URL-encoded reserved characters:
+    assert auth.parse_credentials("user%3Aname:%23%40%5E@example.com") \
+        == ("user:name", "#@^")