From 2543814ffeadf6bb52d902f42d4b6420b50fb5e8 Mon Sep 17 00:00:00 2001 From: can Date: Wed, 31 Jul 2019 21:03:43 +0300 Subject: [PATCH] added netrc support #177 --- httpx/api.py | 16 +++++++++++++ httpx/client.py | 48 +++++++++++++++++++++++++++++++++++++-- httpx/utils.py | 11 +++++++++ tests/.netrc | 3 +++ tests/client/test_auth.py | 25 ++++++++++++++++++++ tests/test_utils.py | 23 ++++++++++++++++++- 6 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 tests/.netrc diff --git a/httpx/api.py b/httpx/api.py index 99d60128ab..be3390e430 100644 --- a/httpx/api.py +++ b/httpx/api.py @@ -32,6 +32,7 @@ def request( cert: CertTypes = None, verify: VerifyTypes = True, stream: bool = False, + trust_env: bool = True, ) -> Response: with Client() as client: return client.request( @@ -49,6 +50,7 @@ def request( cert=cert, verify=verify, timeout=timeout, + trust_env=trust_env, ) @@ -64,6 +66,7 @@ def get( cert: CertTypes = None, verify: VerifyTypes = True, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> Response: return request( "GET", @@ -77,6 +80,7 @@ def get( cert=cert, verify=verify, timeout=timeout, + trust_env=trust_env, ) @@ -92,6 +96,7 @@ def options( cert: CertTypes = None, verify: VerifyTypes = True, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> Response: return request( "OPTIONS", @@ -105,6 +110,7 @@ def options( cert=cert, verify=verify, timeout=timeout, + trust_env=trust_env, ) @@ -120,6 +126,7 @@ def head( cert: CertTypes = None, verify: VerifyTypes = True, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> Response: return request( "HEAD", @@ -133,6 +140,7 @@ def head( cert=cert, verify=verify, timeout=timeout, + trust_env=trust_env, ) @@ -151,6 +159,7 @@ def post( cert: CertTypes = None, verify: VerifyTypes = True, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> Response: return request( "POST", @@ -167,6 +176,7 @@ def post( cert=cert, verify=verify, timeout=timeout, + trust_env=trust_env, ) @@ -185,6 +195,7 @@ def put( cert: CertTypes = None, verify: VerifyTypes = True, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> Response: return request( "PUT", @@ -201,6 +212,7 @@ def put( cert=cert, verify=verify, timeout=timeout, + trust_env=trust_env, ) @@ -219,6 +231,7 @@ def patch( cert: CertTypes = None, verify: VerifyTypes = True, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> Response: return request( "PATCH", @@ -235,6 +248,7 @@ def patch( cert=cert, verify=verify, timeout=timeout, + trust_env=trust_env, ) @@ -253,6 +267,7 @@ def delete( cert: CertTypes = None, verify: VerifyTypes = True, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> Response: return request( "DELETE", @@ -269,4 +284,5 @@ def delete( cert=cert, verify=verify, timeout=timeout, + trust_env=trust_env, ) diff --git a/httpx/client.py b/httpx/client.py index 60088c2657..46be340ccf 100644 --- a/httpx/client.py +++ b/httpx/client.py @@ -43,6 +43,7 @@ URLTypes, ) from .status_codes import codes +from .utils import get_netrc_login class BaseClient: @@ -61,6 +62,7 @@ def __init__( app: typing.Callable = None, raise_app_exceptions: bool = True, backend: ConcurrencyBackend = None, + trust_env: bool = True, ): if backend is None: backend = AsyncioBackend() @@ -101,6 +103,7 @@ def __init__( self.max_redirects = max_redirects self.dispatch = async_dispatch self.concurrency_backend = backend + self.trust_env = trust_env def merge_cookies( self, cookies: CookieTypes = None @@ -130,6 +133,7 @@ async def send( verify: VerifyTypes = None, cert: CertTypes = None, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> AsyncResponse: if auth is None: auth = self.auth @@ -139,8 +143,16 @@ async def send( if url.scheme not in ("http", "https"): raise InvalidURL('URL scheme must be "http" or "https".') - if auth is None and (url.username or url.password): - auth = HTTPBasicAuth(username=url.username, password=url.password) + if auth is None: + if url.username or url.password: + auth = HTTPBasicAuth(username=url.username, password=url.password) + elif trust_env: + netrc_login = get_netrc_login(url.authority) + if netrc_login: + netrc_username, _, netrc_password = netrc_login + auth = HTTPBasicAuth( + username=netrc_username, password=netrc_password + ) if auth is not None: if isinstance(auth, tuple): @@ -312,6 +324,7 @@ async def get( cert: CertTypes = None, verify: VerifyTypes = None, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> AsyncResponse: return await self.request( "GET", @@ -325,6 +338,7 @@ async def get( verify=verify, cert=cert, timeout=timeout, + trust_env=trust_env, ) async def options( @@ -340,6 +354,7 @@ async def options( cert: CertTypes = None, verify: VerifyTypes = None, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> AsyncResponse: return await self.request( "OPTIONS", @@ -353,6 +368,7 @@ async def options( verify=verify, cert=cert, timeout=timeout, + trust_env=trust_env, ) async def head( @@ -368,6 +384,7 @@ async def head( cert: CertTypes = None, verify: VerifyTypes = None, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> AsyncResponse: return await self.request( "HEAD", @@ -381,6 +398,7 @@ async def head( verify=verify, cert=cert, timeout=timeout, + trust_env=trust_env, ) async def post( @@ -399,6 +417,7 @@ async def post( cert: CertTypes = None, verify: VerifyTypes = None, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> AsyncResponse: return await self.request( "POST", @@ -415,6 +434,7 @@ async def post( verify=verify, cert=cert, timeout=timeout, + trust_env=trust_env, ) async def put( @@ -433,6 +453,7 @@ async def put( cert: CertTypes = None, verify: VerifyTypes = None, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> AsyncResponse: return await self.request( "PUT", @@ -449,6 +470,7 @@ async def put( verify=verify, cert=cert, timeout=timeout, + trust_env=trust_env, ) async def patch( @@ -467,6 +489,7 @@ async def patch( cert: CertTypes = None, verify: VerifyTypes = None, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> AsyncResponse: return await self.request( "PATCH", @@ -483,6 +506,7 @@ async def patch( verify=verify, cert=cert, timeout=timeout, + trust_env=trust_env, ) async def delete( @@ -501,6 +525,7 @@ async def delete( cert: CertTypes = None, verify: VerifyTypes = None, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> AsyncResponse: return await self.request( "DELETE", @@ -517,6 +542,7 @@ async def delete( verify=verify, cert=cert, timeout=timeout, + trust_env=trust_env, ) async def request( @@ -536,6 +562,7 @@ async def request( cert: CertTypes = None, verify: VerifyTypes = None, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> AsyncResponse: url = self.base_url.join(url) headers = self.merge_headers(headers) @@ -558,6 +585,7 @@ async def request( verify=verify, cert=cert, timeout=timeout, + trust_env=trust_env, ) return response @@ -618,6 +646,7 @@ def request( cert: CertTypes = None, verify: VerifyTypes = None, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> Response: url = self.base_url.join(url) headers = self.merge_headers(headers) @@ -643,6 +672,7 @@ def request( "verify": verify, "cert": cert, "timeout": timeout, + "trust_env": trust_env, } async_response = concurrency_backend.run(coroutine, *args, **kwargs) @@ -685,6 +715,7 @@ def get( cert: CertTypes = None, verify: VerifyTypes = None, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> Response: return self.request( "GET", @@ -698,6 +729,7 @@ def get( verify=verify, cert=cert, timeout=timeout, + trust_env=trust_env, ) def options( @@ -713,6 +745,7 @@ def options( cert: CertTypes = None, verify: VerifyTypes = None, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> Response: return self.request( "OPTIONS", @@ -726,6 +759,7 @@ def options( verify=verify, cert=cert, timeout=timeout, + trust_env=trust_env, ) def head( @@ -741,6 +775,7 @@ def head( cert: CertTypes = None, verify: VerifyTypes = None, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> Response: return self.request( "HEAD", @@ -754,6 +789,7 @@ def head( verify=verify, cert=cert, timeout=timeout, + trust_env=trust_env, ) def post( @@ -772,6 +808,7 @@ def post( cert: CertTypes = None, verify: VerifyTypes = None, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> Response: return self.request( "POST", @@ -788,6 +825,7 @@ def post( verify=verify, cert=cert, timeout=timeout, + trust_env=trust_env, ) def put( @@ -806,6 +844,7 @@ def put( cert: CertTypes = None, verify: VerifyTypes = None, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> Response: return self.request( "PUT", @@ -822,6 +861,7 @@ def put( verify=verify, cert=cert, timeout=timeout, + trust_env=trust_env, ) def patch( @@ -840,6 +880,7 @@ def patch( cert: CertTypes = None, verify: VerifyTypes = None, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> Response: return self.request( "PATCH", @@ -856,6 +897,7 @@ def patch( verify=verify, cert=cert, timeout=timeout, + trust_env=trust_env, ) def delete( @@ -874,6 +916,7 @@ def delete( cert: CertTypes = None, verify: VerifyTypes = None, timeout: TimeoutTypes = None, + trust_env: bool = True, ) -> Response: return self.request( "DELETE", @@ -890,6 +933,7 @@ def delete( verify=verify, cert=cert, timeout=timeout, + trust_env=trust_env, ) def close(self) -> None: diff --git a/httpx/utils.py b/httpx/utils.py index 3d0d6607d6..5f9db6cf35 100644 --- a/httpx/utils.py +++ b/httpx/utils.py @@ -1,4 +1,6 @@ import codecs +import netrc +import os import typing @@ -64,3 +66,12 @@ def guess_json_utf(data: bytes) -> typing.Optional[str]: return "utf-32-le" # Did not detect a valid UTF-32 ascii-range character return None + + +def get_netrc_login(host: str) -> typing.Optional[typing.Tuple[str, str, str]]: + try: + netrc_info = netrc.netrc(os.environ.get("NETRC")) # type: ignore + except FileNotFoundError: + return None + + return netrc_info.authenticators(host) # type: ignore diff --git a/tests/.netrc b/tests/.netrc new file mode 100644 index 0000000000..ed65ee7d3d --- /dev/null +++ b/tests/.netrc @@ -0,0 +1,3 @@ +machine netrcexample.org +login example-username +password example-password \ No newline at end of file diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index 44099fbada..1b74307612 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -1,4 +1,5 @@ import json +import os from httpx import ( AsyncDispatcher, @@ -67,3 +68,27 @@ def auth(request): assert response.status_code == 200 assert response.json() == {"auth": "Token 123"} + + +def test_entrc_auth(): + os.environ["NETRC"] = "tests/.netrc" + url = "http://netrcexample.org" + + with Client(dispatch=MockDispatch()) as client: + response = client.get(url) + + assert response.status_code == 200 + assert response.json() == { + "auth": "Basic ZXhhbXBsZS11c2VybmFtZTpleGFtcGxlLXBhc3N3b3Jk" + } + + +def test_trust_env_auth(): + os.environ["NETRC"] = "tests/.netrc" + url = "http://netrcexample.org" + + with Client(dispatch=MockDispatch()) as client: + response = client.get(url, trust_env=False) + + assert response.status_code == 200 + assert response.json() == {"auth": None} diff --git a/tests/test_utils.py b/tests/test_utils.py index b2b167116c..96d186b561 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,8 @@ +import os + import pytest -from httpx.utils import guess_json_utf +from httpx.utils import get_netrc_login, guess_json_utf @pytest.mark.parametrize( @@ -37,3 +39,22 @@ def test_bad_utf_like_encoding(): def test_guess_by_bom(encoding, expected): data = u"\ufeff{}".encode(encoding) assert guess_json_utf(data) == expected + + +def test_bad_get_netrc_login(): + assert get_netrc_login("url") is None + + os.environ["NETRC"] = "tests/.netrc" + assert get_netrc_login("url") is None + + os.environ["NETRC"] = "wrongpath" + assert get_netrc_login("url") is None + + +def test_get_netrc_login(): + os.environ["NETRC"] = "tests/.netrc" + assert get_netrc_login("netrcexample.org") == ( + "example-username", + None, + "example-password", + )