Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support using netrc for non-proxy HTTP credentials #7131

Merged
merged 51 commits into from
Mar 17, 2023
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
2ee2464
Read HTTP Basic auth from netrc if no explict auth is passed
yuvipanda Dec 16, 2022
2c8e709
Read from .netrc only when trust_env is passed
yuvipanda Dec 16, 2022
d26d261
Deduplicate creating basicauth from netrc
yuvipanda Dec 17, 2022
f59b4c3
Remove possibly unneeded cast
yuvipanda Dec 17, 2022
3329055
Cleanup how username & password are determined
yuvipanda Dec 17, 2022
dc7833f
Reflow boolean checks to be clearer
yuvipanda Dec 19, 2022
81237e2
Cleanup netrc parsing code
yuvipanda Dec 19, 2022
11e0969
Add tests for netrc helpers
yuvipanda Dec 19, 2022
b691694
Add test for client_req
yuvipanda Dec 19, 2022
cadbbd3
Adding self to CONTRIBUTORS
yuvipanda Dec 19, 2022
fc57968
Add documentation for getting HTTP Basic Auth creds from netrc file
yuvipanda Dec 19, 2022
9bfc648
Add .feature file
yuvipanda Dec 19, 2022
fd362af
Set versionchanged string to 3.9 for HTTP Basic Auth netrc support
yuvipanda Dec 19, 2022
1cbf9ab
Reword changes file
yuvipanda Dec 19, 2022
a8682f1
Use built in tmp_path pytest fixture
yuvipanda Dec 19, 2022
14be170
Use pytest monkeypatch.setenv
yuvipanda Dec 19, 2022
3847daf
Cleanup tests to use better fixtures
yuvipanda Dec 21, 2022
5074c98
Fix comment typo
yuvipanda Dec 21, 2022
e3cbf59
Use exceptions for control flow
yuvipanda Dec 21, 2022
bfba449
Don't touch netrc if self.url.host is not None
yuvipanda Dec 23, 2022
7f60553
Merge pull request #2 from yuvipanda/exceptional
yuvipanda Dec 23, 2022
993675d
Encode information about conditional into variable name
yuvipanda Dec 23, 2022
8330097
Return netrc path from fixture
yuvipanda Jan 7, 2023
19dabd3
Use better rST syntax
yuvipanda Jan 7, 2023
8aad3d0
Simplify parametrize call
yuvipanda Jan 7, 2023
b556229
Simplify parametrize call
yuvipanda Jan 7, 2023
6ff6ffc
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 7, 2023
4722d95
Use a label for boolean condition
yuvipanda Jan 7, 2023
653b93b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 7, 2023
4eadd65
Don't write out netrc file in fixture if not passed in
yuvipanda Jan 7, 2023
5f1111a
Remove spurious re-import of pathlib
yuvipanda Jan 7, 2023
e374b90
Use more rST
yuvipanda Jan 17, 2023
37561cf
Add another rST directive
yuvipanda Jan 17, 2023
e1c7616
Cleanup some unused fixtures
yuvipanda Jan 17, 2023
22df825
Use match= for pytest.raises
yuvipanda Jan 17, 2023
8a2f62a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 17, 2023
6932234
Use match= for pytest.raises *correctly*
yuvipanda Jan 17, 2023
273c049
Remove unused netrc_contents
yuvipanda Jan 17, 2023
8a8f40b
Hardcode "example.com" as hostname
yuvipanda Jan 17, 2023
9b16ad9
Cleanup client_request test
yuvipanda Jan 17, 2023
1d53474
Rename test case
yuvipanda Jan 17, 2023
d7ca088
Split up another test
yuvipanda Jan 17, 2023
deb4cf7
Split up another test
yuvipanda Jan 17, 2023
1a72bfb
Add glossary entry
yuvipanda Jan 17, 2023
a9fc639
Add section for environment variables
yuvipanda Jan 17, 2023
f8c6562
Fix line length 'error'
yuvipanda Jan 17, 2023
4a6f050
Account for netrc_obj being optionally None
yuvipanda Jan 17, 2023
524c04d
Don't use intermediary bools for None checks
yuvipanda Jan 17, 2023
53658b0
Merge pull request #3 from yuvipanda/no-intermediaries
yuvipanda Jan 17, 2023
1e40665
Update docs/glossary.rst
Dreamsorcerer Feb 3, 2023
38a86d9
Fix typo
yuvipanda Feb 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/7131.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for using Basic Auth credentials from :file:`.netrc` file when making HTTP requests with the :py:class:`~aiohttp.ClientSession` ``trust_env`` argument is set to ``True`` -- by :user:`yuvipanda`.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ Yury Pliner
Yury Selivanov
Yusuke Tsutsumi
Yuval Ofir
Yuvi Panda
Zainab Lawal
Zeal Wierslee
Zlatan Sičanica
Expand Down
1 change: 1 addition & 0 deletions aiohttp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ async def _request(
ssl=ssl,
proxy_headers=proxy_headers,
traces=traces,
trust_env=self.trust_env,
)

