python-proxy-headers 0.2.3__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/header_utils.py +40 -2
- python_proxy_headers/pycurl_proxy.py +23 -3
- {python_proxy_headers-0.2.3.dist-info → python_proxy_headers-0.2.4.dist-info}/METADATA +1 -1
- {python_proxy_headers-0.2.3.dist-info → python_proxy_headers-0.2.4.dist-info}/RECORD +7 -7
- {python_proxy_headers-0.2.3.dist-info → python_proxy_headers-0.2.4.dist-info}/WHEEL +1 -1
- {python_proxy_headers-0.2.3.dist-info → python_proxy_headers-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {python_proxy_headers-0.2.3.dist-info → python_proxy_headers-0.2.4.dist-info}/top_level.txt +0 -0
|
@@ -9,6 +9,7 @@ existing origin headers are never overwritten.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
from typing import Any, Dict, List, Mapping, MutableMapping, Optional, Sequence, Tuple, Union
|
|
12
|
+
from urllib.parse import urlparse
|
|
12
13
|
|
|
13
14
|
RawHeaders = Union[Mapping[Any, Any], Sequence[Tuple[Any, Any]], None]
|
|
14
15
|
|
|
@@ -88,11 +89,11 @@ def validate_header_value(value: Any) -> str:
|
|
|
88
89
|
|
|
89
90
|
|
|
90
91
|
def validate_headers(headers: Optional[Mapping[Any, Any]]) -> Dict[str, str]:
|
|
91
|
-
"""Validate a mapping of
|
|
92
|
+
"""Validate a mapping of HTTP header names and values.
|
|
92
93
|
|
|
93
94
|
Raises:
|
|
94
95
|
ValueError: If any name or value contains CR, LF, NUL, or other
|
|
95
|
-
characters illegal in a single
|
|
96
|
+
characters illegal in a single header line.
|
|
96
97
|
"""
|
|
97
98
|
if not headers:
|
|
98
99
|
return {}
|
|
@@ -102,6 +103,43 @@ def validate_headers(headers: Optional[Mapping[Any, Any]]) -> Dict[str, str]:
|
|
|
102
103
|
return validated
|
|
103
104
|
|
|
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
|
+
|
|
105
143
|
def _header_items(headers: RawHeaders) -> List[Tuple[Any, Any]]:
|
|
106
144
|
if not headers:
|
|
107
145
|
return []
|
|
@@ -38,7 +38,7 @@ 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
|
|
41
|
+
from .header_utils import validate_headers, validate_http_method, validate_http_url
|
|
42
42
|
|
|
43
43
|
try:
|
|
44
44
|
import pycurl
|
|
@@ -263,6 +263,18 @@ class Response:
|
|
|
263
263
|
raise Exception(f"HTTP Error {self.status_code}")
|
|
264
264
|
|
|
265
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
|
+
|
|
266
278
|
def request(
|
|
267
279
|
method: str,
|
|
268
280
|
url: str,
|
|
@@ -278,7 +290,7 @@ def request(
|
|
|
278
290
|
|
|
279
291
|
Args:
|
|
280
292
|
method: HTTP method (GET, POST, etc.)
|
|
281
|
-
url: Target URL
|
|
293
|
+
url: Target URL (http and https only)
|
|
282
294
|
proxy: Proxy URL (e.g., 'http://user:pass@proxy:8080')
|
|
283
295
|
proxy_headers: Headers to send to the proxy
|
|
284
296
|
headers: Headers to send to the origin server
|
|
@@ -288,17 +300,25 @@ def request(
|
|
|
288
300
|
|
|
289
301
|
Returns:
|
|
290
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.
|
|
291
307
|
"""
|
|
308
|
+
url = validate_http_url(url)
|
|
309
|
+
method = validate_http_method(method)
|
|
310
|
+
headers = validate_headers(headers)
|
|
311
|
+
|
|
292
312
|
c = pycurl.Curl()
|
|
293
313
|
body = BytesIO()
|
|
294
314
|
capture = HeaderCapture(c)
|
|
295
315
|
|
|
296
316
|
try:
|
|
317
|
+
_restrict_http_protocols(c)
|
|
297
318
|
c.setopt(pycurl.URL, url)
|
|
298
319
|
c.setopt(pycurl.WRITEFUNCTION, body.write)
|
|
299
320
|
|
|
300
321
|
# HTTP method
|
|
301
|
-
method = method.upper()
|
|
302
322
|
if method == 'GET':
|
|
303
323
|
c.setopt(pycurl.HTTPGET, 1)
|
|
304
324
|
elif method == 'POST':
|
|
@@ -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
|
|
@@ -2,13 +2,13 @@ python_proxy_headers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
|
|
|
2
2
|
python_proxy_headers/aiohttp_proxy.py,sha256=c6-MS6ldiLS5rxPmunrk9Z8-BkXryJOe6HNxbi3amo8,5256
|
|
3
3
|
python_proxy_headers/autoscraper_proxy.py,sha256=wQoTE2akpbY2nXZWQvxmP5Ik2k8vSVd6ErYlz24YWCM,11415
|
|
4
4
|
python_proxy_headers/cloudscraper_proxy.py,sha256=kdzqPHX_9Bf05EeV9jalbA5uqGEKdKXanyBV0y-N85E,8058
|
|
5
|
-
python_proxy_headers/header_utils.py,sha256=
|
|
5
|
+
python_proxy_headers/header_utils.py,sha256=llAy606dwqEES4N60SLzSlB3ijdpcJVm7fnoNwdLtVY,5880
|
|
6
6
|
python_proxy_headers/httpx_proxy.py,sha256=HxaKs-MWbxS6Jhxp0356OUvkT5Vun4Bab6G-NmfdhC0,12232
|
|
7
|
-
python_proxy_headers/pycurl_proxy.py,sha256=
|
|
7
|
+
python_proxy_headers/pycurl_proxy.py,sha256=Ogan8WT4cC5odifLbPXfct1twGy0JYbSmEkoRGwk9nM,12602
|
|
8
8
|
python_proxy_headers/requests_adapter.py,sha256=ln3cXaWCHHMgL4uNlNrp9MDsocsFL68FuZ_Kyuj4U8U,2540
|
|
9
9
|
python_proxy_headers/urllib3_proxy_manager.py,sha256=Yi_OlG5eZtQSPhj8-Tw7Y5hKmwCj8pApZn6fJARoOD8,5516
|
|
10
|
-
python_proxy_headers-0.2.
|
|
11
|
-
python_proxy_headers-0.2.
|
|
12
|
-
python_proxy_headers-0.2.
|
|
13
|
-
python_proxy_headers-0.2.
|
|
14
|
-
python_proxy_headers-0.2.
|
|
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,,
|
{python_proxy_headers-0.2.3.dist-info → python_proxy_headers-0.2.4.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|