|
1 | 1 | import os |
2 | | -import socket |
3 | | -import subprocess |
4 | | -import tempfile |
5 | | -import time |
6 | | -from pathlib import Path |
7 | | -from typing import NamedTuple |
8 | 2 |
|
9 | 3 | import pytest |
10 | 4 |
|
@@ -48,257 +42,3 @@ def clear_proxy_env_vars(): |
48 | 42 | os.environ["NO_PROXY"] = original_no_proxy |
49 | 43 | elif "NO_PROXY" in os.environ: |
50 | 44 | del os.environ["NO_PROXY"] |
51 | | - |
52 | | - |
53 | | -class MitmProxyInfo(NamedTuple): |
54 | | - """Information about a running mitmproxy instance. |
55 | | -
|
56 | | - Use this to configure your connection or environment variables as needed. |
57 | | - """ |
58 | | - |
59 | | - host: str |
60 | | - port: int |
61 | | - ca_cert_path: Path |
62 | | - proxy_url: str |
63 | | - |
64 | | - def set_env_vars(self, monkeypatch): |
65 | | - """Helper to set proxy environment variables. |
66 | | -
|
67 | | - Args: |
68 | | - monkeypatch: pytest monkeypatch fixture |
69 | | - """ |
70 | | - monkeypatch.setenv("HTTP_PROXY", self.proxy_url) |
71 | | - monkeypatch.setenv("HTTPS_PROXY", self.proxy_url) |
72 | | - monkeypatch.setenv("REQUESTS_CA_BUNDLE", str(self.ca_cert_path)) |
73 | | - monkeypatch.setenv("CURL_CA_BUNDLE", str(self.ca_cert_path)) |
74 | | - monkeypatch.setenv("SSL_CERT_FILE", str(self.ca_cert_path)) |
75 | | - |
76 | | - |
77 | | -@pytest.fixture(scope="session") |
78 | | -def mitm_proxy(): |
79 | | - """Start mitmproxy for transparent HTTPS proxying in tests. |
80 | | -
|
81 | | - This fixture (session-scoped): |
82 | | - - Starts mitmdump once for all tests |
83 | | - - Waits for CA certificate generation |
84 | | - - Returns proxy information (does NOT set env vars automatically) |
85 | | - - Cleans up after all tests complete |
86 | | -
|
87 | | - The fixture does NOT automatically configure proxy settings. |
88 | | - Tests should explicitly use the proxy via: |
89 | | - 1. Environment variables: mitm_proxy.set_env_vars(monkeypatch) |
90 | | - 2. Connection parameters: conn_cnx(proxy_host=..., proxy_port=...) |
91 | | -
|
92 | | - Yields: |
93 | | - MitmProxyInfo: Information about the running proxy instance |
94 | | - """ |
95 | | - print("\n[MITM] Starting mitmproxy fixture setup...") |
96 | | - |
97 | | - # Check if mitmproxy is available |
98 | | - print("[MITM] Checking if mitmdump is installed...") |
99 | | - try: |
100 | | - subprocess.run( |
101 | | - ["mitmdump", "--version"], |
102 | | - capture_output=True, |
103 | | - check=True, |
104 | | - timeout=5, |
105 | | - ) |
106 | | - print("[MITM] mitmdump is installed and available") |
107 | | - except ( |
108 | | - subprocess.CalledProcessError, |
109 | | - FileNotFoundError, |
110 | | - subprocess.TimeoutExpired, |
111 | | - ) as e: |
112 | | - print(f"[MITM] mitmdump check failed: {e}") |
113 | | - pytest.fail( |
114 | | - "mitmproxy (mitmdump) is not installed. Install with: pip install mitmproxy" |
115 | | - ) |
116 | | - |
117 | | - proxy_host = "127.0.0.1" |
118 | | - |
119 | | - # Create a temporary addon script to capture the assigned port |
120 | | - # This is the recommended approach per mitmproxy maintainers: |
121 | | - # https://github.com/mitmproxy/mitmproxy/discussions/6011 |
122 | | - port_file = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") |
123 | | - port_file_path = port_file.name |
124 | | - port_file.close() |
125 | | - |
126 | | - addon_script = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".py") |
127 | | - addon_script.write( |
128 | | - f''' |
129 | | -from mitmproxy import ctx |
130 | | -
|
131 | | -def running(): |
132 | | - """Called when mitmproxy is fully started and ready.""" |
133 | | - # Get the actual port that was bound (when using --listen-port 0) |
134 | | - # ctx.master.addons.get("proxyserver").listen_addrs() returns: |
135 | | - # [('::', port, 0, 0), ('0.0.0.0', port)] |
136 | | - addrs = ctx.master.addons.get("proxyserver").listen_addrs() |
137 | | - if addrs: |
138 | | - port = addrs[0][1] |
139 | | - with open(r"{port_file_path}", "w") as f: |
140 | | - f.write(str(port)) |
141 | | - ctx.log.info(f"Proxy listening on port {{port}}") |
142 | | -''' |
143 | | - ) |
144 | | - addon_script.close() |
145 | | - addon_script_path = addon_script.name |
146 | | - print(f"[MITM] Created port detection addon: {addon_script_path}") |
147 | | - print(f"[MITM] Port will be written to: {port_file_path}") |
148 | | - |
149 | | - # Start mitmdump with port 0 (let OS assign a free port) - thread-safe! |
150 | | - print(f"[MITM] Starting mitmdump process on {proxy_host}:0 (auto-assign port)...") |
151 | | - mitm_process = subprocess.Popen( |
152 | | - [ |
153 | | - "mitmdump", |
154 | | - "--listen-host", |
155 | | - proxy_host, |
156 | | - "--listen-port", |
157 | | - "0", # OS will assign a free port |
158 | | - "--set", |
159 | | - "connection_strategy=lazy", # Don't connect to upstream unless needed |
160 | | - "--set", |
161 | | - "stream_large_bodies=1m", # Stream large bodies |
162 | | - "-s", |
163 | | - addon_script_path, # Load our addon to capture the port |
164 | | - ], |
165 | | - stdout=subprocess.PIPE, |
166 | | - stderr=subprocess.PIPE, |
167 | | - text=True, |
168 | | - ) |
169 | | - print(f"[MITM] mitmdump process started with PID {mitm_process.pid}") |
170 | | - |
171 | | - # Wait for mitmproxy to generate CA certificate |
172 | | - ca_cert_path = Path.home() / ".mitmproxy" / "mitmproxy-ca-cert.pem" |
173 | | - print(f"[MITM] Waiting for CA certificate at: {ca_cert_path}") |
174 | | - max_wait_seconds = 30 |
175 | | - start_time = time.time() |
176 | | - |
177 | | - cert_wait_count = 0 |
178 | | - while not ca_cert_path.exists(): |
179 | | - cert_wait_count += 1 |
180 | | - elapsed = time.time() - start_time |
181 | | - |
182 | | - if cert_wait_count % 4 == 0: # Print every 2 seconds |
183 | | - print(f"[MITM] Still waiting for CA cert... ({elapsed:.1f}s elapsed)") |
184 | | - |
185 | | - if elapsed > max_wait_seconds: |
186 | | - print( |
187 | | - f"[MITM] Timeout waiting for CA certificate after {max_wait_seconds}s" |
188 | | - ) |
189 | | - mitm_process.kill() |
190 | | - pytest.fail( |
191 | | - f"mitmproxy CA certificate not generated after {max_wait_seconds}s" |
192 | | - ) |
193 | | - |
194 | | - if mitm_process.poll() is not None: |
195 | | - # Process died |
196 | | - print("[MITM] ERROR: mitmdump process died during CA cert generation") |
197 | | - stdout, stderr = mitm_process.communicate() |
198 | | - pytest.fail( |
199 | | - f"mitmproxy process died during startup.\nStdout: {stdout}\nStderr: {stderr}" |
200 | | - ) |
201 | | - |
202 | | - time.sleep(0.5) |
203 | | - |
204 | | - print(f"[MITM] CA certificate found at {ca_cert_path}") |
205 | | - |
206 | | - # Wait for the addon to write the port to the file |
207 | | - print("[MITM] Waiting for addon to write port to file...") |
208 | | - proxy_port = None |
209 | | - max_port_wait = 10 |
210 | | - port_start_time = time.time() |
211 | | - |
212 | | - while proxy_port is None: |
213 | | - elapsed = time.time() - port_start_time |
214 | | - |
215 | | - if elapsed > max_port_wait: |
216 | | - print(f"[MITM] Timeout waiting for port file after {max_port_wait}s") |
217 | | - print(f"[MITM] Port file path: {port_file_path}") |
218 | | - print(f"[MITM] Port file exists: {Path(port_file_path).exists()}") |
219 | | - mitm_process.kill() |
220 | | - # Cleanup temp files |
221 | | - try: |
222 | | - os.unlink(port_file_path) |
223 | | - os.unlink(addon_script_path) |
224 | | - except OSError: |
225 | | - pass |
226 | | - pytest.fail("Could not determine mitmproxy port from addon") |
227 | | - |
228 | | - if mitm_process.poll() is not None: |
229 | | - print("[MITM] ERROR: mitmdump process died during port detection") |
230 | | - stdout, stderr = mitm_process.communicate() |
231 | | - # Cleanup temp files |
232 | | - try: |
233 | | - os.unlink(port_file_path) |
234 | | - os.unlink(addon_script_path) |
235 | | - except OSError: |
236 | | - pass |
237 | | - pytest.fail( |
238 | | - f"mitmproxy died before port detection.\nStdout: {stdout}\nStderr: {stderr}" |
239 | | - ) |
240 | | - |
241 | | - # Check if port file has been written |
242 | | - if Path(port_file_path).exists(): |
243 | | - try: |
244 | | - with open(port_file_path) as f: |
245 | | - port_str = f.read().strip() |
246 | | - if port_str: |
247 | | - proxy_port = int(port_str) |
248 | | - print(f"[MITM] Port detected from file: {proxy_port}") |
249 | | - break |
250 | | - except (ValueError, OSError) as e: |
251 | | - print(f"[MITM] Error reading port file: {e}") |
252 | | - |
253 | | - time.sleep(0.1) |
254 | | - |
255 | | - print(f"[MITM] Successfully detected port {proxy_port}") |
256 | | - |
257 | | - # Cleanup temp files |
258 | | - try: |
259 | | - os.unlink(port_file_path) |
260 | | - os.unlink(addon_script_path) |
261 | | - print("[MITM] Cleaned up temporary addon files") |
262 | | - except Exception as e: |
263 | | - print(f"[MITM] Warning: Could not cleanup temp files: {e}") |
264 | | - |
265 | | - proxy_url = f"http://{proxy_host}:{proxy_port}" |
266 | | - print(f"[MITM] Proxy URL: {proxy_url}") |
267 | | - |
268 | | - # Verify proxy is listening |
269 | | - print( |
270 | | - f"[MITM] Verifying proxy is accepting connections on {proxy_host}:{proxy_port}..." |
271 | | - ) |
272 | | - try: |
273 | | - with socket.create_connection((proxy_host, proxy_port), timeout=5): |
274 | | - pass |
275 | | - print("[MITM] Proxy is accepting connections!") |
276 | | - except (socket.timeout, ConnectionRefusedError) as e: |
277 | | - print(f"[MITM] ERROR: Proxy not accepting connections: {e}") |
278 | | - mitm_process.kill() |
279 | | - pytest.fail(f"mitmproxy not accepting connections: {e}") |
280 | | - |
281 | | - proxy_info = MitmProxyInfo( |
282 | | - host=proxy_host, |
283 | | - port=proxy_port, |
284 | | - ca_cert_path=ca_cert_path, |
285 | | - proxy_url=proxy_url, |
286 | | - ) |
287 | | - |
288 | | - print(f"[MITM] Setup complete! Proxy ready at {proxy_url}") |
289 | | - print(f"[MITM] CA cert: {ca_cert_path}") |
290 | | - |
291 | | - try: |
292 | | - yield proxy_info |
293 | | - finally: |
294 | | - # Cleanup: stop mitmproxy |
295 | | - print("[MITM] Cleaning up: stopping mitmproxy...") |
296 | | - mitm_process.terminate() |
297 | | - try: |
298 | | - mitm_process.wait(timeout=5) |
299 | | - print("[MITM] mitmproxy stopped gracefully") |
300 | | - except subprocess.TimeoutExpired: |
301 | | - print("[MITM] mitmproxy didn't stop gracefully, killing...") |
302 | | - mitm_process.kill() |
303 | | - mitm_process.wait() |
304 | | - print("[MITM] mitmproxy killed") |
0 commit comments