Repository: aio-libs/aiohttp

Issue:
Proxy auth proposal
Currently proxy auth can be invoked only by passing proxy url like "login:password@host", however it is not obvious especially because http auth can be invoked using `auth` param in `aiohttp.request`. 
Maybe it makes sense to add `auth` param to ProxyConnector, e.g.:

```
connector = ProxyConnector('http://localhost', auth=('login', 'pass'))
```



Discussion:
I agree, but I'd make separate parameters for login and password 

But `aiohttp.request` accepts `auth` as tuple of (login, password) 

I think that was wrong decision. I used requests library as an example, but that's wrong

We have to be consistent.
If we will use `login` and `password` as parameters for Proxy Connector
than we need to support that in aiohttp.request too (as well as in other
places).

On Thu, Jun 26, 2014 at 6:43 PM, Nikolay Kim notifications@github.com
wrote:

> I think that was wrong decision. I used requests library as an example,
> but that's wrong
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/KeepSafe/aiohttp/issues/84#issuecomment-47242567.

## 

Thanks,
Andrew Svetlov

I agree. How about basic_login instead of just login or basic_auth_login—
Sent from Mailbox

On Thu, Jun 26, 2014 at 8:48 AM, Andrew Svetlov notifications@github.com
wrote:

> We have to be consistent.
> If we will use `login` and `password` as parameters for Proxy Connector
> than we need to support that in aiohttp.request too (as well as in other
> places).
> On Thu, Jun 26, 2014 at 6:43 PM, Nikolay Kim notifications@github.com
> wrote:
> 
> > I think that was wrong decision. I used requests library as an example,
> > but that's wrong
> > 
> > —
> > Reply to this email directly or view it on GitHub
> > https://github.com/KeepSafe/aiohttp/issues/84#issuecomment-47242567.
> > 
> > ## 
> > 
> > Thanks,
> > 
> > ## Andrew Svetlov
> > 
> > Reply to this email directly or view it on GitHub:
> > https://github.com/KeepSafe/aiohttp/issues/84#issuecomment-47243114

`basic_login` and `basic_passwd`?
Both should be either `None` or not `None`

Looks good. And proxy_login for proxy—
Sent from Mailbox

On Thu, Jun 26, 2014 at 8:57 AM, Andrew Svetlov notifications@github.com
wrote:

> `basic_login` and `basic_passwd`?
> 
> ## Both should be either `None` or not `None`
> 
> Reply to this email directly or view it on GitHub:
> https://github.com/KeepSafe/aiohttp/issues/84#issuecomment-47244287


