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
4 changes: 4 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ updates:
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
4 changes: 2 additions & 2 deletions .github/workflows/github_release_on_release_branch_merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout merge commit
uses: actions/checkout@v6
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}

- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
with:
python-version: "3.12"

Expand Down
28 changes: 26 additions & 2 deletions .github/workflows/proxy_integration_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,40 @@ permissions:
contents: read

jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
with:
python-version: "3.x"

- name: Install system dependencies (pycurl)
run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev

- name: Install package and test dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
pip install urllib3 requests aiohttp httpx pycurl

- name: Run header security unit tests
run: python test_header_security.py

integration:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
with:
python-version: "3.x"

Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
outputs:
publish: ${{ steps.decide.outputs.publish }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
if: github.event_name == 'workflow_run'
with:
ref: main
Expand Down Expand Up @@ -64,12 +64,12 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
with:
ref: ${{ github.event_name == 'workflow_run' && 'main' || github.event_name == 'release' && github.ref || 'main' }}

- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
with:
python-version: "3.x"

Expand All @@ -82,7 +82,7 @@ jobs:
run: python -m build

- name: Store distribution packages
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: python-package-distributions
path: dist/
Expand All @@ -102,13 +102,13 @@ jobs:

steps:
- name: Download distribution packages
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: python-package-distributions
path: dist/

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33

publish-to-testpypi:
name: Publish to TestPyPI
Expand All @@ -122,12 +122,12 @@ jobs:

steps:
- name: Download distribution packages
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: python-package-distributions
path: dist/

- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
with:
repository-url: https://test.pypi.org/legacy/
2 changes: 1 addition & 1 deletion docs/aiohttp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ When establishing an HTTPS connection through a proxy, the connector:
* Creates a CONNECT request to the proxy server
* Includes any custom proxy headers you've specified
* Captures the proxy's response headers (e.g., ``X-ProxyMesh-IP``)
* Stores them so they can be merged into the final response
* Stores them so safe CONNECT values can be merged into the final response without overwriting origin headers. Hop-by-hop and security-sensitive names are omitted. The full CONNECT set is on ``response.proxy_headers``.

You typically don't need to use this class directly - it's automatically configured when using ``ProxyClientSession``.

Expand Down
2 changes: 1 addition & 1 deletion docs/httpx.rst
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ The extension classes work by intercepting the CONNECT request/response cycle du

2. **CONNECT Response**: The proxy responds with a CONNECT response (status 200) that may include proxy information headers in the response (e.g., ``X-ProxyMesh-IP: 192.168.1.1``)

3. **Header Merging**: These proxy response headers are captured during the tunnel establishment and stored. When the actual HTTP request is made through the tunnel, the proxy response headers are merged into the final HTTP response headers using ``merge_headers()``
3. **Header Merging**: Safe CONNECT headers are stored and later merged into the origin response with ``merge_headers()``. Origin headers are never overwritten, and hop-by-hop or security-sensitive names such as ``Set-Cookie`` or ``Location`` from CONNECT are not copied.

4. **Access**: Your application can then access both the target server's response headers and the proxy's response headers from the same response object

Expand Down
2 changes: 2 additions & 0 deletions docs/requests.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ This section shows you how to quickly get up and running with proxy headers in r

That's it! The ``requests_adapter`` module handles sending your custom headers to the proxy and makes proxy response headers available in the response.

CONNECT response headers are copied onto ``response.headers`` except hop-by-hop and security-sensitive names (``Set-Cookie``, ``Location``, ``Content-Type``, and similar). Existing origin headers are never overwritten. The full CONNECT set is on ``response.proxy_headers``.

Using Proxies with requests
---------------------------

Expand Down
2 changes: 2 additions & 0 deletions docs/urllib3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ This section shows you how to quickly get up and running with proxy headers in u

That's it! The ``ProxyHeaderManager`` handles sending your custom headers to the proxy and makes proxy response headers available in the response.

CONNECT response headers are copied onto ``response.headers`` except hop-by-hop and security-sensitive names (``Set-Cookie``, ``Location``, ``Content-Type``, and similar). Existing origin headers are never overwritten. The full CONNECT set is on ``response.proxy_headers``.

Using Proxies with urllib3
--------------------------

Expand Down
11 changes: 10 additions & 1 deletion python_proxy_headers/aiohttp_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from aiohttp.helpers import reify
from aiohttp import hdrs
from multidict import CIMultiDict, CIMultiDictProxy
from .header_utils import merge_proxy_response_headers

class ProxyTCPConnector(TCPConnector):
async def _create_proxy_connection(self, req: ClientRequest, traces, timeout):
Expand Down Expand Up @@ -143,13 +144,21 @@ async def send(self, conn):
return resp

class ProxyClientResponse(ClientResponse):
@reify
def proxy_headers(self):
"""Headers from the proxy CONNECT response, not merged into origin headers."""
raw = getattr(self, "_proxy_headers", None)
if raw:
return CIMultiDictProxy(CIMultiDict(raw))
return CIMultiDictProxy(CIMultiDict())

@reify
def headers(self):
proxy_headers = getattr(self, '_proxy_headers', None)

if proxy_headers:
headers = CIMultiDict(self._headers)
headers.extend(proxy_headers)
merge_proxy_response_headers(headers, proxy_headers)
return CIMultiDictProxy(headers)
else:
return self._headers
Expand Down
5 changes: 3 additions & 2 deletions python_proxy_headers/autoscraper_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"Install it with: pip install autoscraper"
)

