ulak 0.1.0__tar.gz

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.
ulak-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Musa SANA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ulak-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: ulak
3
+ Version: 0.1.0
4
+ Summary: Drop-in HTTP(S) proxy hook: route all of a Python script's traffic through Burp/mitmproxy/ZAP with one import.
5
+ Author-email: Musa SANA <musa.sana@hotmail.com>
6
+ Maintainer-email: Musa SANA <musa.sana@hotmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/musana/ulak
9
+ Project-URL: Repository, https://github.com/musana/ulak
10
+ Project-URL: Issues, https://github.com/musana/ulak/issues
11
+ Project-URL: Changelog, https://github.com/musana/ulak/releases
12
+ Keywords: proxy,http,https,burp,mitmproxy,interception,security,monkeypatch,debugging
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Topic :: Security
16
+ Classifier: Topic :: Internet :: Proxy Servers
17
+ Classifier: Topic :: Software Development :: Testing
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Operating System :: OS Independent
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: test
24
+ Requires-Dist: requests; extra == "test"
25
+ Requires-Dist: httpx; extra == "test"
26
+ Requires-Dist: aiohttp; extra == "test"
27
+ Requires-Dist: urllib3; extra == "test"
28
+ Requires-Dist: niquests; extra == "test"
29
+ Requires-Dist: httplib2; extra == "test"
30
+ Requires-Dist: curl_cffi; extra == "test"
31
+ Requires-Dist: pytest; extra == "test"
32
+ Dynamic: license-file
33
+
34
+ # ulak
35
+
36
+ Send every HTTP and HTTPS request a Python script makes through your proxy, with a single import, without changing the script.
37
+
38
+ ## The problem
39
+
40
+ When you run a script someone else wrote, you often want to see the HTTP traffic it sends. You can inspect it in Burp, mitmproxy, or ZAP, but getting there is painful. Either you set a system wide proxy, or you hunt down every request call in the code and add a proxy argument by hand. And Python has many HTTP libraries, so there is no single place to change.
41
+
42
+ ## The solution
43
+
44
+ `ulak` does it in one line. Add it to the top of the script and all outbound traffic goes through the proxy you pick. The name is the Turkish word for a courier who carries a message from one post to the next, which is what this does for the script's requests.
45
+
46
+ ```python
47
+ import ulak
48
+ ulak.proxy("127.0.0.1", 8080)
49
+ ```
50
+
51
+ Every request after that line is routed through `127.0.0.1:8080`.
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pip install ulak
57
+ ```
58
+
59
+ There are no required dependencies. A library is only hooked if the script already uses it.
60
+
61
+ ## What it covers
62
+
63
+ Hooked directly, so they go through the proxy even when they ignore the environment:
64
+
65
+ - requests
66
+ - urllib (standard library)
67
+ - urllib3
68
+ - httpx
69
+ - aiohttp
70
+ - httplib2
71
+ - niquests
72
+ - curl_cffi
73
+ - tornado (needs pycurl; without it, tornado is left alone and reported as not proxied)
74
+
75
+ Covered through the proxy environment variables that `ulak` sets:
76
+
77
+ - pycurl and libcurl
78
+ - botocore and boto3, Google API clients
79
+ - any child process started with subprocess, such as curl or wget
80
+
81
+ ## How to use
82
+
83
+ ```python
84
+ import ulak
85
+
86
+ ulak.proxy("127.0.0.1", 8080) # verify off by default
87
+ ulak.proxy("127.0.0.1", 8080, verify=True) # keep normal TLS checks
88
+ ulak.proxy("127.0.0.1", 8080, verify="burp-ca.pem") # trust your proxy's CA
89
+ ulak.off() # undo everything
90
+ ```
91
+
92
+ Only for one block of code:
93
+
94
+ ```python
95
+ with ulak.proxied("127.0.0.1", 8080):
96
+ run_the_thing()
97
+ ```
98
+
99
+ Check the current state:
100
+
101
+ ```python
102
+ ulak.status() # what was patched, skipped, or failed
103
+ ulak.is_active() # True or False
104
+ ```
105
+
106
+ ## A note on TLS
107
+
108
+ A proxy like Burp breaks into TLS with its own certificate authority, which your client does not trust. That is why `verify` is off by default, so HTTPS requests do not fail. This turns off certificate checking, so use it only with a local proxy you control. The cleaner way is to export your proxy's CA certificate and pass `verify="path/to/ca.pem"`, which keeps verification on and trusts the proxy.
109
+
110
+ ## Limits
111
+
112
+ `ulak` works at the library level, so it cannot catch traffic that skips those libraries, such as raw socket clients or libraries that carry their own TLS stack in C. When you call `ulak.off()`, every change is reverted and the environment is restored to how it was.
113
+
114
+ ## License
115
+
116
+ MIT
ulak-0.1.0/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # ulak
2
+
3
+ Send every HTTP and HTTPS request a Python script makes through your proxy, with a single import, without changing the script.
4
+
5
+ ## The problem
6
+
7
+ When you run a script someone else wrote, you often want to see the HTTP traffic it sends. You can inspect it in Burp, mitmproxy, or ZAP, but getting there is painful. Either you set a system wide proxy, or you hunt down every request call in the code and add a proxy argument by hand. And Python has many HTTP libraries, so there is no single place to change.
8
+
9
+ ## The solution
10
+
11
+ `ulak` does it in one line. Add it to the top of the script and all outbound traffic goes through the proxy you pick. The name is the Turkish word for a courier who carries a message from one post to the next, which is what this does for the script's requests.
12
+
13
+ ```python
14
+ import ulak
15
+ ulak.proxy("127.0.0.1", 8080)
16
+ ```
17
+
18
+ Every request after that line is routed through `127.0.0.1:8080`.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install ulak
24
+ ```
25
+
26
+ There are no required dependencies. A library is only hooked if the script already uses it.
27
+
28
+ ## What it covers
29
+
30
+ Hooked directly, so they go through the proxy even when they ignore the environment:
31
+
32
+ - requests
33
+ - urllib (standard library)
34
+ - urllib3
35
+ - httpx
36
+ - aiohttp
37
+ - httplib2
38
+ - niquests
39
+ - curl_cffi
40
+ - tornado (needs pycurl; without it, tornado is left alone and reported as not proxied)
41
+
42
+ Covered through the proxy environment variables that `ulak` sets:
43
+
44
+ - pycurl and libcurl
45
+ - botocore and boto3, Google API clients
46
+ - any child process started with subprocess, such as curl or wget
47
+
48
+ ## How to use
49
+
50
+ ```python
51
+ import ulak
52
+
53
+ ulak.proxy("127.0.0.1", 8080) # verify off by default
54
+ ulak.proxy("127.0.0.1", 8080, verify=True) # keep normal TLS checks
55
+ ulak.proxy("127.0.0.1", 8080, verify="burp-ca.pem") # trust your proxy's CA
56
+ ulak.off() # undo everything
57
+ ```
58
+
59
+ Only for one block of code:
60
+
61
+ ```python
62
+ with ulak.proxied("127.0.0.1", 8080):
63
+ run_the_thing()
64
+ ```
65
+
66
+ Check the current state:
67
+
68
+ ```python
69
+ ulak.status() # what was patched, skipped, or failed
70
+ ulak.is_active() # True or False
71
+ ```
72
+
73
+ ## A note on TLS
74
+
75
+ A proxy like Burp breaks into TLS with its own certificate authority, which your client does not trust. That is why `verify` is off by default, so HTTPS requests do not fail. This turns off certificate checking, so use it only with a local proxy you control. The cleaner way is to export your proxy's CA certificate and pass `verify="path/to/ca.pem"`, which keeps verification on and trusts the proxy.
76
+
77
+ ## Limits
78
+
79
+ `ulak` works at the library level, so it cannot catch traffic that skips those libraries, such as raw socket clients or libraries that carry their own TLS stack in C. When you call `ulak.off()`, every change is reverted and the environment is restored to how it was.
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ulak"
7
+ dynamic = ["version"]
8
+ description = "Drop-in HTTP(S) proxy hook: route all of a Python script's traffic through Burp/mitmproxy/ZAP with one import."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Musa SANA", email = "musa.sana@hotmail.com" }]
14
+ maintainers = [{ name = "Musa SANA", email = "musa.sana@hotmail.com" }]
15
+ keywords = ["proxy", "http", "https", "burp", "mitmproxy", "interception", "security", "monkeypatch", "debugging"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "Topic :: Security",
20
+ "Topic :: Internet :: Proxy Servers",
21
+ "Topic :: Software Development :: Testing",
22
+ "Programming Language :: Python :: 3",
23
+ "Operating System :: OS Independent",
24
+ ]
25
+
26
+ # No required dependencies: every HTTP library is patched only if it is present.
27
+
28
+ [project.optional-dependencies]
29
+ # Install the clients you want covered in tests, e.g. pip install ulak[test]
30
+ test = ["requests", "httpx", "aiohttp", "urllib3", "niquests", "httplib2", "curl_cffi", "pytest"]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/musana/ulak"
34
+ Repository = "https://github.com/musana/ulak"
35
+ Issues = "https://github.com/musana/ulak/issues"
36
+ Changelog = "https://github.com/musana/ulak/releases"
37
+
38
+ [tool.setuptools.dynamic]
39
+ version = { attr = "ulak.__version__" }
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
ulak-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,602 @@
1
+ """
2
+ ulak - a drop-in HTTP(S) proxy hook for arbitrary Python scripts.
3
+
4
+ The name comes from the Turkish word for a courier/relay rider: a messenger
5
+ who carries dispatches from one post to the next. This library does the same
6
+ for a script's network traffic: it quietly relays every HTTP/HTTPS request the
7
+ script makes through a proxy of your choice (Burp Suite, mitmproxy, ZAP, your
8
+ own listener), without touching the script's source code.
9
+
10
+ Why
11
+ ---
12
+ There is no single built-in switch that reroutes *all* outbound HTTP in a
13
+ Python process: every HTTP library opens its own socket and builds its own
14
+ connection. ``ulak`` therefore works on two layers at once:
15
+
16
+ 1. It sets the standard proxy environment variables, which many clients read.
17
+ 2. It monkey-patches the entry points of the well-known HTTP libraries, to
18
+ force the proxy even on clients that ignore the environment.
19
+
20
+ Usage
21
+ -----
22
+ Add two lines at the very top of the target script::
23
+
24
+ import ulak
25
+ ulak.proxy("127.0.0.1", 8080)
26
+
27
+ Every request made after that line flows through 127.0.0.1:8080.
28
+
29
+ ulak.proxy("127.0.0.1", 8080) # verify disabled (MITM default)
30
+ ulak.proxy("127.0.0.1", 8080, verify=True) # keep normal TLS verification
31
+ ulak.proxy("127.0.0.1", 8080, verify="burp-ca.pem")# trust the proxy's CA bundle
32
+ ulak.off() # revert everything
33
+
34
+ As a context manager::
35
+
36
+ with ulak.proxied("127.0.0.1", 8080):
37
+ run_the_thing()
38
+
39
+ Coverage
40
+ --------
41
+ Directly patched: requests, urllib (stdlib), urllib3, httpx, aiohttp,
42
+ httplib2, niquests, curl_cffi, tornado. Covered via environment variables:
43
+ pycurl / libcurl, botocore / boto3, google-api clients, and any child process
44
+ spawned with subprocess (e.g. curl, wget) since children inherit the
45
+ environment. See README for the full matrix and the known limits.
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ import logging
51
+ import os
52
+ import ssl
53
+ import sys
54
+ import warnings
55
+ from typing import Callable, Dict, List, Union
56
+
57
+ __all__ = ["proxy", "off", "proxied", "status", "is_active"]
58
+ __version__ = "0.1.0"
59
+
60
+ Verify = Union[bool, str]
61
+
62
+ logger = logging.getLogger("ulak")
63
+ logger.addHandler(logging.NullHandler())
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Internal state
67
+ # ---------------------------------------------------------------------------
68
+
69
+ _STATE: Dict[str, object] = {
70
+ "active": False,
71
+ "host": None,
72
+ "port": None,
73
+ "url": None,
74
+ "verify": None, # True / False / path to a CA bundle
75
+ "patched": [], # libraries successfully patched
76
+ "skipped": [], # libraries not installed, skipped
77
+ "unsupported": {}, # libraries present but not proxyable here (with reason)
78
+ "errors": {}, # libraries that raised while patching
79
+ }
80
+
81
+ # Originals, kept so off() can restore them exactly.
82
+ _ORIG: Dict[str, object] = {}
83
+ _SAVED_ENV: Dict[str, Union[str, None]] = {}
84
+
85
+ _VERBOSE = True
86
+
87
+
88
+ def _banner(msg: str) -> None:
89
+ logger.info(msg)
90
+ if _VERBOSE:
91
+ print(f"[ulak] {msg}", file=sys.stderr)
92
+
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Environment variables
96
+ # ---------------------------------------------------------------------------
97
+
98
+ _PROXY_ENV = ("HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy",
99
+ "ALL_PROXY", "all_proxy")
100
+ _NO_PROXY_ENV = ("NO_PROXY", "no_proxy")
101
+
102
+
103
+ def _set_env(proxy_url: str) -> None:
104
+ if not _SAVED_ENV:
105
+ for key in _PROXY_ENV + _NO_PROXY_ENV:
106
+ _SAVED_ENV[key] = os.environ.get(key)
107
+ for key in _PROXY_ENV:
108
+ os.environ[key] = proxy_url
109
+ # A NO_PROXY set by the target script would exempt some hosts; clear it so
110
+ # that everything is routed through the proxy.
111
+ for key in _NO_PROXY_ENV:
112
+ os.environ.pop(key, None)
113
+
114
+
115
+ def _restore_env() -> None:
116
+ for key, value in _SAVED_ENV.items():
117
+ if value is None:
118
+ os.environ.pop(key, None)
119
+ else:
120
+ os.environ[key] = value
121
+ _SAVED_ENV.clear()
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # TLS helpers
126
+ # ---------------------------------------------------------------------------
127
+
128
+ def _make_ssl_context(verify: Verify) -> ssl.SSLContext:
129
+ """Build an SSLContext matching the requested verification policy."""
130
+ if verify is False:
131
+ ctx = ssl.create_default_context()
132
+ ctx.check_hostname = False
133
+ ctx.verify_mode = ssl.CERT_NONE
134
+ return ctx
135
+ if isinstance(verify, str):
136
+ return ssl.create_default_context(cafile=verify)
137
+ return ssl.create_default_context()
138
+
139
+
140
+ def _silence_insecure_warnings() -> None:
141
+ if _STATE["verify"] is not False:
142
+ return
143
+ try:
144
+ import urllib3
145
+ urllib3.disable_warnings()
146
+ except Exception: # pragma: no cover - urllib3 may be absent
147
+ pass
148
+ try:
149
+ from urllib3.exceptions import InsecureRequestWarning
150
+ warnings.simplefilter("ignore", InsecureRequestWarning)
151
+ except Exception: # pragma: no cover
152
+ pass
153
+
154
+
155
+ # ===========================================================================
156
+ # Patchers. Each returns True when it did something, raises ImportError when
157
+ # its library is absent, and is otherwise wrapped so a failure cannot crash
158
+ # the host script.
159
+ # ===========================================================================
160
+
161
+ def _patch_env() -> bool:
162
+ _set_env(str(_STATE["url"]))
163
+ return True
164
+
165
+
166
+ def _patch_ssl() -> bool:
167
+ """Globally relax/redirect stdlib TLS verification when asked to."""
168
+ verify = _STATE["verify"]
169
+ if verify is True:
170
+ return False
171
+ _ORIG.setdefault("ssl._create_default_https_context",
172
+ ssl._create_default_https_context)
173
+ if verify is False:
174
+ ssl._create_default_https_context = ssl._create_unverified_context
175
+ else:
176
+ cafile = verify
177
+ ssl._create_default_https_context = (
178
+ lambda *a, **k: ssl.create_default_context(cafile=cafile))
179
+ return True
180
+
181
+
182
+ def _patch_urllib() -> bool:
183
+ import urllib.request as ur
184
+
185
+ proxy_url = str(_STATE["url"])
186
+ handlers: List[object] = [
187
+ ur.ProxyHandler({"http": proxy_url, "https": proxy_url})
188
+ ]
189
+ if _STATE["verify"] is not True:
190
+ handlers.append(ur.HTTPSHandler(context=_make_ssl_context(_STATE["verify"])))
191
+
192
+ _ORIG.setdefault("urllib.opener", ur._opener)
193
+ ur.install_opener(ur.build_opener(*handlers))
194
+ return True
195
+
196
+
197
+ def _patch_requests() -> bool:
198
+ import requests # noqa: F401
199
+ from requests.sessions import Session
200
+
201
+ _ORIG.setdefault("requests.Session.request", Session.request)
202
+ orig = _ORIG["requests.Session.request"]
203
+ proxies = {"http": _STATE["url"], "https": _STATE["url"]}
204
+ verify = _STATE["verify"]
205
+
206
+ def request(self, method, url, **kwargs):
207
+ kwargs["proxies"] = proxies
208
+ kwargs["verify"] = verify
209
+ return orig(self, method, url, **kwargs)
210
+
211
+ Session.request = request
212
+ _silence_insecure_warnings()
213
+ return True
214
+
215
+
216
+ def _patch_niquests() -> bool:
217
+ """niquests is a requests-compatible fork with its own module."""
218
+ import niquests
219
+ Session = niquests.Session
220
+
221
+ _ORIG.setdefault("niquests.Session.request", Session.request)
222
+ orig = _ORIG["niquests.Session.request"]
223
+ proxies = {"http": _STATE["url"], "https": _STATE["url"]}
224
+ verify = _STATE["verify"]
225
+
226
+ def request(self, method, url, **kwargs):
227
+ kwargs["proxies"] = proxies
228
+ kwargs["verify"] = verify
229
+ return orig(self, method, url, **kwargs)
230
+
231
+ Session.request = request
232
+ return True
233
+
234
+
235
+ def _patch_urllib3() -> bool:
236
+ """urllib3 ignores the environment and uses its own PoolManager, so we
237
+ swap PoolManager for a ProxyManager bound to the proxy. requests binds its
238
+ own PoolManager reference at import time, so this does not affect it."""
239
+ import urllib3
240
+
241
+ proxy_url = str(_STATE["url"])
242
+ verify = _STATE["verify"]
243
+
244
+ _ORIG.setdefault("urllib3.PoolManager", urllib3.PoolManager)
245
+ base_proxy_manager = urllib3.ProxyManager
246
+
247
+ extra: Dict[str, object] = {}
248
+ if verify is False:
249
+ extra["cert_reqs"] = "CERT_NONE"
250
+ elif isinstance(verify, str):
251
+ extra["ca_certs"] = verify
252
+
253
+ class _ProxiedPoolManager(base_proxy_manager): # type: ignore[valid-type,misc]
254
+ def __init__(self, num_pools=10, headers=None, **conn_kw):
255
+ merged = dict(extra)
256
+ merged.update(conn_kw) # caller-supplied values win
257
+ super().__init__(proxy_url, num_pools=num_pools,
258
+ headers=headers, **merged)
259
+
260
+ urllib3.PoolManager = _ProxiedPoolManager
261
+ _silence_insecure_warnings()
262
+ return True
263
+
264
+
265
+ def _patch_httpx() -> bool:
266
+ import inspect
267
+ import httpx
268
+
269
+ proxy_url = str(_STATE["url"])
270
+ verify = _STATE["verify"]
271
+ patched_any = False
272
+
273
+ for cls_name in ("Client", "AsyncClient"):
274
+ cls = getattr(httpx, cls_name, None)
275
+ if cls is None:
276
+ continue
277
+ key = f"httpx.{cls_name}.__init__"
278
+ _ORIG.setdefault(key, cls.__init__)
279
+ orig_init = _ORIG[key]
280
+ params = inspect.signature(orig_init).parameters
281
+
282
+ def make_init(orig_init, params):
283
+ def __init__(self, *args, **kwargs):
284
+ # httpx renamed the argument: "proxies" (<0.26) -> "proxy".
285
+ if "proxy" in params:
286
+ kwargs.setdefault("proxy", proxy_url)
287
+ elif "proxies" in params:
288
+ kwargs.setdefault("proxies", proxy_url)
289
+ if "verify" in params:
290
+ kwargs["verify"] = verify
291
+ return orig_init(self, *args, **kwargs)
292
+ return __init__
293
+
294
+ cls.__init__ = make_init(orig_init, params)
295
+ patched_any = True
296
+ return patched_any
297
+
298
+
299
+ def _patch_aiohttp() -> bool:
300
+ import aiohttp
301
+
302
+ proxy_url = str(_STATE["url"])
303
+ verify = _STATE["verify"]
304
+ cls = aiohttp.ClientSession
305
+
306
+ _ORIG.setdefault("aiohttp.ClientSession._request", cls._request)
307
+ orig = _ORIG["aiohttp.ClientSession._request"]
308
+
309
+ async def _request(self, method, str_or_url, **kwargs):
310
+ kwargs.setdefault("proxy", proxy_url)
311
+ if verify is False:
312
+ kwargs.setdefault("ssl", False)
313
+ elif isinstance(verify, str):
314
+ kwargs.setdefault("ssl", _make_ssl_context(verify))
315
+ return await orig(self, method, str_or_url, **kwargs)
316
+
317
+ cls._request = _request
318
+ return True
319
+
320
+
321
+ def _patch_httplib2() -> bool:
322
+ import httplib2
323
+
324
+ host, port = _STATE["host"], _STATE["port"]
325
+ verify = _STATE["verify"]
326
+
327
+ _ORIG.setdefault("httplib2.Http.__init__", httplib2.Http.__init__)
328
+ orig = _ORIG["httplib2.Http.__init__"]
329
+ proxy_info = httplib2.ProxyInfo(httplib2.socks.PROXY_TYPE_HTTP, host, int(port))
330
+
331
+ def __init__(self, *args, **kwargs):
332
+ kwargs["proxy_info"] = proxy_info
333
+ if verify is False:
334
+ kwargs["disable_ssl_certificate_validation"] = True
335
+ elif isinstance(verify, str):
336
+ kwargs["ca_certs"] = verify
337
+ orig(self, *args, **kwargs)
338
+
339
+ httplib2.Http.__init__ = __init__
340
+ return True
341
+
342
+
343
+ def _patch_curl_cffi() -> bool:
344
+ """curl_cffi exposes a requests-like API (sync Session and AsyncSession)."""
345
+ from curl_cffi import requests as cffi
346
+
347
+ proxies = {"http": _STATE["url"], "https": _STATE["url"]}
348
+ verify = _STATE["verify"]
349
+ patched_any = False
350
+
351
+ for cls_name in ("Session", "AsyncSession"):
352
+ cls = getattr(cffi, cls_name, None)
353
+ if cls is None or not hasattr(cls, "request"):
354
+ continue
355
+ key = f"curl_cffi.{cls_name}.request"
356
+ _ORIG.setdefault(key, cls.request)
357
+ orig = _ORIG[key]
358
+
359
+ def make_request(orig):
360
+ def request(self, method, url, **kwargs):
361
+ kwargs["proxies"] = proxies
362
+ kwargs["verify"] = verify
363
+ return orig(self, method, url, **kwargs)
364
+ return request
365
+
366
+ cls.request = make_request(orig)
367
+ patched_any = True
368
+ return patched_any
369
+
370
+
371
+ def _patch_tornado() -> bool:
372
+ """Route tornado through the proxy.
373
+
374
+ tornado's default (simple) client has no proxy support and raises if you
375
+ pass ``proxy_host``. Only its libcurl-backed client can proxy, so we switch
376
+ tornado to that client (requires pycurl) and inject the proxy fields into
377
+ every HTTPRequest. Without pycurl we cannot proxy tornado without breaking
378
+ it, so we raise NotImplementedError and leave tornado untouched.
379
+ """
380
+ try:
381
+ import pycurl # noqa: F401
382
+ except ImportError:
383
+ raise NotImplementedError(
384
+ "tornado can only be proxied via its libcurl client; install "
385
+ "pycurl to enable tornado interception")
386
+
387
+ import tornado.httpclient as thc
388
+
389
+ host, port = _STATE["host"], _STATE["port"]
390
+ verify = _STATE["verify"]
391
+
392
+ if "tornado.impl" not in _ORIG:
393
+ _ORIG["tornado.impl"] = getattr(thc.AsyncHTTPClient, "_impl_class", None)
394
+ thc.AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
395
+
396
+ _ORIG.setdefault("tornado.HTTPRequest.__init__", thc.HTTPRequest.__init__)
397
+ orig = _ORIG["tornado.HTTPRequest.__init__"]
398
+
399
+ def __init__(self, *args, **kwargs):
400
+ kwargs.setdefault("proxy_host", host)
401
+ kwargs.setdefault("proxy_port", int(port))
402
+ if verify is False:
403
+ kwargs.setdefault("validate_cert", False)
404
+ elif isinstance(verify, str):
405
+ kwargs.setdefault("ca_certs", verify)
406
+ orig(self, *args, **kwargs)
407
+
408
+ thc.HTTPRequest.__init__ = __init__
409
+ return True
410
+
411
+
412
+ # (name, patcher, restorer-key-prefix). Order matters: env and ssl first.
413
+ _PATCHERS: List[tuple] = [
414
+ ("env", _patch_env),
415
+ ("ssl", _patch_ssl),
416
+ ("urllib", _patch_urllib),
417
+ ("requests", _patch_requests),
418
+ ("niquests", _patch_niquests),
419
+ ("urllib3", _patch_urllib3),
420
+ ("httpx", _patch_httpx),
421
+ ("aiohttp", _patch_aiohttp),
422
+ ("httplib2", _patch_httplib2),
423
+ ("curl_cffi", _patch_curl_cffi),
424
+ ("tornado", _patch_tornado),
425
+ ]
426
+
427
+
428
+ # ---------------------------------------------------------------------------
429
+ # Restore logic (mirror of the patchers)
430
+ # ---------------------------------------------------------------------------
431
+
432
+ def _restore_all() -> None:
433
+ if "ssl._create_default_https_context" in _ORIG:
434
+ ssl._create_default_https_context = _ORIG.pop("ssl._create_default_https_context")
435
+
436
+ if "urllib.opener" in _ORIG:
437
+ import urllib.request as ur
438
+ saved = _ORIG.pop("urllib.opener")
439
+ if saved is None:
440
+ ur._opener = None
441
+ else:
442
+ ur.install_opener(saved)
443
+
444
+ if "requests.Session.request" in _ORIG:
445
+ from requests.sessions import Session
446
+ Session.request = _ORIG.pop("requests.Session.request")
447
+
448
+ if "niquests.Session.request" in _ORIG:
449
+ import niquests
450
+ niquests.Session.request = _ORIG.pop("niquests.Session.request")
451
+
452
+ if "urllib3.PoolManager" in _ORIG:
453
+ import urllib3
454
+ urllib3.PoolManager = _ORIG.pop("urllib3.PoolManager")
455
+
456
+ for cls_name in ("Client", "AsyncClient"):
457
+ key = f"httpx.{cls_name}.__init__"
458
+ if key in _ORIG:
459
+ import httpx
460
+ getattr(httpx, cls_name).__init__ = _ORIG.pop(key)
461
+
462
+ if "aiohttp.ClientSession._request" in _ORIG:
463
+ import aiohttp
464
+ aiohttp.ClientSession._request = _ORIG.pop("aiohttp.ClientSession._request")
465
+
466
+ if "httplib2.Http.__init__" in _ORIG:
467
+ import httplib2
468
+ httplib2.Http.__init__ = _ORIG.pop("httplib2.Http.__init__")
469
+
470
+ for cls_name in ("Session", "AsyncSession"):
471
+ key = f"curl_cffi.{cls_name}.request"
472
+ if key in _ORIG:
473
+ from curl_cffi import requests as cffi
474
+ getattr(cffi, cls_name).request = _ORIG.pop(key)
475
+
476
+ if "tornado.HTTPRequest.__init__" in _ORIG:
477
+ import tornado.httpclient as thc
478
+ thc.HTTPRequest.__init__ = _ORIG.pop("tornado.HTTPRequest.__init__")
479
+ if "tornado.impl" in _ORIG:
480
+ import tornado.httpclient as thc
481
+ prior = _ORIG.pop("tornado.impl")
482
+ thc.AsyncHTTPClient.configure(prior)
483
+
484
+ _restore_env()
485
+
486
+
487
+ # ---------------------------------------------------------------------------
488
+ # Public API
489
+ # ---------------------------------------------------------------------------
490
+
491
+ def proxy(host: str = "127.0.0.1",
492
+ port: int = 8080,
493
+ *,
494
+ verify: Verify = False,
495
+ scheme: str = "http",
496
+ verbose: bool = True) -> Dict[str, object]:
497
+ """Route all outbound HTTP/HTTPS traffic of this process through a proxy.
498
+
499
+ Parameters
500
+ ----------
501
+ host, port:
502
+ Proxy address. Defaults to 127.0.0.1:8080 (Burp Suite's default).
503
+ verify:
504
+ TLS verification policy.
505
+ ``False`` (default) disables verification, which is what an
506
+ intercepting proxy with its own CA normally requires. ``True`` keeps
507
+ normal verification. A string is treated as a path to a CA bundle to
508
+ trust (export your proxy's CA and pass it here).
509
+ scheme:
510
+ Proxy scheme, ``"http"`` by default (Burp/ZAP/mitmproxy are HTTP
511
+ proxies).
512
+ verbose:
513
+ Print a one-line activation banner to stderr.
514
+
515
+ Calling this again re-applies with the new settings (the previous hook is
516
+ reverted first). Returns a status dict describing what was patched.
517
+ """
518
+ global _VERBOSE
519
+ _VERBOSE = verbose
520
+
521
+ if _STATE["active"]:
522
+ off()
523
+
524
+ proxy_url = f"{scheme}://{host}:{port}"
525
+ _STATE.update({
526
+ "active": True,
527
+ "host": host,
528
+ "port": port,
529
+ "url": proxy_url,
530
+ "verify": verify,
531
+ "patched": [],
532
+ "skipped": [],
533
+ "unsupported": {},
534
+ "errors": {},
535
+ })
536
+
537
+ for name, fn in _PATCHERS:
538
+ try:
539
+ if fn():
540
+ _STATE["patched"].append(name) # type: ignore[union-attr]
541
+ except ImportError:
542
+ _STATE["skipped"].append(name) # type: ignore[union-attr]
543
+ except NotImplementedError as exc:
544
+ _STATE["unsupported"][name] = str(exc) # type: ignore[index]
545
+ except Exception as exc: # noqa: BLE001 - never crash the host script
546
+ _STATE["errors"][name] = repr(exc) # type: ignore[index]
547
+ logger.exception("ulak: patching %s failed", name)
548
+
549
+ _banner(f"routing HTTP/HTTPS through {proxy_url} (verify={verify!r})")
550
+ _banner("patched: " + ", ".join(_STATE["patched"])) # type: ignore[arg-type]
551
+ if _STATE["unsupported"]:
552
+ for lib, reason in _STATE["unsupported"].items(): # type: ignore[attr-defined]
553
+ _banner(f"note: {lib} not proxied ({reason})")
554
+ if _STATE["errors"]:
555
+ _banner(f"errors: {_STATE['errors']}")
556
+ if verify is False:
557
+ _banner("WARNING: TLS verification is DISABLED; use only with a proxy you trust.")
558
+ return status()
559
+
560
+
561
+ def off() -> None:
562
+ """Revert every patch and restore the original environment variables."""
563
+ if not _STATE["active"]:
564
+ return
565
+ try:
566
+ _restore_all()
567
+ finally:
568
+ _banner("proxy disabled; all hooks reverted.")
569
+ _STATE.update({
570
+ "active": False, "host": None, "port": None, "url": None,
571
+ "verify": None, "patched": [], "skipped": [],
572
+ "unsupported": {}, "errors": {},
573
+ })
574
+
575
+
576
+ def is_active() -> bool:
577
+ """Return whether a proxy hook is currently installed."""
578
+ return bool(_STATE["active"])
579
+
580
+
581
+ def status() -> Dict[str, object]:
582
+ """Return a copy of the current state."""
583
+ return dict(_STATE)
584
+
585
+
586
+ class proxied:
587
+ """Context manager: proxy on inside the block, reverted on exit.
588
+
589
+ >>> with proxied("127.0.0.1", 8080):
590
+ ... do_requests()
591
+ """
592
+
593
+ def __init__(self, host: str = "127.0.0.1", port: int = 8080, **kwargs):
594
+ self._args = (host, port)
595
+ self._kwargs = kwargs
596
+
597
+ def __enter__(self) -> Dict[str, object]:
598
+ return proxy(*self._args, **self._kwargs)
599
+
600
+ def __exit__(self, *exc) -> bool:
601
+ off()
602
+ return False
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: ulak
3
+ Version: 0.1.0
4
+ Summary: Drop-in HTTP(S) proxy hook: route all of a Python script's traffic through Burp/mitmproxy/ZAP with one import.
5
+ Author-email: Musa SANA <musa.sana@hotmail.com>
6
+ Maintainer-email: Musa SANA <musa.sana@hotmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/musana/ulak
9
+ Project-URL: Repository, https://github.com/musana/ulak
10
+ Project-URL: Issues, https://github.com/musana/ulak/issues
11
+ Project-URL: Changelog, https://github.com/musana/ulak/releases
12
+ Keywords: proxy,http,https,burp,mitmproxy,interception,security,monkeypatch,debugging
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Topic :: Security
16
+ Classifier: Topic :: Internet :: Proxy Servers
17
+ Classifier: Topic :: Software Development :: Testing
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Operating System :: OS Independent
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: test
24
+ Requires-Dist: requests; extra == "test"
25
+ Requires-Dist: httpx; extra == "test"
26
+ Requires-Dist: aiohttp; extra == "test"
27
+ Requires-Dist: urllib3; extra == "test"
28
+ Requires-Dist: niquests; extra == "test"
29
+ Requires-Dist: httplib2; extra == "test"
30
+ Requires-Dist: curl_cffi; extra == "test"
31
+ Requires-Dist: pytest; extra == "test"
32
+ Dynamic: license-file
33
+
34
+ # ulak
35
+
36
+ Send every HTTP and HTTPS request a Python script makes through your proxy, with a single import, without changing the script.
37
+
38
+ ## The problem
39
+
40
+ When you run a script someone else wrote, you often want to see the HTTP traffic it sends. You can inspect it in Burp, mitmproxy, or ZAP, but getting there is painful. Either you set a system wide proxy, or you hunt down every request call in the code and add a proxy argument by hand. And Python has many HTTP libraries, so there is no single place to change.
41
+
42
+ ## The solution
43
+
44
+ `ulak` does it in one line. Add it to the top of the script and all outbound traffic goes through the proxy you pick. The name is the Turkish word for a courier who carries a message from one post to the next, which is what this does for the script's requests.
45
+
46
+ ```python
47
+ import ulak
48
+ ulak.proxy("127.0.0.1", 8080)
49
+ ```
50
+
51
+ Every request after that line is routed through `127.0.0.1:8080`.
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pip install ulak
57
+ ```
58
+
59
+ There are no required dependencies. A library is only hooked if the script already uses it.
60
+
61
+ ## What it covers
62
+
63
+ Hooked directly, so they go through the proxy even when they ignore the environment:
64
+
65
+ - requests
66
+ - urllib (standard library)
67
+ - urllib3
68
+ - httpx
69
+ - aiohttp
70
+ - httplib2
71
+ - niquests
72
+ - curl_cffi
73
+ - tornado (needs pycurl; without it, tornado is left alone and reported as not proxied)
74
+
75
+ Covered through the proxy environment variables that `ulak` sets:
76
+
77
+ - pycurl and libcurl
78
+ - botocore and boto3, Google API clients
79
+ - any child process started with subprocess, such as curl or wget
80
+
81
+ ## How to use
82
+
83
+ ```python
84
+ import ulak
85
+
86
+ ulak.proxy("127.0.0.1", 8080) # verify off by default
87
+ ulak.proxy("127.0.0.1", 8080, verify=True) # keep normal TLS checks
88
+ ulak.proxy("127.0.0.1", 8080, verify="burp-ca.pem") # trust your proxy's CA
89
+ ulak.off() # undo everything
90
+ ```
91
+
92
+ Only for one block of code:
93
+
94
+ ```python
95
+ with ulak.proxied("127.0.0.1", 8080):
96
+ run_the_thing()
97
+ ```
98
+
99
+ Check the current state:
100
+
101
+ ```python
102
+ ulak.status() # what was patched, skipped, or failed
103
+ ulak.is_active() # True or False
104
+ ```
105
+
106
+ ## A note on TLS
107
+
108
+ A proxy like Burp breaks into TLS with its own certificate authority, which your client does not trust. That is why `verify` is off by default, so HTTPS requests do not fail. This turns off certificate checking, so use it only with a local proxy you control. The cleaner way is to export your proxy's CA certificate and pass `verify="path/to/ca.pem"`, which keeps verification on and trusts the proxy.
109
+
110
+ ## Limits
111
+
112
+ `ulak` works at the library level, so it cannot catch traffic that skips those libraries, such as raw socket clients or libraries that carry their own TLS stack in C. When you call `ulak.off()`, every change is reverted and the environment is restored to how it was.
113
+
114
+ ## License
115
+
116
+ MIT
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/ulak/__init__.py
5
+ src/ulak.egg-info/PKG-INFO
6
+ src/ulak.egg-info/SOURCES.txt
7
+ src/ulak.egg-info/dependency_links.txt
8
+ src/ulak.egg-info/requires.txt
9
+ src/ulak.egg-info/top_level.txt
10
+ tests/test_ulak.py
@@ -0,0 +1,10 @@
1
+
2
+ [test]
3
+ requests
4
+ httpx
5
+ aiohttp
6
+ urllib3
7
+ niquests
8
+ httplib2
9
+ curl_cffi
10
+ pytest
@@ -0,0 +1 @@
1
+ ulak
@@ -0,0 +1,239 @@
1
+ """Tests for ulak.
2
+
3
+ A tiny in-process transparent proxy is started on an ephemeral port. Each
4
+ supported HTTP client is pointed at it and we assert the proxy actually saw
5
+ the connection. Clients that are not installed are skipped. Network access to
6
+ example.com is required for the live-traffic tests.
7
+ """
8
+ import asyncio
9
+ import importlib.util
10
+ import select
11
+ import socket
12
+ import ssl
13
+ import threading
14
+
15
+ import pytest
16
+
17
+ import ulak
18
+
19
+
20
+ # --------------------------------------------------------------------------
21
+ # In-process transparent proxy (HTTP absolute-form + HTTPS CONNECT tunnel)
22
+ # --------------------------------------------------------------------------
23
+
24
+ class TunnelProxy:
25
+ def __init__(self):
26
+ self.seen = []
27
+ self._srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
28
+ self._srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
29
+ self._srv.bind(("127.0.0.1", 0))
30
+ self._srv.listen(50)
31
+ self.host, self.port = self._srv.getsockname()
32
+ self._stop = False
33
+ self._thread = threading.Thread(target=self._serve, daemon=True)
34
+ self._thread.start()
35
+
36
+ def _serve(self):
37
+ while not self._stop:
38
+ try:
39
+ self._srv.settimeout(0.5)
40
+ client, _ = self._srv.accept()
41
+ except socket.timeout:
42
+ continue
43
+ except OSError:
44
+ break
45
+ threading.Thread(target=self._handle, args=(client,), daemon=True).start()
46
+
47
+ def _handle(self, client):
48
+ try:
49
+ req = b""
50
+ while b"\r\n\r\n" not in req:
51
+ chunk = client.recv(4096)
52
+ if not chunk:
53
+ return
54
+ req += chunk
55
+ method, target, _ = req.split(b"\r\n", 1)[0].decode("latin1").split(" ", 2)
56
+ self.seen.append((method, target))
57
+ if method == "CONNECT":
58
+ host, port = target.split(":")
59
+ upstream = socket.create_connection((host, int(port)))
60
+ client.sendall(b"HTTP/1.1 200 Connection established\r\n\r\n")
61
+ self._pipe(client, upstream)
62
+ else:
63
+ from urllib.parse import urlparse
64
+ u = urlparse(target)
65
+ upstream = socket.create_connection((u.hostname, u.port or 80))
66
+ upstream.sendall(req)
67
+ self._pipe(client, upstream)
68
+ except Exception:
69
+ pass
70
+ finally:
71
+ client.close()
72
+
73
+ @staticmethod
74
+ def _pipe(a, b):
75
+ try:
76
+ while True:
77
+ r, _, _ = select.select([a, b], [], [], 15)
78
+ if not r:
79
+ return
80
+ for s in r:
81
+ data = s.recv(65536)
82
+ if not data:
83
+ return
84
+ (b if s is a else a).sendall(data)
85
+ except Exception:
86
+ pass
87
+
88
+ def close(self):
89
+ self._stop = True
90
+ try:
91
+ self._srv.close()
92
+ except OSError:
93
+ pass
94
+
95
+
96
+ @pytest.fixture
97
+ def proxy_server():
98
+ srv = TunnelProxy()
99
+ yield srv
100
+ srv.close()
101
+
102
+
103
+ @pytest.fixture
104
+ def hooked(proxy_server):
105
+ ulak.proxy(proxy_server.host, proxy_server.port, verify=False, verbose=False)
106
+ yield proxy_server
107
+ ulak.off()
108
+
109
+
110
+ def _installed(name):
111
+ return importlib.util.find_spec(name) is not None
112
+
113
+
114
+ HTTPS = "https://example.com/"
115
+ HTTP = "http://example.com/"
116
+
117
+
118
+ # --------------------------------------------------------------------------
119
+ # Live-traffic coverage, one test per client (skipped if not installed)
120
+ # --------------------------------------------------------------------------
121
+
122
+ @pytest.mark.skipif(not _installed("requests"), reason="requests not installed")
123
+ def test_requests(hooked):
124
+ import requests
125
+ assert requests.get(HTTPS, timeout=20).status_code == 200
126
+ assert requests.get(HTTP, timeout=20).status_code == 200
127
+ assert any("example.com" in t for _, t in hooked.seen)
128
+
129
+
130
+ def test_urllib(hooked):
131
+ import urllib.request
132
+ with urllib.request.urlopen(HTTPS, timeout=20) as r:
133
+ assert r.status == 200
134
+ assert hooked.seen
135
+
136
+
137
+ @pytest.mark.skipif(not _installed("urllib3"), reason="urllib3 not installed")
138
+ def test_urllib3(hooked):
139
+ import urllib3
140
+ assert urllib3.PoolManager().request("GET", HTTPS, timeout=20).status == 200
141
+ assert hooked.seen
142
+
143
+
144
+ @pytest.mark.skipif(not _installed("httpx"), reason="httpx not installed")
145
+ def test_httpx(hooked):
146
+ import httpx
147
+ assert httpx.get(HTTPS, timeout=20).status_code == 200
148
+ assert hooked.seen
149
+
150
+
151
+ @pytest.mark.skipif(not _installed("aiohttp"), reason="aiohttp not installed")
152
+ def test_aiohttp(hooked):
153
+ import aiohttp
154
+
155
+ async def go():
156
+ async with aiohttp.ClientSession() as s:
157
+ async with s.get(HTTPS, timeout=aiohttp.ClientTimeout(total=20)) as r:
158
+ return r.status
159
+
160
+ assert asyncio.run(go()) == 200
161
+ assert hooked.seen
162
+
163
+
164
+ @pytest.mark.skipif(not _installed("niquests"), reason="niquests not installed")
165
+ def test_niquests(hooked):
166
+ import niquests
167
+ assert niquests.get(HTTPS, timeout=20).status_code == 200
168
+ assert hooked.seen
169
+
170
+
171
+ @pytest.mark.skipif(not _installed("httplib2"), reason="httplib2 not installed")
172
+ def test_httplib2(hooked):
173
+ import httplib2
174
+ resp, _ = httplib2.Http().request(HTTPS, "GET")
175
+ assert resp.status == 200
176
+ assert hooked.seen
177
+
178
+
179
+ @pytest.mark.skipif(not _installed("curl_cffi"), reason="curl_cffi not installed")
180
+ def test_curl_cffi(hooked):
181
+ from curl_cffi import requests as cffi
182
+ assert cffi.get(HTTPS, timeout=20).status_code == 200
183
+ assert hooked.seen
184
+
185
+
186
+ # --------------------------------------------------------------------------
187
+ # State / lifecycle
188
+ # --------------------------------------------------------------------------
189
+
190
+ def test_revert_restores_everything(proxy_server):
191
+ import ssl as _ssl
192
+ import urllib.request as ur
193
+
194
+ before = {}
195
+ if _installed("requests"):
196
+ from requests.sessions import Session
197
+ before["requests"] = Session.request
198
+ if _installed("urllib3"):
199
+ import urllib3
200
+ before["urllib3"] = urllib3.PoolManager
201
+ before["ssl"] = _ssl._create_default_https_context
202
+ before["opener"] = ur._opener
203
+
204
+ ulak.proxy(proxy_server.host, proxy_server.port, verify=False, verbose=False)
205
+ assert ulak.is_active()
206
+ ulak.off()
207
+ assert not ulak.is_active()
208
+
209
+ if "requests" in before:
210
+ from requests.sessions import Session
211
+ assert Session.request is before["requests"]
212
+ if "urllib3" in before:
213
+ import urllib3
214
+ assert urllib3.PoolManager is before["urllib3"]
215
+ assert _ssl._create_default_https_context is before["ssl"]
216
+ assert ur._opener is before["opener"]
217
+
218
+
219
+ def test_double_off_is_safe():
220
+ ulak.off()
221
+ ulak.off()
222
+
223
+
224
+ def test_context_manager(proxy_server):
225
+ import os
226
+ with ulak.proxied(proxy_server.host, proxy_server.port, verbose=False):
227
+ assert ulak.is_active()
228
+ assert os.environ.get("HTTP_PROXY")
229
+ assert not ulak.is_active()
230
+ assert os.environ.get("HTTP_PROXY") is None
231
+
232
+
233
+ def test_status_reports_patched(proxy_server):
234
+ st = ulak.proxy(proxy_server.host, proxy_server.port, verbose=False)
235
+ try:
236
+ assert "env" in st["patched"]
237
+ assert st["url"].endswith(str(proxy_server.port))
238
+ finally:
239
+ ulak.off()