# connection timeout
Expand Down
12 changes: 10 additions & 2 deletions aiohttp/client_reqrep.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import codecs
import contextlib
import dataclasses
import functools
import io
Expand Down Expand Up @@ -44,7 +45,9 @@
BasicAuth,
HeadersMixin,
TimerNoop,
basicauth_from_netrc,
is_expected_content_type,
netrc_from_env,
noop,
parse_mimetype,
reify,
Expand Down Expand Up @@ -210,6 +213,7 @@ def __init__(
ssl: Union[SSLContext, bool, Fingerprint, None] = None,
proxy_headers: Optional[LooseHeaders] = None,
traces: Optional[List["Trace"]] = None,
trust_env: bool = False,
):
match = _CONTAINS_CONTROL_CHAR_RE.search(method)
if match:
Expand Down Expand Up @@ -251,7 +255,7 @@ def __init__(
self.update_auto_headers(skip_auto_headers)
self.update_cookies(cookies)
self.update_content_encoding(data)
self.update_auth(auth)
self.update_auth(auth, trust_env)
self.update_proxy(proxy, proxy_auth, proxy_headers)

self.update_body_from_data(data)
Expand Down Expand Up @@ -428,10 +432,14 @@ def update_transfer_encoding(self) -> None:
if hdrs.CONTENT_LENGTH not in self.headers:
self.headers[hdrs.CONTENT_LENGTH] = str(len(self.body))

def update_auth(self, auth: Optional[BasicAuth]) -> None:
def update_auth(self, auth: Optional[BasicAuth], trust_env: bool = False) -> None:
"""Set basic auth."""
if auth is None:
auth = self.auth
if auth is None and trust_env and self.url.host is not None:
webknjaz marked this conversation as resolved.
Show resolved Hide resolved
netrc_obj = netrc_from_env()
with contextlib.suppress(LookupError):
auth = basicauth_from_netrc(netrc_obj, self.url.host)
if auth is None:
return

Expand Down
43 changes: 33 additions & 10 deletions aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
Type,
TypeVar,
Union,
cast,
overload,
)
from urllib.parse import quote
Expand Down Expand Up @@ -244,6 +243,35 @@ class ProxyInfo:
proxy_auth: Optional[BasicAuth]


def basicauth_from_netrc(netrc_obj: Optional[netrc.netrc], host: str) -> BasicAuth:
"""
Return :py:class:`~aiohttp.BasicAuth` credentials for ``host`` from ``netrc_obj``.

:raises LookupError: if ``netrc_obj`` is :py:data:`None` or if no
entry is found for the ``host``.
"""
if netrc_obj is None:
raise LookupError("No .netrc file found")
auth_from_netrc = netrc_obj.authenticators(host)
yuvipanda marked this conversation as resolved.
Show resolved Hide resolved

if auth_from_netrc is None:
raise LookupError(f"No entry for {host!s} found in the `.netrc` file.")
login, account, password = auth_from_netrc

