## .netrc Support
-HTTPX supports .netrc file. In `trust_env=True` cases, if auth parameter is
-not defined, HTTPX tries to add auth into request's header from .netrc file.
+HTTPX can be configured to use [a `.netrc` config file](https://everything.curl.dev/usingcurl/netrc) for authentication.
-!!! note
- The NETRC file is cached across requests made by a client.
- If you need to refresh the cache (e.g. because the NETRC file has changed),
- you should create a new client or restart the interpreter.
+The `.netrc` config file allows authentication credentials to be associated with specified hosts. When a request is made to a host that is found in the netrc file, the username and password will be included using HTTP basic auth.
-As default `trust_env` is true. To set false:
-```pycon
->>> httpx.get('https://example.org/', trust_env=False)
+Example `.netrc` file:
+
+```
+machine example.org
+login example-username
+password example-password
+
+machine python-httpx.org
+login other-username
+password other-password
```
-If `NETRC` environment is empty, HTTPX tries to use default files.
-(`~/.netrc`, `~/_netrc`)
+Some examples of configuring `.netrc` authentication with `httpx`.
+
+Use the default `.netrc` file in the users home directory:
-To change `NETRC` environment:
```pycon
->>> import os
->>> os.environ["NETRC"] = "my_default_folder/.my_netrc"
+>>> auth = httpx.NetRCAuth()
+>>> client = httpx.Client(auth=auth)
```
-.netrc file content example:
-```
-machine netrcexample.org
-login example-username
-password example-password
+Use an explicit path to a `.netrc` file:
-...
+```pycon
+>>> auth = httpx.NetRCAuth(file="/path/to/.netrc")
+>>> client = httpx.Client(auth=auth)
```
-When using `Client` instances, `trust_env` should be set on the client itself, rather than on the request methods:
+Use the `NETRC` environment variable to configure a path to the `.netrc` file,
+or fallback to the default.
-```python
-client = httpx.Client(trust_env=False)
+```pycon
+>>> auth = httpx.NetRCAuth(file=os.environ.get("NETRC"))
+>>> client = httpx.Client(auth=auth)
```
+The `NetRCAuth()` class uses [the `netrc.netrc()` function from the Python standard library](https://docs.python.org/3/library/netrc.html). See the documentation there for more details on exceptions that may be raised if the netrc file is not found, or cannot be parsed.
+
## HTTP Proxying
HTTPX supports setting up [HTTP proxies](https://en.wikipedia.org/wiki/Proxy_server#Web_proxy_servers) via the `proxies` parameter to be passed on client initialization or top-level API functions like `httpx.get(..., proxies=...)`.
When issuing requests or instantiating a client, the `auth` argument can be used to pass an authentication scheme to use. The `auth` argument may be one of the following...
* A two-tuple of `username`/`password`, to be used with basic authentication.
-* An instance of `httpx.BasicAuth()` or `httpx.DigestAuth()`.
+* An instance of `httpx.BasicAuth()`, `httpx.DigestAuth()`, or `httpx.NetRCAuth()`.
* A callable, accepting a request and returning an authenticated request instance.
* An instance of subclasses of `httpx.Auth`.
SSL_CERT_DIR=/path/to/ca-certs/ python -c "import httpx; httpx.get('https://example.com')"
```
-## `NETRC`
-
-Valid values: a filename
-
-If this environment variable is set but auth parameter is not defined, HTTPX will add auth information stored in the .netrc file into the request's header. If you do not provide NETRC environment either, HTTPX will use default files. (~/.netrc, ~/_netrc)
-
-Example:
-
-```console
-NETRC=/path/to/netrcfile/.my_netrc python -c "import httpx; httpx.get('https://example.com')"
-```
-
## Proxies
The environment variables documented below are used as a convention by various HTTP tooling, including:
python -c "import httpx; httpx.get('http://127.0.0.1:5000/my-api')"
python -c "import httpx; httpx.get('https://www.python-httpx.org')"
```
-
from .__version__ import __description__, __title__, __version__
from ._api import delete, get, head, options, patch, post, put, request, stream
-from ._auth import Auth, BasicAuth, DigestAuth
+from ._auth import Auth, BasicAuth, DigestAuth, NetRCAuth
from ._client import USE_CLIENT_DEFAULT, AsyncClient, Client
from ._config import Limits, Proxy, Timeout, create_ssl_context
from ._content import ByteStream
"LocalProtocolError",
"main",
"MockTransport",
+ "NetRCAuth",
"NetworkError",
"options",
"patch",
import hashlib
+import netrc
import os
import re
import time
return f"Basic {token}"
+class NetRCAuth(Auth):
+ """
+ Use a 'netrc' file to lookup basic auth credentials based on the url host.
+ """
+
+ def __init__(self, file: typing.Optional[str]):
+ self._netrc_info = netrc.netrc(file)
+
+ def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
+ auth_info = self._netrc_info.authenticators(request.url.host)
+ if auth_info is None or not auth_info[2]:
+ # The netrc file did not have authentication credentials for this host.
+ yield request
+ else:
+ # Build a basic auth header with credentials from the netrc file.
+ request.headers["Authorization"] = self._build_auth_header(
+ username=auth_info[0], password=auth_info[2]
+ )
+ yield request
+
+ def _build_auth_header(
+ self, username: typing.Union[str, bytes], password: typing.Union[str, bytes]
+ ) -> str:
+ userpass = b":".join((to_bytes(username), to_bytes(password)))
+ token = b64encode(userpass).decode()
+ return f"Basic {token}"
+
+
class DigestAuth(Auth):
_ALGORITHM_TO_HASH_FUNCTION: typing.Dict[str, typing.Callable[[bytes], "_Hash"]] = {
"MD5": hashlib.md5,
)
from ._urls import URL, QueryParams
from ._utils import (
- NetRCInfo,
Timer,
URLPattern,
get_environment_proxies,
}
self._trust_env = trust_env
self._default_encoding = default_encoding
- self._netrc = NetRCInfo()
self._state = ClientState.UNOPENED
@property
if username or password:
return BasicAuth(username=username, password=password)
- if self.trust_env and "Authorization" not in request.headers:
- credentials = self._netrc.get_credentials(request.url.host)
- if credentials is not None:
- return BasicAuth(username=credentials[0], password=credentials[1])
-
return Auth()
def _build_redirect_request(self, request: Request, response: Response) -> Request:
import email.message
import logging
import mimetypes
-import netrc
import os
import re
import sys
return None
-class NetRCInfo:
- def __init__(self, files: typing.Optional[typing.List[str]] = None) -> None:
- if files is None:
- files = [os.getenv("NETRC", ""), "~/.netrc", "~/_netrc"]
- self.netrc_files = files
-
- @property
- def netrc_info(self) -> typing.Optional[netrc.netrc]:
- if not hasattr(self, "_netrc_info"):
- self._netrc_info = None
- for file_path in self.netrc_files:
- expanded_path = Path(file_path).expanduser()
- try:
- if expanded_path.is_file():
- self._netrc_info = netrc.netrc(str(expanded_path))
- break
- except (netrc.NetrcParseError, IOError): # pragma: no cover
- # Issue while reading the netrc file, ignore...
- pass
- return self._netrc_info
-
- def get_credentials(self, host: str) -> typing.Optional[typing.Tuple[str, str]]:
- if self.netrc_info is None:
- return None
-
- auth_info = self.netrc_info.authenticators(host)
- if auth_info is None or auth_info[2] is None:
- return None
- return (auth_info[0], auth_info[2])
-
-
def get_ca_bundle_from_env() -> typing.Optional[str]:
if "SSL_CERT_FILE" in os.environ:
ssl_file = Path(os.environ["SSL_CERT_FILE"])
Unit tests for auth classes also exist in tests/test_auth.py
"""
import hashlib
+import netrc
import os
+import sys
import threading
import typing
from urllib.request import parse_keqv_list
class App:
+ """
+ A mock app to test auth credentials.
+ """
+
def __init__(self, auth_header: str = "", status_code: int = 200) -> None:
self.auth_header = auth_header
self.status_code = status_code
assert response.json() == {"auth": "Token 123"}
-@pytest.mark.anyio
-async def test_netrc_auth() -> None:
- os.environ["NETRC"] = str(FIXTURES_DIR / ".netrc")
+def test_netrc_auth_credentials_exist() -> None:
+ """
+ When netrc auth is being used and a request is made to a host that is
+ in the netrc file, then the relevant credentials should be applied.
+ """
+ netrc_file = str(FIXTURES_DIR / ".netrc")
url = "http://netrcexample.org"
app = App()
+ auth = httpx.NetRCAuth(netrc_file)
- async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client:
- response = await client.get(url)
+ with httpx.Client(transport=httpx.MockTransport(app), auth=auth) as client:
+ response = client.get(url)
assert response.status_code == 200
assert response.json() == {
}
-@pytest.mark.anyio
-async def test_auth_header_has_priority_over_netrc() -> None:
- os.environ["NETRC"] = str(FIXTURES_DIR / ".netrc")
- url = "http://netrcexample.org"
+def test_netrc_auth_credentials_do_not_exist() -> None:
+ """
+ When netrc auth is being used and a request is made to a host that is
+ not in the netrc file, then no credentials should be applied.
+ """
+ netrc_file = str(FIXTURES_DIR / ".netrc")
+ url = "http://example.org"
app = App()
+ auth = httpx.NetRCAuth(netrc_file)
- async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client:
- response = await client.get(url, headers={"Authorization": "Override"})
+ with httpx.Client(transport=httpx.MockTransport(app), auth=auth) as client:
+ response = client.get(url)
assert response.status_code == 200
- assert response.json() == {"auth": "Override"}
+ assert response.json() == {"auth": None}
-@pytest.mark.anyio
-async def test_trust_env_auth() -> None:
- os.environ["NETRC"] = str(FIXTURES_DIR / ".netrc")
- url = "http://netrcexample.org"
+@pytest.mark.skipif(
+ sys.version_info < (3, 11),
+ reason="netrc files without a password are invalid with Python < 3.11",
+)
+def test_netrc_auth_nopassword() -> None: # pragma: no cover
+ """
+ Python has different netrc parsing behaviours with different versions.
+ For Python 3.11+ a netrc file with no password is valid. In this case
+ we want to check that we allow the netrc auth, and simply don't provide
+ any credentials in the request.
+ """
+ netrc_file = str(FIXTURES_DIR / ".netrc-nopassword")
+ url = "http://example.org"
app = App()
+ auth = httpx.NetRCAuth(netrc_file)
- async with httpx.AsyncClient(
- transport=httpx.MockTransport(app), trust_env=False
- ) as client:
- response = await client.get(url)
+ with httpx.Client(transport=httpx.MockTransport(app), auth=auth) as client:
+ response = client.get(url)
assert response.status_code == 200
assert response.json() == {"auth": None}
- async with httpx.AsyncClient(
- transport=httpx.MockTransport(app), trust_env=True
- ) as client:
- response = await client.get(url)
- assert response.status_code == 200
- assert response.json() == {
- "auth": "Basic ZXhhbXBsZS11c2VybmFtZTpleGFtcGxlLXBhc3N3b3Jk"
- }
+@pytest.mark.skipif(
+ sys.version_info >= (3, 11),
+ reason="netrc files without a password are valid from Python >= 3.11",
+)
+def test_netrc_auth_nopassword_parse_error() -> None: # pragma: no cover
+ """
+ Python has different netrc parsing behaviours with different versions.
+ For Python < 3.11 a netrc file with no password is invalid. In this case
+ we want to allow the parse error to be raised.
+ """
+ netrc_file = str(FIXTURES_DIR / ".netrc-nopassword")
+ with pytest.raises(netrc.NetrcParseError):
+ httpx.NetRCAuth(netrc_file)
@pytest.mark.anyio
client = httpx.Client()
client.event_hooks = {"request": [on_request]}
assert client.event_hooks == {"request": [on_request], "response": []}
+
+
+def test_client_trust_env():
+ client = httpx.Client()
+ assert client.trust_env
+
+ client = httpx.Client(trust_env=False)
+ assert not client.trust_env
--- /dev/null
+machine netrcexample.org
+login example-username
import httpx
from httpx._utils import (
- NetRCInfo,
URLPattern,
get_ca_bundle_from_env,
get_environment_proxies,
)
from tests.utils import override_log_level
-from .common import FIXTURES_DIR, TESTS_DIR
+from .common import TESTS_DIR
@pytest.mark.parametrize(
assert guess_json_utf(data) == expected
-def test_bad_get_netrc_login():
- netrc_info = NetRCInfo([str(FIXTURES_DIR / "does-not-exist")])
- assert netrc_info.get_credentials("netrcexample.org") is None
-
-
-def test_get_netrc_login():
- netrc_info = NetRCInfo([str(FIXTURES_DIR / ".netrc")])
- expected_credentials = (
- "example-username",
- "example-password",
- )
- assert netrc_info.get_credentials("netrcexample.org") == expected_credentials
-
-
-def test_get_netrc_unknown():
- netrc_info = NetRCInfo([str(FIXTURES_DIR / ".netrc")])
- assert netrc_info.get_credentials("nonexistent.org") is None
-
-
@pytest.mark.parametrize(
"value, expected",
(