from .header_utils import validate_headers
from .requests_adapter import ProxySession


Expand Down Expand Up @@ -71,7 +72,7 @@ def __init__(
stack_list: Optional[List] = None
):
super().__init__(stack_list=stack_list)
self._proxy_headers = proxy_headers or {}
self._proxy_headers = validate_headers(proxy_headers)
self._session: Optional[ProxySession] = None

def _get_session(self) -> ProxySession:
Expand All @@ -90,7 +91,7 @@ def set_proxy_headers(self, proxy_headers: Dict[str, str]):
Args:
proxy_headers: New proxy headers to use
"""
self._proxy_headers = proxy_headers
self._proxy_headers = validate_headers(proxy_headers)
if self._session is not None:
self._session.close()
self._session = None
Expand Down
12 changes: 9 additions & 3 deletions python_proxy_headers/cloudscraper_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"Install it with: pip install cloudscraper"
)

from .header_utils import validate_headers
from .urllib3_proxy_manager import proxy_from_url


Expand All @@ -41,8 +42,13 @@ class CipherSuiteProxyHeaderAdapter(CipherSuiteAdapter):
"""

def __init__(self, proxy_headers: Optional[Dict[str, str]] = None, **kwargs):
self._proxy_headers = proxy_headers or {}
self._proxy_headers = validate_headers(proxy_headers)
super().__init__(**kwargs)

def build_response(self, req, resp):
response = super().build_response(req, resp)
response.proxy_headers = getattr(resp, "proxy_headers", {}) or {}
return response

def proxy_manager_for(self, proxy, **proxy_kwargs):
"""
Expand Down Expand Up @@ -103,7 +109,7 @@ class ProxyCloudScraper(cloudscraper.CloudScraper):
"""

def __init__(self, proxy_headers: Optional[Dict[str, str]] = None, **kwargs):
self._proxy_headers = proxy_headers or {}
self._proxy_headers = validate_headers(proxy_headers)

# Call parent init
super().__init__(**kwargs)
Expand Down Expand Up @@ -142,7 +148,7 @@ def set_proxy_headers(self, proxy_headers: Dict[str, str]):
Args:
proxy_headers: New proxy headers to use
"""
self._proxy_headers = proxy_headers
self._proxy_headers = validate_headers(proxy_headers)

# Remount adapters with new headers
self.mount(
Expand Down
Loading