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.
@@ -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 CONNECT request headers.
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 CONNECT header line.
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
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=EHX_XE2-6J2Q43LTy0u3lUFEmvJxYfzkUl0UBvh4Ttc,4552
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=2zy-KOioPzCdlGh9jbtynmyjxzvof5ey4d86vl5IxTM,11818
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.3.dist-info/licenses/LICENSE,sha256=i_H6fvudjqZOKkZFCIHGmbPHIxszjCAT90UJSY2OM0U,1066
11
- python_proxy_headers-0.2.3.dist-info/METADATA,sha256=oap_oG9q7mWY2tOUXZoU6Pl--N5evuANSIxjE-YH27Q,6288
12
- python_proxy_headers-0.2.3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
13
- python_proxy_headers-0.2.3.dist-info/top_level.txt,sha256=Bhm-Qc6vn0DAd2Li8ZBn6KLkls2zJnM4-CtXHvIiRh8,21
14
- python_proxy_headers-0.2.3.dist-info/RECORD,,
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (84.0.0)
2
+ Generator: setuptools (84.2.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5