python-proxy-headers 0.2.2__py3-none-any.whl → 0.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_proxy_headers/aiohttp_proxy.py +10 -1
- python_proxy_headers/autoscraper_proxy.py +3 -2
- python_proxy_headers/cloudscraper_proxy.py +9 -3
- python_proxy_headers/header_utils.py +203 -0
- python_proxy_headers/httpx_proxy.py +17 -4
- python_proxy_headers/pycurl_proxy.py +26 -3
- python_proxy_headers/requests_adapter.py +7 -1
- python_proxy_headers/urllib3_proxy_manager.py +15 -4
- {python_proxy_headers-0.2.2.dist-info → python_proxy_headers-0.2.4.dist-info}/METADATA +1 -1
- python_proxy_headers-0.2.4.dist-info/RECORD +14 -0
- {python_proxy_headers-0.2.2.dist-info → python_proxy_headers-0.2.4.dist-info}/WHEEL +1 -1
- python_proxy_headers-0.2.2.dist-info/RECORD +0 -13
- {python_proxy_headers-0.2.2.dist-info → python_proxy_headers-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {python_proxy_headers-0.2.2.dist-info → python_proxy_headers-0.2.4.dist-info}/top_level.txt +0 -0
|
@@ -6,6 +6,7 @@ from aiohttp.client import ClientSession
|
|
|
6
6
|
from aiohttp.helpers import reify
|
|
7
7
|
from aiohttp import hdrs
|
|
8
8
|
from multidict import CIMultiDict, CIMultiDictProxy
|
|
9
|
+
from .header_utils import merge_proxy_response_headers
|
|
9
10
|
|
|
10
11
|
class ProxyTCPConnector(TCPConnector):
|
|
11
12
|
async def _create_proxy_connection(self, req: ClientRequest, traces, timeout):
|
|
@@ -143,13 +144,21 @@ class ProxyClientRequest(ClientRequest):
|
|
|
143
144
|
return resp
|
|
144
145
|
|
|
145
146
|
class ProxyClientResponse(ClientResponse):
|
|
147
|
+
@reify
|
|
148
|
+
def proxy_headers(self):
|
|
149
|
+
"""Headers from the proxy CONNECT response, not merged into origin headers."""
|
|
150
|
+
raw = getattr(self, "_proxy_headers", None)
|
|
151
|
+
if raw:
|
|
152
|
+
return CIMultiDictProxy(CIMultiDict(raw))
|
|
153
|
+
return CIMultiDictProxy(CIMultiDict())
|
|
154
|
+
|
|
146
155
|
@reify
|
|
147
156
|
def headers(self):
|
|
148
157
|
proxy_headers = getattr(self, '_proxy_headers', None)
|
|
149
158
|
|
|
150
159
|
if proxy_headers:
|
|
151
160
|
headers = CIMultiDict(self._headers)
|
|
152
|
-
headers
|
|
161
|
+
merge_proxy_response_headers(headers, proxy_headers)
|
|
153
162
|
return CIMultiDictProxy(headers)
|
|
154
163
|
else:
|
|
155
164
|
return self._headers
|
|
@@ -35,6 +35,7 @@ except ImportError:
|
|
|
35
35
|
"Install it with: pip install autoscraper"
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
+
from .header_utils import validate_headers
|
|
38
39
|
from .requests_adapter import ProxySession
|
|
39
40
|
|
|
40
41
|
|
|
@@ -71,7 +72,7 @@ class ProxyAutoScraper(AutoScraper):
|
|
|
71
72
|
stack_list: Optional[List] = None
|
|
72
73
|
):
|
|
73
74
|
super().__init__(stack_list=stack_list)
|
|
74
|
-
self._proxy_headers = proxy_headers
|
|
75
|
+
self._proxy_headers = validate_headers(proxy_headers)
|
|
75
76
|
self._session: Optional[ProxySession] = None
|
|
76
77
|
|
|
77
78
|
def _get_session(self) -> ProxySession:
|
|
@@ -90,7 +91,7 @@ class ProxyAutoScraper(AutoScraper):
|
|
|
90
91
|
Args:
|
|
91
92
|
proxy_headers: New proxy headers to use
|
|
92
93
|
"""
|
|
93
|
-
self._proxy_headers = proxy_headers
|
|
94
|
+
self._proxy_headers = validate_headers(proxy_headers)
|
|
94
95
|
if self._session is not None:
|
|
95
96
|
self._session.close()
|
|
96
97
|
self._session = None
|
|
@@ -27,6 +27,7 @@ except ImportError:
|
|
|
27
27
|
"Install it with: pip install cloudscraper"
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
+
from .header_utils import validate_headers
|
|
30
31
|
from .urllib3_proxy_manager import proxy_from_url
|
|
31
32
|
|
|
32
33
|
|
|
@@ -41,8 +42,13 @@ class CipherSuiteProxyHeaderAdapter(CipherSuiteAdapter):
|
|
|
41
42
|
"""
|
|
42
43
|
|
|
43
44
|
def __init__(self, proxy_headers: Optional[Dict[str, str]] = None, **kwargs):
|
|
44
|
-
self._proxy_headers = proxy_headers
|
|
45
|
+
self._proxy_headers = validate_headers(proxy_headers)
|
|
45
46
|
super().__init__(**kwargs)
|
|
47
|
+
|
|
48
|
+
def build_response(self, req, resp):
|
|
49
|
+
response = super().build_response(req, resp)
|
|
50
|
+
response.proxy_headers = getattr(resp, "proxy_headers", {}) or {}
|
|
51
|
+
return response
|
|
46
52
|
|
|
47
53
|
def proxy_manager_for(self, proxy, **proxy_kwargs):
|
|
48
54
|
"""
|
|
@@ -103,7 +109,7 @@ class ProxyCloudScraper(cloudscraper.CloudScraper):
|
|
|
103
109
|
"""
|
|
104
110
|
|
|
105
111
|
def __init__(self, proxy_headers: Optional[Dict[str, str]] = None, **kwargs):
|
|
106
|
-
self._proxy_headers = proxy_headers
|
|
112
|
+
self._proxy_headers = validate_headers(proxy_headers)
|
|
107
113
|
|
|
108
114
|
# Call parent init
|
|
109
115
|
super().__init__(**kwargs)
|
|
@@ -142,7 +148,7 @@ class ProxyCloudScraper(cloudscraper.CloudScraper):
|
|
|
142
148
|
Args:
|
|
143
149
|
proxy_headers: New proxy headers to use
|
|
144
150
|
"""
|
|
145
|
-
self._proxy_headers = proxy_headers
|
|
151
|
+
self._proxy_headers = validate_headers(proxy_headers)
|
|
146
152
|
|
|
147
153
|
# Remount adapters with new headers
|
|
148
154
|
self.mount(
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Validation and safe merging for proxy CONNECT headers.
|
|
2
|
+
|
|
3
|
+
CONNECT requests are raw HTTP and must not interpolate CR, LF, or NUL.
|
|
4
|
+
CONNECT response headers are not origin HTTPS headers: hop-by-hop and
|
|
5
|
+
security-sensitive names are not copied onto the origin response, and
|
|
6
|
+
existing origin headers are never overwritten.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Dict, List, Mapping, MutableMapping, Optional, Sequence, Tuple, Union
|
|
12
|
+
from urllib.parse import urlparse
|
|
13
|
+
|
|
14
|
+
RawHeaders = Union[Mapping[Any, Any], Sequence[Tuple[Any, Any]], None]
|
|
15
|
+
|
|
16
|
+
# Never copy these from a CONNECT response onto the origin response.
|
|
17
|
+
BLOCKED_PROXY_RESPONSE_HEADERS = frozenset({
|
|
18
|
+
"age",
|
|
19
|
+
"authorization",
|
|
20
|
+
"cache-control",
|
|
21
|
+
"clear-site-data",
|
|
22
|
+
"connection",
|
|
23
|
+
"content-disposition",
|
|
24
|
+
"content-encoding",
|
|
25
|
+
"content-language",
|
|
26
|
+
"content-length",
|
|
27
|
+
"content-location",
|
|
28
|
+
"content-range",
|
|
29
|
+
"content-security-policy",
|
|
30
|
+
"content-security-policy-report-only",
|
|
31
|
+
"content-type",
|
|
32
|
+
"cookie",
|
|
33
|
+
"date",
|
|
34
|
+
"etag",
|
|
35
|
+
"expires",
|
|
36
|
+
"host",
|
|
37
|
+
"keep-alive",
|
|
38
|
+
"last-modified",
|
|
39
|
+
"link",
|
|
40
|
+
"location",
|
|
41
|
+
"pragma",
|
|
42
|
+
"proxy-agent",
|
|
43
|
+
"proxy-authenticate",
|
|
44
|
+
"proxy-authorization",
|
|
45
|
+
"proxy-connection",
|
|
46
|
+
"refresh",
|
|
47
|
+
"server",
|
|
48
|
+
"set-cookie",
|
|
49
|
+
"set-cookie2",
|
|
50
|
+
"strict-transport-security",
|
|
51
|
+
"te",
|
|
52
|
+
"trailer",
|
|
53
|
+
"transfer-encoding",
|
|
54
|
+
"upgrade",
|
|
55
|
+
"vary",
|
|
56
|
+
"via",
|
|
57
|
+
"warning",
|
|
58
|
+
"www-authenticate",
|
|
59
|
+
"x-content-type-options",
|
|
60
|
+
"x-frame-options",
|
|
61
|
+
"x-xss-protection",
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _as_str(value: Any) -> str:
|
|
66
|
+
if isinstance(value, bytes):
|
|
67
|
+
return value.decode("latin-1")
|
|
68
|
+
return str(value)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def validate_header_name(name: Any) -> str:
|
|
72
|
+
"""Return a header name, or raise ValueError if it is not a single token."""
|
|
73
|
+
name_s = _as_str(name)
|
|
74
|
+
if not name_s:
|
|
75
|
+
raise ValueError("Header name must not be empty")
|
|
76
|
+
for char in name_s:
|
|
77
|
+
code = ord(char)
|
|
78
|
+
if char in "()<>@,;:\\\"/[]?={} \t:" or code <= 32 or code == 127:
|
|
79
|
+
raise ValueError(f"Invalid header name {name_s!r}")
|
|
80
|
+
return name_s
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def validate_header_value(value: Any) -> str:
|
|
84
|
+
"""Return a header value, or raise ValueError if it contains CR, LF, or NUL."""
|
|
85
|
+
value_s = _as_str(value)
|
|
86
|
+
if any(char in value_s for char in "\r\n\x00"):
|
|
87
|
+
raise ValueError(f"Invalid header value {value_s!r}")
|
|
88
|
+
return value_s
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def validate_headers(headers: Optional[Mapping[Any, Any]]) -> Dict[str, str]:
|
|
92
|
+
"""Validate a mapping of HTTP header names and values.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ValueError: If any name or value contains CR, LF, NUL, or other
|
|
96
|
+
characters illegal in a single header line.
|
|
97
|
+
"""
|
|
98
|
+
if not headers:
|
|
99
|
+
return {}
|
|
100
|
+
validated: Dict[str, str] = {}
|
|
101
|
+
for name, value in headers.items():
|
|
102
|
+
validated[validate_header_name(name)] = validate_header_value(value)
|
|
103
|
+
return validated
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def validate_http_method(method: Any) -> str:
|
|
107
|
+
"""Return an HTTP method token, or raise ValueError if it is unsafe.
|
|
108
|
+
|
|
109
|
+
Methods must be a single token (no spaces, slashes, or control characters)
|
|
110
|
+
so they cannot smuggle extra request lines through CURLOPT_CUSTOMREQUEST.
|
|
111
|
+
"""
|
|
112
|
+
method_s = _as_str(method).upper()
|
|
113
|
+
if not method_s:
|
|
114
|
+
raise ValueError("HTTP method must not be empty")
|
|
115
|
+
for char in method_s:
|
|
116
|
+
code = ord(char)
|
|
117
|
+
if char in "()<>@,;:\\\"/[]?={} \t" or code <= 32 or code == 127:
|
|
118
|
+
raise ValueError(f"Invalid HTTP method {method_s!r}")
|
|
119
|
+
return method_s
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def validate_http_url(url: Any) -> str:
|
|
123
|
+
"""Return an http(s) URL, or raise ValueError for other schemes.
|
|
124
|
+
|
|
125
|
+
High-level HTTP helpers must not pass caller URLs to protocol-agile
|
|
126
|
+
backends such as libcurl, which otherwise honor ``file://`` and similar
|
|
127
|
+
schemes (and ignore an HTTP proxy for those schemes).
|
|
128
|
+
"""
|
|
129
|
+
url_s = _as_str(url)
|
|
130
|
+
if not url_s or any(char in url_s for char in "\r\n\x00"):
|
|
131
|
+
raise ValueError(f"Invalid URL {url_s!r}")
|
|
132
|
+
parsed = urlparse(url_s)
|
|
133
|
+
scheme = parsed.scheme.lower()
|
|
134
|
+
if scheme not in ("http", "https"):
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"Unsupported URL scheme {parsed.scheme!r}; only http and https are allowed"
|
|
137
|
+
)
|
|
138
|
+
if not parsed.netloc:
|
|
139
|
+
raise ValueError(f"URL must include a host: {url_s!r}")
|
|
140
|
+
return url_s
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _header_items(headers: RawHeaders) -> List[Tuple[Any, Any]]:
|
|
144
|
+
if not headers:
|
|
145
|
+
return []
|
|
146
|
+
if isinstance(headers, (list, tuple)):
|
|
147
|
+
return list(headers)
|
|
148
|
+
if hasattr(headers, "items"):
|
|
149
|
+
return list(headers.items())
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def origin_has_header(origin_headers: RawHeaders, name: str) -> bool:
|
|
154
|
+
"""Return True if origin_headers already contains ``name`` (case-insensitive)."""
|
|
155
|
+
lowered = _as_str(name).lower()
|
|
156
|
+
for key, _value in _header_items(origin_headers):
|
|
157
|
+
if _as_str(key).lower() == lowered:
|
|
158
|
+
return True
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def is_mergeable_proxy_header(name: Any) -> bool:
|
|
163
|
+
"""Return True if a CONNECT response header may be copied onto origin headers."""
|
|
164
|
+
lowered = _as_str(name).lower()
|
|
165
|
+
if lowered in BLOCKED_PROXY_RESPONSE_HEADERS:
|
|
166
|
+
return False
|
|
167
|
+
if lowered.startswith("access-control-"):
|
|
168
|
+
return False
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def snapshot_headers(headers: RawHeaders) -> Dict[str, str]:
|
|
173
|
+
"""Copy headers into a plain dict of strings."""
|
|
174
|
+
snapshot: Dict[str, str] = {}
|
|
175
|
+
for name, value in _header_items(headers):
|
|
176
|
+
snapshot[_as_str(name)] = _as_str(value)
|
|
177
|
+
return snapshot
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def merge_proxy_response_headers(
|
|
181
|
+
origin_headers: MutableMapping[Any, Any],
|
|
182
|
+
proxy_headers: RawHeaders,
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Copy safe CONNECT headers onto origin headers without overwriting."""
|
|
185
|
+
for name, value in _header_items(proxy_headers):
|
|
186
|
+
name_s = _as_str(name)
|
|
187
|
+
if not is_mergeable_proxy_header(name_s):
|
|
188
|
+
continue
|
|
189
|
+
if origin_has_header(origin_headers, name_s):
|
|
190
|
+
continue
|
|
191
|
+
origin_headers[name_s] = _as_str(value)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def filter_connect_headers(
|
|
195
|
+
origin_headers: RawHeaders,
|
|
196
|
+
connect_headers: RawHeaders,
|
|
197
|
+
) -> List[Tuple[Any, Any]]:
|
|
198
|
+
"""Return CONNECT header pairs that are safe to merge into origin headers."""
|
|
199
|
+
extra: List[Tuple[Any, Any]] = []
|
|
200
|
+
for name, value in _header_items(connect_headers):
|
|
201
|
+
if is_mergeable_proxy_header(name) and not origin_has_header(origin_headers, name):
|
|
202
|
+
extra.append((name, value))
|
|
203
|
+
return extra
|
|
@@ -9,6 +9,7 @@ from httpcore._ssl import default_ssl_context
|
|
|
9
9
|
from httpcore._trace import Trace
|
|
10
10
|
from httpx import AsyncHTTPTransport, HTTPTransport, Client
|
|
11
11
|
from httpx._config import DEFAULT_LIMITS, DEFAULT_TIMEOUT_CONFIG, Proxy, create_ssl_context
|
|
12
|
+
from .header_utils import filter_connect_headers
|
|
12
13
|
|
|
13
14
|
class ProxyTunnelHTTPConnection(TunnelHTTPConnection):
|
|
14
15
|
# Unfortunately the only way to get connect_response.headers into the Response
|
|
@@ -90,10 +91,16 @@ class ProxyTunnelHTTPConnection(TunnelHTTPConnection):
|
|
|
90
91
|
keepalive_expiry=self._keepalive_expiry,
|
|
91
92
|
)
|
|
92
93
|
|
|
94
|
+
self._connect_response_headers = connect_response.headers
|
|
93
95
|
self._connected = True
|
|
94
|
-
#
|
|
96
|
+
# Merge safe CONNECT headers without overwriting origin values
|
|
95
97
|
response = self._connection.handle_request(request)
|
|
96
|
-
response.headers = merge_headers(
|
|
98
|
+
response.headers = merge_headers(
|
|
99
|
+
response.headers,
|
|
100
|
+
filter_connect_headers(
|
|
101
|
+
response.headers, getattr(self, "_connect_response_headers", ())
|
|
102
|
+
),
|
|
103
|
+
)
|
|
97
104
|
return response
|
|
98
105
|
|
|
99
106
|
class AsyncProxyTunnelHTTPConnection(AsyncTunnelHTTPConnection):
|
|
@@ -174,10 +181,16 @@ class AsyncProxyTunnelHTTPConnection(AsyncTunnelHTTPConnection):
|
|
|
174
181
|
keepalive_expiry=self._keepalive_expiry,
|
|
175
182
|
)
|
|
176
183
|
|
|
184
|
+
self._connect_response_headers = connect_response.headers
|
|
177
185
|
self._connected = True
|
|
178
|
-
#
|
|
186
|
+
# Merge safe CONNECT headers without overwriting origin values
|
|
179
187
|
response = await self._connection.handle_async_request(request)
|
|
180
|
-
response.headers = merge_headers(
|
|
188
|
+
response.headers = merge_headers(
|
|
189
|
+
response.headers,
|
|
190
|
+
filter_connect_headers(
|
|
191
|
+
response.headers, getattr(self, "_connect_response_headers", ())
|
|
192
|
+
),
|
|
193
|
+
)
|
|
181
194
|
return response
|
|
182
195
|
|
|
183
196
|
class HTTPProxyHeaders(HTTPProxy):
|
|
@@ -38,6 +38,8 @@ from io import BytesIO
|
|
|
38
38
|
from dataclasses import dataclass, field
|
|
39
39
|
from typing import Dict, List, Optional, Tuple
|
|
40
40
|
|
|
41
|
+
from .header_utils import validate_headers, validate_http_method, validate_http_url
|
|
42
|
+
|
|
41
43
|
try:
|
|
42
44
|
import pycurl
|
|
43
45
|
except ImportError:
|
|
@@ -67,7 +69,8 @@ def set_proxy_headers(curl, headers: Dict[str, str]) -> None:
|
|
|
67
69
|
"""
|
|
68
70
|
if not headers:
|
|
69
71
|
return
|
|
70
|
-
|
|
72
|
+
|
|
73
|
+
headers = validate_headers(headers)
|
|
71
74
|
header_list = [f"{k}: {v}" for k, v in headers.items()]
|
|
72
75
|
|
|
73
76
|
# Set CURLOPT_PROXYHEADER
|
|
@@ -260,6 +263,18 @@ class Response:
|
|
|
260
263
|
raise Exception(f"HTTP Error {self.status_code}")
|
|
261
264
|
|
|
262
265
|
|
|
266
|
+
def _restrict_http_protocols(curl) -> None:
|
|
267
|
+
"""Limit libcurl to HTTP(S) so file:// and similar schemes cannot run."""
|
|
268
|
+
http_https = pycurl.PROTO_HTTP | pycurl.PROTO_HTTPS
|
|
269
|
+
curl.setopt(pycurl.PROTOCOLS, http_https)
|
|
270
|
+
redir_protocols = getattr(pycurl, "REDIR_PROTOCOLS", None)
|
|
271
|
+
if redir_protocols is not None:
|
|
272
|
+
try:
|
|
273
|
+
curl.setopt(redir_protocols, http_https)
|
|
274
|
+
except pycurl.error:
|
|
275
|
+
pass
|
|
276
|
+
|
|
277
|
+
|
|
263
278
|
def request(
|
|
264
279
|
method: str,
|
|
265
280
|
url: str,
|
|
@@ -275,7 +290,7 @@ def request(
|
|
|
275
290
|
|
|
276
291
|
Args:
|
|
277
292
|
method: HTTP method (GET, POST, etc.)
|
|
278
|
-
url: Target URL
|
|
293
|
+
url: Target URL (http and https only)
|
|
279
294
|
proxy: Proxy URL (e.g., 'http://user:pass@proxy:8080')
|
|
280
295
|
proxy_headers: Headers to send to the proxy
|
|
281
296
|
headers: Headers to send to the origin server
|
|
@@ -285,17 +300,25 @@ def request(
|
|
|
285
300
|
|
|
286
301
|
Returns:
|
|
287
302
|
Response object with body, headers, and proxy_headers
|
|
303
|
+
|
|
304
|
+
Raises:
|
|
305
|
+
ValueError: If the URL scheme is not http/https, the method is not a
|
|
306
|
+
single token, or a header name/value contains CR, LF, or NUL.
|
|
288
307
|
"""
|
|
308
|
+
url = validate_http_url(url)
|
|
309
|
+
method = validate_http_method(method)
|
|
310
|
+
headers = validate_headers(headers)
|
|
311
|
+
|
|
289
312
|
c = pycurl.Curl()
|
|
290
313
|
body = BytesIO()
|
|
291
314
|
capture = HeaderCapture(c)
|
|
292
315
|
|
|
293
316
|
try:
|
|
317
|
+
_restrict_http_protocols(c)
|
|
294
318
|
c.setopt(pycurl.URL, url)
|
|
295
319
|
c.setopt(pycurl.WRITEFUNCTION, body.write)
|
|
296
320
|
|
|
297
321
|
# HTTP method
|
|
298
|
-
method = method.upper()
|
|
299
322
|
if method == 'GET':
|
|
300
323
|
c.setopt(pycurl.HTTPGET, 1)
|
|
301
324
|
elif method == 'POST':
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
from requests.adapters import HTTPAdapter
|
|
2
2
|
from requests.sessions import Session
|
|
3
|
+
from .header_utils import validate_headers
|
|
3
4
|
from .urllib3_proxy_manager import proxy_from_url
|
|
4
5
|
|
|
5
6
|
class HTTPProxyHeaderAdapter(HTTPAdapter):
|
|
6
7
|
def __init__(self, proxy_headers=None):
|
|
7
8
|
super().__init__()
|
|
8
|
-
self._proxy_headers = proxy_headers
|
|
9
|
+
self._proxy_headers = validate_headers(proxy_headers)
|
|
10
|
+
|
|
11
|
+
def build_response(self, req, resp):
|
|
12
|
+
response = super().build_response(req, resp)
|
|
13
|
+
response.proxy_headers = getattr(resp, "proxy_headers", {}) or {}
|
|
14
|
+
return response
|
|
9
15
|
|
|
10
16
|
def proxy_manager_for(self, proxy, **proxy_kwargs):
|
|
11
17
|
"""Return urllib3 ProxyManager for the given proxy.
|
|
@@ -6,6 +6,14 @@ from urllib3.poolmanager import ProxyManager
|
|
|
6
6
|
from urllib3.util.request import make_headers
|
|
7
7
|
from urllib3.util.url import parse_url
|
|
8
8
|
|
|
9
|
+
from .header_utils import (
|
|
10
|
+
merge_proxy_response_headers,
|
|
11
|
+
snapshot_headers,
|
|
12
|
+
validate_header_name,
|
|
13
|
+
validate_header_value,
|
|
14
|
+
validate_headers,
|
|
15
|
+
)
|
|
16
|
+
|
|
9
17
|
if sys.version_info < (3, 12, 0):
|
|
10
18
|
#####################################
|
|
11
19
|
### copied from python3.12 source ###
|
|
@@ -77,6 +85,8 @@ class HTTPSProxyConnection(HTTPSConnection):
|
|
|
77
85
|
self._http_vsn_str.encode("ascii"))
|
|
78
86
|
headers = [connect]
|
|
79
87
|
for header, value in self._tunnel_headers.items():
|
|
88
|
+
validate_header_name(header)
|
|
89
|
+
validate_header_value(value)
|
|
80
90
|
headers.append(f"{header}: {value}\r\n".encode("latin-1"))
|
|
81
91
|
headers.append(b"\r\n")
|
|
82
92
|
# Making a single send() call instead of one per line encourages
|
|
@@ -125,7 +135,9 @@ class HTTPSProxyConnectionPool(HTTPSConnectionPool):
|
|
|
125
135
|
|
|
126
136
|
def urlopen(self, *args, **kwargs):
|
|
127
137
|
response = super().urlopen(*args, **kwargs)
|
|
128
|
-
|
|
138
|
+
proxy_headers = getattr(self, "_proxy_response_headers", None)
|
|
139
|
+
response.proxy_headers = snapshot_headers(proxy_headers)
|
|
140
|
+
merge_proxy_response_headers(response.headers, proxy_headers)
|
|
129
141
|
return response
|
|
130
142
|
|
|
131
143
|
class ProxyHeaderManager(ProxyManager):
|
|
@@ -137,15 +149,14 @@ class ProxyHeaderManager(ProxyManager):
|
|
|
137
149
|
if proxy_url is None and args:
|
|
138
150
|
proxy_url = args[0]
|
|
139
151
|
proxy_headers = kwargs.get("proxy_headers")
|
|
140
|
-
merged = dict(proxy_headers or {})
|
|
152
|
+
merged = validate_headers(dict(proxy_headers or {}))
|
|
141
153
|
if isinstance(proxy_url, str):
|
|
142
154
|
parsed = parse_url(proxy_url)
|
|
143
155
|
if parsed.auth and not any(
|
|
144
156
|
k.lower() == "proxy-authorization" for k in merged
|
|
145
157
|
):
|
|
146
158
|
merged.update(make_headers(proxy_basic_auth=parsed.auth))
|
|
147
|
-
|
|
148
|
-
kwargs["proxy_headers"] = merged
|
|
159
|
+
kwargs["proxy_headers"] = merged
|
|
149
160
|
super().__init__(*args, **kwargs)
|
|
150
161
|
self.pool_classes_by_scheme = {"http": HTTPConnectionPool, "https": HTTPSProxyConnectionPool}
|
|
151
162
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-proxy-headers
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Handle custom proxy headers for http & https requests in various python libraries
|
|
5
5
|
Author-email: ProxyMesh <support@proxymesh.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/proxymesh/python-proxy-headers
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
python_proxy_headers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
python_proxy_headers/aiohttp_proxy.py,sha256=c6-MS6ldiLS5rxPmunrk9Z8-BkXryJOe6HNxbi3amo8,5256
|
|
3
|
+
python_proxy_headers/autoscraper_proxy.py,sha256=wQoTE2akpbY2nXZWQvxmP5Ik2k8vSVd6ErYlz24YWCM,11415
|
|
4
|
+
python_proxy_headers/cloudscraper_proxy.py,sha256=kdzqPHX_9Bf05EeV9jalbA5uqGEKdKXanyBV0y-N85E,8058
|
|
5
|
+
python_proxy_headers/header_utils.py,sha256=llAy606dwqEES4N60SLzSlB3ijdpcJVm7fnoNwdLtVY,5880
|
|
6
|
+
python_proxy_headers/httpx_proxy.py,sha256=HxaKs-MWbxS6Jhxp0356OUvkT5Vun4Bab6G-NmfdhC0,12232
|
|
7
|
+
python_proxy_headers/pycurl_proxy.py,sha256=Ogan8WT4cC5odifLbPXfct1twGy0JYbSmEkoRGwk9nM,12602
|
|
8
|
+
python_proxy_headers/requests_adapter.py,sha256=ln3cXaWCHHMgL4uNlNrp9MDsocsFL68FuZ_Kyuj4U8U,2540
|
|
9
|
+
python_proxy_headers/urllib3_proxy_manager.py,sha256=Yi_OlG5eZtQSPhj8-Tw7Y5hKmwCj8pApZn6fJARoOD8,5516
|
|
10
|
+
python_proxy_headers-0.2.4.dist-info/licenses/LICENSE,sha256=i_H6fvudjqZOKkZFCIHGmbPHIxszjCAT90UJSY2OM0U,1066
|
|
11
|
+
python_proxy_headers-0.2.4.dist-info/METADATA,sha256=T_Sy6rc2N8MsSIJXAAgYhb30n0utvdhlTEsxslW48r8,6288
|
|
12
|
+
python_proxy_headers-0.2.4.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
13
|
+
python_proxy_headers-0.2.4.dist-info/top_level.txt,sha256=Bhm-Qc6vn0DAd2Li8ZBn6KLkls2zJnM4-CtXHvIiRh8,21
|
|
14
|
+
python_proxy_headers-0.2.4.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
python_proxy_headers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
python_proxy_headers/aiohttp_proxy.py,sha256=OyEruj3CCrFmlEikmmOXGQsVJnrAfK-TMzycP-sLH_Y,4920
|
|
3
|
-
python_proxy_headers/autoscraper_proxy.py,sha256=g51K71xOah_uNtMIXEXggtd6T8ol7vAukg7oaWVvhEA,11342
|
|
4
|
-
python_proxy_headers/cloudscraper_proxy.py,sha256=6xF58QywHV4IYlC_KtXIWQTXZjE8CeEa5-GfqcSGZ3U,7780
|
|
5
|
-
python_proxy_headers/httpx_proxy.py,sha256=Q8tDlfl4u3fjsZZDA1zj1pbN7fiAwJ3ctFGojbqtaEo,11829
|
|
6
|
-
python_proxy_headers/pycurl_proxy.py,sha256=_JAt_o5gX7o0sRLOQXG3421mEehzBm-rhUl4c9IG5HQ,11738
|
|
7
|
-
python_proxy_headers/requests_adapter.py,sha256=CfGEEYc0eaKKNA11VbhZzv9qFVEL1uMSzAUNvidIyEU,2313
|
|
8
|
-
python_proxy_headers/urllib3_proxy_manager.py,sha256=ryT7QaCPb7lyQtHaP3v6MKfBerurBYrdRj4i-41ORT4,5196
|
|
9
|
-
python_proxy_headers-0.2.2.dist-info/licenses/LICENSE,sha256=i_H6fvudjqZOKkZFCIHGmbPHIxszjCAT90UJSY2OM0U,1066
|
|
10
|
-
python_proxy_headers-0.2.2.dist-info/METADATA,sha256=ltXQg143IYn7qLMgAjQihF4UJCUNurgBjSJfZPWcD1U,6288
|
|
11
|
-
python_proxy_headers-0.2.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
-
python_proxy_headers-0.2.2.dist-info/top_level.txt,sha256=Bhm-Qc6vn0DAd2Li8ZBn6KLkls2zJnM4-CtXHvIiRh8,21
|
|
13
|
-
python_proxy_headers-0.2.2.dist-info/RECORD,,
|
{python_proxy_headers-0.2.2.dist-info → python_proxy_headers-0.2.4.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|