# TODO(PY311): username = login or account
# Up to python 3.10, account could be None if not specified,
# and login will be empty string if not specified. From 3.11,
# login and account will be empty string if not specified.
username = login if (login or account is None) else account

# TODO(PY311): Remove this, as password will be empty string
# if not specified
if password is None:
password = ""

return BasicAuth(username, password)

Dreamsorcerer marked this conversation as resolved.
Show resolved Hide resolved

def proxies_from_env() -> Dict[str, ProxyInfo]:
proxy_urls = {
k: URL(v)
Expand All @@ -261,16 +289,11 @@ def proxies_from_env() -> Dict[str, ProxyInfo]:
)
continue
if netrc_obj and auth is None:
auth_from_netrc = None
if proxy.host is not None:
auth_from_netrc = netrc_obj.authenticators(proxy.host)
if auth_from_netrc is not None:
# auth_from_netrc is a (`user`, `account`, `password`) tuple,
# `user` and `account` both can be username,
# if `user` is None, use `account`
*logins, password = auth_from_netrc
login = logins[0] if logins[0] else logins[-1]
auth = BasicAuth(cast(str, login), cast(str, password))
try:
auth = basicauth_from_netrc(netrc_obj, proxy.host)
except LookupError:
auth = None
ret[proto] = ProxyInfo(proxy, auth)
return ret

Expand Down
9 changes: 9 additions & 0 deletions docs/client_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ The client session supports the context manager protocol for self closing.

Get proxy credentials from ``~/.netrc`` file if present.

Get HTTP Basic Auth credentials from :file:`~/.netrc` file if present.

If :envvar:`NETRC` environment variable is set, read from file specified
webknjaz marked this conversation as resolved.
Show resolved Hide resolved
there rather than from :file:`~/.netrc`.

.. seealso::

``.netrc`` documentation: https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html
Expand All @@ -189,6 +194,10 @@ The client session supports the context manager protocol for self closing.

Added support for ``~/.netrc`` file.

.. versionchanged:: 3.9
Dreamsorcerer marked this conversation as resolved.
Show resolved Hide resolved

Added support for reading HTTP Basic Auth credentials from :file:`~/.netrc` file.

:param bool requote_redirect_url: Apply *URL requoting* for redirection URLs if
automatic redirection is enabled (``True`` by
default).
Expand Down
15 changes: 15 additions & 0 deletions docs/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@
It makes communication faster by getting rid of connection
establishment for every request.



nginx

Nginx [engine x] is an HTTP and reverse proxy server, a mail
Expand Down Expand Up @@ -153,3 +155,16 @@
A library for operating with URL objects.

https://pypi.python.org/pypi/yarl


Environment Variables
=====================

.. envvar:: NETRC

If set, HTTP Basic Auth will be read from the file pointed to by this environment variable,
rather than from :file:`~/.netrc`.

.. seealso::

``.netrc`` documentation: https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html
22 changes: 22 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,25 @@ def selector_loop() -> None:
with loop_context(policy.new_event_loop) as _loop:
asyncio.set_event_loop(_loop)
yield _loop


@pytest.fixture
def netrc_contents(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
request: pytest.FixtureRequest,
):
"""
Prepare :file:`.netrc` with given contents.

Monkey-patches :envvar:`NETRC` to point to created file.
"""
netrc_contents = getattr(request, "param", None)

netrc_file_path = tmp_path / ".netrc"
if netrc_contents is not None:
netrc_file_path.write_text(netrc_contents)

monkeypatch.setenv("NETRC", str(netrc_file_path))
yuvipanda marked this conversation as resolved.
Show resolved Hide resolved

return netrc_file_path
52 changes: 50 additions & 2 deletions tests/test_client_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
import pathlib
import zlib
from http.cookies import BaseCookie, Morsel, SimpleCookie
from typing import Any
from typing import Any, Optional
from unittest import mock

import pytest
from multidict import CIMultiDict, CIMultiDictProxy, istr
from yarl import URL