Resolving patch:
diff --git a/aiohttp/client.py b/aiohttp/client.py
--- a/aiohttp/client.py
+++ b/aiohttp/client.py
@@ -35,6 +35,8 @@ def request(method, url, *,
             cookies=None,
             files=None,
             auth=None,
+            basic_login=None,
+            basic_passwd=None,
             allow_redirects=True,
             max_redirects=10,
             encoding='utf-8',
@@ -60,7 +62,9 @@ def request(method, url, *,
     :param cookies: (optional) Dict object to send with the request
     :param files: (optional) Dictionary of 'name': file-like-objects
        for multipart encoding upload
-    :param auth: (optional) Auth tuple to enable Basic HTTP Auth
+    :param auth: (optional) Basic HTTP Auth tuple, Deprecated!
+    :param basic_login: (optional) Basic HTTP Auth login
+    :param basic_passwd: (optional) Basic HTTP Auth password
     :param allow_redirects: (optional) Boolean. Set to True if POST/PUT/DELETE
        redirect following is allowed.
     :param compress: Boolean. Set to True if request has to be compressed
@@ -93,10 +97,24 @@ def request(method, url, *,
     if connector is None:
         connector = aiohttp.TCPConnector(force_close=True, loop=loop)
 
+    if auth is not None:
+        warnings.warn(
+            'auth is deprecated, use basic_login and basic_passwd instead',
+            DeprecationWarning
+        )
+        if basic_login is None and basic_passwd is None:
+            basic_login, basic_passwd = auth
+        elif basic_login is not None and basic_passwd is not None:
+            if not auth == (basic_login, basic_passwd):
+                raise ValueError(
+                    "auth and (basic_login, basic_passwd) are different"
+                )
+
     while True:
         req = request_class(
             method, url, params=params, headers=headers, data=data,
-            cookies=cookies, files=files, auth=auth, encoding=encoding,
+            cookies=cookies, files=files, basic_login=basic_login,
+            basic_passwd=basic_passwd, encoding=encoding,
             version=version, compress=compress, chunked=chunked,
             loop=loop, expect100=expect100,
             response_class=response_class)
@@ -153,7 +171,8 @@ class ClientRequest:
     }
 
     body = b''
-    auth = None
+    basic_login = None
+    basic_passwd = None
     response = None
     response_class = None
 
@@ -171,9 +190,10 @@ class ClientRequest:
 
     def __init__(self, method, url, *,
                  params=None, headers=None, data=None, cookies=None,
-                 files=None, auth=None, encoding='utf-8', version=(1, 1),
-                 compress=None, chunked=None, expect100=False,
-                 verify_ssl=True, loop=None, response_class=None):
+                 files=None, basic_login=None, basic_passwd=None,
+                 encoding='utf-8', version=(1, 1), compress=None,
+                 chunked=None, expect100=False, verify_ssl=True,
+                 loop=None, response_class=None):
         self.url = url
         self.method = method.upper()
         self.encoding = encoding
@@ -189,7 +209,7 @@ def __init__(self, method, url, *,
         self.update_headers(headers)
         self.update_cookies(cookies)
         self.update_content_encoding()
-        self.update_auth(auth)
+        self.update_auth(basic_login, basic_passwd)
 
         if data and not files:
             if self.method not in self.GET_METHODS:
@@ -215,9 +235,10 @@ def update_host(self, url):
         # basic auth info
         if '@' in netloc:
             authinfo, netloc = netloc.split('@', 1)
-            self.auth = authinfo.split(':', 1)
-            if len(self.auth) == 1:
-                self.auth.append('')
+            creds = authinfo.split(':', 1)
+            self.basic_login = creds[0]
+            if len(creds) > 1:
+                self.basic_passwd = creds[1]
 
         # Record entire netloc for usage in host header
         self.netloc = netloc
@@ -335,20 +356,20 @@ def update_content_encoding(self):
             self.headers['CONTENT-ENCODING'] = self.compress
             self.chunked = True  # enable chunked, no need to deal with length
 
-    def update_auth(self, auth):
+    def update_auth(self, basic_login, basic_passwd):
         """Set basic auth."""
-        if not auth:
-            auth = self.auth
-
-        if auth:
-            if isinstance(auth, (tuple, list)) and len(auth) == 2:
-                # basic auth
-                self.headers['AUTHORIZATION'] = 'Basic %s' % (
-                    base64.b64encode(
-                        ('%s:%s' % (auth[0], auth[1])).encode('latin1'))
-                    .strip().decode('latin1'))
-            else:
-                raise ValueError("Only basic auth is supported")
+        if not basic_login:
+            basic_login = self.basic_login
+        if not basic_passwd:
+            basic_passwd = self.basic_passwd
+
+        if basic_login is not None and basic_passwd is not None:
+            self.headers['AUTHORIZATION'] = 'Basic %s' % (
+                base64.b64encode(
+                    ('%s:%s' % (basic_login, basic_passwd)).encode('latin1'))
+                .strip().decode('latin1'))
+        elif basic_login is not None or basic_passwd is not None:
+            raise ValueError("HTTP Auth login of password is missing")
 
     def update_body_from_data(self, data):
         if (hasattr(data, '__iter__') and not isinstance(
@@ -855,7 +876,8 @@ def request(self, method=None, path=None, *,
                 headers=None,
                 cookies=None,
                 files=None,
-                auth=None,
+                basic_login=None,
+                basic_passwd=None,
                 allow_redirects=True,
                 max_redirects=10,
                 encoding='utf-8',
@@ -891,8 +913,9 @@ def request(self, method=None, path=None, *,
             try:
                 resp = yield from request(
                     method, url, params=params, data=data, headers=headers,
-                    cookies=cookies, files=files, auth=auth,
-                    encoding=encoding, allow_redirects=allow_redirects,
+                    cookies=cookies, files=files, basic_login=basic_login,
+                    basic_passwd=basic_passwd, encoding=encoding,
+                    allow_redirects=allow_redirects,
                     version=version, max_redirects=max_redirects,
                     compress=compress, chunked=chunked,
                     expect100=expect100, read_until_eof=read_until_eof,
diff --git a/aiohttp/connector.py b/aiohttp/connector.py
--- a/aiohttp/connector.py
+++ b/aiohttp/connector.py
@@ -254,9 +254,12 @@ def _create_connection(self, req, **kwargs):
 class ProxyConnector(TCPConnector):
     """Http Proxy connector."""
 
-    def __init__(self, proxy, *args, **kwargs):
+    def __init__(self, proxy, proxy_login=None, proxy_passwd=None,
+                 *args, **kwargs):
         super().__init__(*args, **kwargs)
         self._proxy = proxy
+        self._basic_login = proxy_login
+        self._basic_passwd = proxy_passwd
         assert proxy.startswith('http://'), (
             "Only http proxy supported", proxy)
 
@@ -268,6 +271,8 @@ def proxy(self):
     def _create_connection(self, req, **kwargs):
         proxy_req = ClientRequest('GET', self._proxy,
                                   headers={'Host': req.host},
+                                  basic_login=self._basic_login,
+                                  basic_passwd=self._basic_passwd,
                                   loop=self._loop)
         try:
             transport, proto = yield from super()._create_connection(proxy_req)
@@ -276,11 +281,10 @@ def _create_connection(self, req, **kwargs):
         req.path = '{scheme}://{host}{path}'.format(scheme=req.scheme,
                                                     host=req.host,
                                                     path=req.path)
-        if proxy_req.auth:
+        if 'AUTHORIZATION' in proxy_req.headers:
             auth = proxy_req.headers['AUTHORIZATION']
             del proxy_req.headers['AUTHORIZATION']
             req.headers['PROXY-AUTHORIZATION'] = auth
-            proxy_req.headers['PROXY-AUTHORIZATION'] = auth
 
         if req.ssl:
             # For HTTPS requests over HTTP proxy


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -205,7 +205,9 @@ def test_no_path(self):
         self.assertEqual('/', req.path)
 
     def test_basic_auth(self):
-        req = ClientRequest('get', 'http://python.org', auth=('nkim', '1234'))
+        req = ClientRequest('get', 'http://python.org',
+                            basic_login='nkim',
+                            basic_passwd='1234')
         self.assertIn('AUTHORIZATION', req.headers)
         self.assertEqual('Basic bmtpbToxMjM0', req.headers['AUTHORIZATION'])
 
@@ -214,19 +216,17 @@ def test_basic_auth_from_url(self):
         self.assertIn('AUTHORIZATION', req.headers)
         self.assertEqual('Basic bmtpbToxMjM0', req.headers['AUTHORIZATION'])
 
-        req = ClientRequest('get', 'http://nkim@python.org')
-        self.assertIn('AUTHORIZATION', req.headers)
-        self.assertEqual('Basic bmtpbTo=', req.headers['AUTHORIZATION'])
-
         req = ClientRequest(
-            'get', 'http://nkim@python.org', auth=('nkim', '1234'))
+            'get', 'http://nkim@python.org',
+            basic_login='nkim', basic_passwd='1234')
         self.assertIn('AUTHORIZATION', req.headers)
         self.assertEqual('Basic bmtpbToxMjM0', req.headers['AUTHORIZATION'])
 
     def test_basic_auth_err(self):
+        # missing password here
         self.assertRaises(
             ValueError, ClientRequest,
-            'get', 'http://python.org', auth=(1, 2, 3))
+            'get', 'http://python.org', basic_login='nkim')
 
     def test_no_content_length(self):
         req = ClientRequest('get', 'http://python.org', loop=self.loop)
diff --git a/tests/test_connector.py b/tests/test_connector.py
--- a/tests/test_connector.py
+++ b/tests/test_connector.py
@@ -339,6 +339,7 @@ def test_connect(self, ClientRequestMock):
 
         ClientRequestMock.assert_called_with(
             'GET', 'http://proxy.example.com',
+            basic_login=None, basic_passwd=None,
             headers={'Host': 'www.python.org'},
             loop=loop_mock)
 
@@ -357,6 +358,35 @@ def test_proxy_connection_error(self):
 
     @unittest.mock.patch('aiohttp.connector.ClientRequest')
     def test_auth(self, ClientRequestMock):
+        proxy_req = ClientRequest('GET', 'http://proxy.example.com',
+                                  basic_login='user', basic_passwd='pass')
+        ClientRequestMock.return_value = proxy_req
+        self.assertIn('AUTHORIZATION', proxy_req.headers)
+        self.assertNotIn('PROXY-AUTHORIZATION', proxy_req.headers)
+
+        loop_mock = unittest.mock.Mock()
+        connector = aiohttp.ProxyConnector(
+            'http://proxy.example.com', loop=loop_mock,
+            proxy_login='user', proxy_passwd='pass')
+        connector._resolve_host = resolve_mock = unittest.mock.Mock()
+        self._fake_coroutine(resolve_mock, [unittest.mock.MagicMock()])
+
+        tr, proto = unittest.mock.Mock(), unittest.mock.Mock()
+        self._fake_coroutine(loop_mock.create_connection, (tr, proto))
+
+        req = ClientRequest('GET', 'http://www.python.org')
+        self.assertNotIn('AUTHORIZATION', req.headers)
+        self.assertNotIn('PROXY-AUTHORIZATION', req.headers)
+        self.loop.run_until_complete(connector.connect(req))
+
+        self.assertEqual(req.path, 'http://www.python.org/')
+        self.assertNotIn('AUTHORIZATION', req.headers)
+        self.assertIn('PROXY-AUTHORIZATION', req.headers)
+        self.assertNotIn('AUTHORIZATION', proxy_req.headers)
+        self.assertNotIn('PROXY-AUTHORIZATION', proxy_req.headers)
+
+    @unittest.mock.patch('aiohttp.connector.ClientRequest')
+    def test_auth_from_url(self, ClientRequestMock):
         proxy_req = ClientRequest('GET', 'http://user:pass@proxy.example.com')
         ClientRequestMock.return_value = proxy_req
         self.assertIn('AUTHORIZATION', proxy_req.headers)
@@ -380,7 +410,7 @@ def test_auth(self, ClientRequestMock):
         self.assertNotIn('AUTHORIZATION', req.headers)
         self.assertIn('PROXY-AUTHORIZATION', req.headers)
         self.assertNotIn('AUTHORIZATION', proxy_req.headers)
-        self.assertIn('PROXY-AUTHORIZATION', proxy_req.headers)
+        self.assertNotIn('PROXY-AUTHORIZATION', proxy_req.headers)
 
     @unittest.mock.patch('aiohttp.connector.ClientRequest')
     def test_auth__not_modifying_request(self, ClientRequestMock):