Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion httpcore/_async/http_proxy.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import ssl
from base64 import b64encode
from typing import List, Mapping, Optional, Sequence, Tuple, Union

from .._exceptions import ProxyError
from .._models import URL, Origin, Request, Response, enforce_headers, enforce_url
from .._models import (
URL,
Origin,
Request,
Response,
enforce_bytes,
enforce_headers,
enforce_url,
)
from .._ssl import default_ssl_context
from .._synchronization import AsyncLock
from .._trace import Trace
Expand Down Expand Up @@ -35,6 +44,11 @@ def merge_headers(
return default_headers + override_headers


def build_auth_header(username: bytes, password: bytes) -> bytes:
userpass = username + b":" + password
return b"Basic " + b64encode(userpass)


class AsyncHTTPProxy(AsyncConnectionPool):
"""
A connection pool that sends requests via an HTTP proxy.
Expand All @@ -43,6 +57,7 @@ class AsyncHTTPProxy(AsyncConnectionPool):
def __init__(
self,
proxy_url: Union[URL, bytes, str],
proxy_auth: Tuple[Union[bytes, str], Union[bytes, str]] = None,
proxy_headers: Union[HeadersAsMapping, HeadersAsSequence] = None,
ssl_context: ssl.SSLContext = None,
max_connections: Optional[int] = 10,
Expand All @@ -61,6 +76,8 @@ def __init__(
Parameters:
proxy_url: The URL to use when connecting to the proxy server.
For example `"http://127.0.0.1:8080/"`.
proxy_auth: Any proxy authentication as a two-tuple of
(username, password). May be either bytes or ascii-only str.
proxy_headers: Any HTTP headers to use for the proxy requests.
For example `{"Proxy-Authorization": "Basic <username>:<password>"}`.
ssl_context: An SSL context to use for verifying connections.
Expand Down Expand Up @@ -102,6 +119,13 @@ def __init__(
self._ssl_context = ssl_context
self._proxy_url = enforce_url(proxy_url, name="proxy_url")
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
if proxy_auth is not None:
username = enforce_bytes(proxy_auth[0], name="proxy_auth")
password = enforce_bytes(proxy_auth[1], name="proxy_auth")
authorization = build_auth_header(username, password)
self._proxy_headers = [
(b"Proxy-Authorization", authorization)
] + self._proxy_headers

def create_connection(self, origin: Origin) -> AsyncConnectionInterface:
if origin.scheme == b"http":
Expand Down
26 changes: 25 additions & 1 deletion httpcore/_sync/http_proxy.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import ssl
from base64 import b64encode
from typing import List, Mapping, Optional, Sequence, Tuple, Union

from .._exceptions import ProxyError
from .._models import URL, Origin, Request, Response, enforce_headers, enforce_url
from .._models import (
URL,
Origin,
Request,
Response,
enforce_bytes,
enforce_headers,
enforce_url,
)
from .._ssl import default_ssl_context
from .._synchronization import Lock
from .._trace import Trace
Expand Down Expand Up @@ -35,6 +44,11 @@ def merge_headers(
return default_headers + override_headers


def build_auth_header(username: bytes, password: bytes) -> bytes:
userpass = username + b":" + password
return b"Basic " + b64encode(userpass)


class HTTPProxy(ConnectionPool):
"""
A connection pool that sends requests via an HTTP proxy.
Expand All @@ -43,6 +57,7 @@ class HTTPProxy(ConnectionPool):
def __init__(
self,
proxy_url: Union[URL, bytes, str],
proxy_auth: Tuple[Union[bytes, str], Union[bytes, str]] = None,
proxy_headers: Union[HeadersAsMapping, HeadersAsSequence] = None,
ssl_context: ssl.SSLContext = None,
max_connections: Optional[int] = 10,
Expand All @@ -61,6 +76,8 @@ def __init__(
Parameters:
proxy_url: The URL to use when connecting to the proxy server.
For example `"http://127.0.0.1:8080/"`.
proxy_auth: Any proxy authentication as a two-tuple of
(username, password). May be either bytes or ascii-only str.
proxy_headers: Any HTTP headers to use for the proxy requests.
For example `{"Proxy-Authorization": "Basic <username>:<password>"}`.
ssl_context: An SSL context to use for verifying connections.
Expand Down Expand Up @@ -102,6 +119,13 @@ def __init__(
self._ssl_context = ssl_context
self._proxy_url = enforce_url(proxy_url, name="proxy_url")
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
if proxy_auth is not None:
username = enforce_bytes(proxy_auth[0], name="proxy_auth")
password = enforce_bytes(proxy_auth[1], name="proxy_auth")
authorization = build_auth_header(username, password)
self._proxy_headers = [
(b"Proxy-Authorization", authorization)
] + self._proxy_headers

def create_connection(self, origin: Origin) -> ConnectionInterface:
if origin.scheme == b"http":
Expand Down
37 changes: 34 additions & 3 deletions tests/_async/test_http_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ async def test_proxy_tunneling():

async with AsyncHTTPProxy(
proxy_url="http://localhost:8080/",
max_connections=10,
network_backend=network_backend,
) as proxy:
# Sending an intial request, which once complete will return to the pool, IDLE.
Expand Down Expand Up @@ -169,7 +168,6 @@ async def test_proxy_tunneling_http2():

async with AsyncHTTPProxy(
proxy_url="http://localhost:8080/",
max_connections=10,
network_backend=network_backend,
http2=True,
) as proxy:
Expand Down Expand Up @@ -219,10 +217,43 @@ async def test_proxy_tunneling_with_403():

async with AsyncHTTPProxy(
proxy_url="http://localhost:8080/",
max_connections=10,
network_backend=network_backend,
) as proxy:
with pytest.raises(ProxyError) as exc_info:
await proxy.request("GET", "https://example.com/")
assert str(exc_info.value) == "403 Permission Denied"
assert not proxy.connections


@pytest.mark.anyio
async def test_proxy_tunneling_with_auth():
"""
Send an authenticated HTTPS request via a proxy.
"""
network_backend = AsyncMockBackend(
[
# The initial response to the proxy CONNECT
b"HTTP/1.1 200 OK\r\n\r\n",
# The actual response from the remote server
b"HTTP/1.1 200 OK\r\n",
b"Content-Type: plain/text\r\n",
b"Content-Length: 13\r\n",
b"\r\n",
b"Hello, world!",
]
)

async with AsyncHTTPProxy(
proxy_url="http://localhost:8080/",
proxy_auth=("username", "password"),
network_backend=network_backend,
) as proxy:
response = await proxy.request("GET", "https://example.com/")
assert response.status == 200
assert response.content == b"Hello, world!"

# Dig into this private property as a cheap lazy way of
# checking that the proxy header is set correctly.
assert proxy._proxy_headers == [ # type: ignore
(b"Proxy-Authorization", b"Basic dXNlcm5hbWU6cGFzc3dvcmQ=")
]
37 changes: 34 additions & 3 deletions tests/_sync/test_http_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ def test_proxy_tunneling():

with HTTPProxy(
proxy_url="http://localhost:8080/",
max_connections=10,
network_backend=network_backend,
) as proxy:
# Sending an intial request, which once complete will return to the pool, IDLE.
Expand Down Expand Up @@ -169,7 +168,6 @@ def test_proxy_tunneling_http2():

with HTTPProxy(
proxy_url="http://localhost:8080/",
max_connections=10,
network_backend=network_backend,
http2=True,
) as proxy:
Expand Down Expand Up @@ -219,10 +217,43 @@ def test_proxy_tunneling_with_403():

with HTTPProxy(
proxy_url="http://localhost:8080/",
max_connections=10,
network_backend=network_backend,
) as proxy:
with pytest.raises(ProxyError) as exc_info:
proxy.request("GET", "https://example.com/")
assert str(exc_info.value) == "403 Permission Denied"
assert not proxy.connections



def test_proxy_tunneling_with_auth():
"""
Send an authenticated HTTPS request via a proxy.
"""
network_backend = MockBackend(
[
# The initial response to the proxy CONNECT
b"HTTP/1.1 200 OK\r\n\r\n",
# The actual response from the remote server
b"HTTP/1.1 200 OK\r\n",
b"Content-Type: plain/text\r\n",
b"Content-Length: 13\r\n",
b"\r\n",
b"Hello, world!",
]
)

with HTTPProxy(
proxy_url="http://localhost:8080/",
proxy_auth=("username", "password"),
network_backend=network_backend,
) as proxy:
response = proxy.request("GET", "https://example.com/")
assert response.status == 200
assert response.content == b"Hello, world!"

# Dig into this private property as a cheap lazy way of
# checking that the proxy header is set correctly.
assert proxy._proxy_headers == [ # type: ignore
(b"Proxy-Authorization", b"Basic dXNlcm5hbWU6cGFzc3dvcmQ=")
]