import aiohttp
from aiohttp import BaseConnector, hdrs, payload
from aiohttp import BaseConnector, hdrs, helpers, payload
from aiohttp.client_reqrep import (
ClientRequest,
ClientResponse,
Expand Down Expand Up @@ -1230,3 +1230,51 @@ def test_loose_cookies_types(loop: Any) -> None:
def test_gen_default_accept_encoding(has_brotli: Any, expected: Any) -> None:
with mock.patch("aiohttp.client_reqrep.HAS_BROTLI", has_brotli):
assert _gen_default_accept_encoding() == expected


@pytest.mark.parametrize(
("netrc_contents", "expected_auth"),
[
(
"machine example.com login username password pass\n",
helpers.BasicAuth("username", "pass"),
)
],
indirect=("netrc_contents",),
)
@pytest.mark.usefixtures("netrc_contents")
def test_basicauth_from_netrc_present(
make_request: Any,
expected_auth: Optional[helpers.BasicAuth],
):
"""Test appropriate Authorization header is sent when netrc is not empty."""
req = make_request("get", "http://example.com", trust_env=True)
assert req.headers[hdrs.AUTHORIZATION] == expected_auth.encode()


yuvipanda marked this conversation as resolved.
Show resolved Hide resolved
@pytest.mark.parametrize(
"netrc_contents",
("machine example.com login username password pass\n",),
indirect=("netrc_contents",),
)
@pytest.mark.usefixtures("netrc_contents")
def test_basicauth_from_netrc_present_untrusted_env(
make_request: Any,
):
"""Test no authorization header is sent via netrc if trust_env is False"""
req = make_request("get", "http://example.com", trust_env=False)
assert hdrs.AUTHORIZATION not in req.headers


@pytest.mark.parametrize(
"netrc_contents",
("",),
indirect=("netrc_contents",),
)
@pytest.mark.usefixtures("netrc_contents")
def test_basicauth_from_empty_netrc(
make_request: Any,
):
"""Test that no Authorization header is sent when netrc is empty"""
req = make_request("get", "http://example.com", trust_env=True)
assert hdrs.AUTHORIZATION not in req.headers
63 changes: 63 additions & 0 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -975,3 +975,66 @@ def test_populate_with_cookies():
)
def test_parse_http_date(value, expected):
assert parse_http_date(value) == expected


@pytest.mark.parametrize(
["netrc_contents", "expected_username"],
[
(
"machine example.com login username password pass\n",
"username",
),
],
indirect=("netrc_contents",),
)
@pytest.mark.usefixtures("netrc_contents")
def test_netrc_from_env(expected_username: str):
"""Test that reading netrc files from env works as expected"""
netrc_obj = helpers.netrc_from_env()
assert netrc_obj.authenticators("example.com")[0] == expected_username


@pytest.mark.parametrize(
["netrc_contents", "expected_auth"],
[
(
"machine example.com login username password pass\n",
helpers.BasicAuth("username", "pass"),
),
(
"machine example.com account username password pass\n",
helpers.BasicAuth("username", "pass"),
),
(
"machine example.com password pass\n",
helpers.BasicAuth("", "pass"),
),
],
indirect=("netrc_contents",),
)
@pytest.mark.usefixtures("netrc_contents")
def est_basicauth_present_in_netrc(
yuvipanda marked this conversation as resolved.
Show resolved Hide resolved
expected_auth: helpers.BasicAuth,
):
"""Test that netrc file contents are properly parsed into BasicAuth tuples"""
netrc_obj = helpers.netrc_from_env()
webknjaz marked this conversation as resolved.
Show resolved Hide resolved

assert expected_auth == helpers.basicauth_from_netrc(netrc_obj, "example.com")


@pytest.mark.parametrize(
["netrc_contents"],
[
("",),
],
indirect=("netrc_contents",),
)
@pytest.mark.usefixtures("netrc_contents")
def test_read_basicauth_from_empty_netrc():
"""Test that an error is raised if netrc doesn't have an entry for our host"""
netrc_obj = helpers.netrc_from_env()

with pytest.raises(
LookupError, match="No entry for example.com found in the `.netrc` file."
):
helpers.basicauth_from_netrc(netrc_obj, "example.com")