ja4proxy 0.2.0__tar.gz → 0.3.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.
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/PKG-INFO +4 -3
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/README.md +3 -2
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy/__init__.py +1 -1
- ja4proxy-0.3.0/ja4proxy/__main__.py +5 -0
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy/server.py +31 -8
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy.egg-info/PKG-INFO +4 -3
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy.egg-info/SOURCES.txt +1 -0
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/pyproject.toml +1 -1
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/tests/test_live_proxy.py +33 -1
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/LICENSE +0 -0
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy/certs.py +0 -0
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy/cli.py +0 -0
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy/engine/bridge.py +0 -0
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy/engine/loader.py +0 -0
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy/parser.py +0 -0
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy/profiles.py +0 -0
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy.egg-info/dependency_links.txt +0 -0
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy.egg-info/entry_points.txt +0 -0
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy.egg-info/requires.txt +0 -0
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy.egg-info/top_level.txt +0 -0
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/setup.cfg +0 -0
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/tests/test_certs.py +0 -0
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/tests/test_engine.py +0 -0
- {ja4proxy-0.2.0 → ja4proxy-0.3.0}/tests/test_parser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ja4proxy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: High-performance universal proxy server with browser TLS (JA4) and HTTP/2 impersonation
|
|
5
5
|
Author-email: cipherwire <dev@cipherwire.link>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -657,7 +657,7 @@ Run the full automated test suite covering CA generation, SOCKS5 state machines,
|
|
|
657
657
|
python -m unittest discover -s tests -p "test_*.py" -v
|
|
658
658
|
```
|
|
659
659
|
|
|
660
|
-
All
|
|
660
|
+
All 19 unit and live tests execute with zero warnings and zero regressions:
|
|
661
661
|
```text
|
|
662
662
|
test_ca_creation (test_certs.TestCertificateAuthority) ... ok
|
|
663
663
|
test_leaf_cert_generation (test_certs.TestCertificateAuthority) ... ok
|
|
@@ -667,6 +667,7 @@ test_live_post_httpbin (test_engine.TestNativeEngine) ... ok
|
|
|
667
667
|
test_bad_gateway_mapping_status_502 (test_live_proxy.TestLiveProxy) ... ok
|
|
668
668
|
test_chunked_request_upload (test_live_proxy.TestLiveProxy) ... ok
|
|
669
669
|
test_client_proxy_authentication (test_live_proxy.TestLiveProxy) ... ok
|
|
670
|
+
test_connect_tls_handshake_timeout (test_live_proxy.TestLiveProxy) ... ok
|
|
670
671
|
test_gateway_mode_gzip (test_live_proxy.TestLiveProxy) ... ok
|
|
671
672
|
test_safari_ios_headers (test_live_proxy.TestLiveProxy) ... ok
|
|
672
673
|
test_socks5_ingress_tls_peet (test_live_proxy.TestLiveProxy) ... ok
|
|
@@ -679,7 +680,7 @@ test_parse_credentials_proxy_with_hyphens (test_parser.TestParser) ... ok
|
|
|
679
680
|
test_parse_credentials_tags (test_parser.TestParser) ... ok
|
|
680
681
|
|
|
681
682
|
----------------------------------------------------------------------
|
|
682
|
-
Ran
|
|
683
|
+
Ran 19 tests in 6.421s
|
|
683
684
|
|
|
684
685
|
OK
|
|
685
686
|
```
|
|
@@ -644,7 +644,7 @@ Run the full automated test suite covering CA generation, SOCKS5 state machines,
|
|
|
644
644
|
python -m unittest discover -s tests -p "test_*.py" -v
|
|
645
645
|
```
|
|
646
646
|
|
|
647
|
-
All
|
|
647
|
+
All 19 unit and live tests execute with zero warnings and zero regressions:
|
|
648
648
|
```text
|
|
649
649
|
test_ca_creation (test_certs.TestCertificateAuthority) ... ok
|
|
650
650
|
test_leaf_cert_generation (test_certs.TestCertificateAuthority) ... ok
|
|
@@ -654,6 +654,7 @@ test_live_post_httpbin (test_engine.TestNativeEngine) ... ok
|
|
|
654
654
|
test_bad_gateway_mapping_status_502 (test_live_proxy.TestLiveProxy) ... ok
|
|
655
655
|
test_chunked_request_upload (test_live_proxy.TestLiveProxy) ... ok
|
|
656
656
|
test_client_proxy_authentication (test_live_proxy.TestLiveProxy) ... ok
|
|
657
|
+
test_connect_tls_handshake_timeout (test_live_proxy.TestLiveProxy) ... ok
|
|
657
658
|
test_gateway_mode_gzip (test_live_proxy.TestLiveProxy) ... ok
|
|
658
659
|
test_safari_ios_headers (test_live_proxy.TestLiveProxy) ... ok
|
|
659
660
|
test_socks5_ingress_tls_peet (test_live_proxy.TestLiveProxy) ... ok
|
|
@@ -666,7 +667,7 @@ test_parse_credentials_proxy_with_hyphens (test_parser.TestParser) ... ok
|
|
|
666
667
|
test_parse_credentials_tags (test_parser.TestParser) ... ok
|
|
667
668
|
|
|
668
669
|
----------------------------------------------------------------------
|
|
669
|
-
Ran
|
|
670
|
+
Ran 19 tests in 6.421s
|
|
670
671
|
|
|
671
672
|
OK
|
|
672
673
|
```
|
|
@@ -22,6 +22,7 @@ from ja4proxy.parser import (
|
|
|
22
22
|
logger = logging.getLogger("ja4proxy")
|
|
23
23
|
|
|
24
24
|
DEFAULT_IDLE_TIMEOUT: float = 15.0
|
|
25
|
+
DEFAULT_TLS_HANDSHAKE_TIMEOUT: float = 10.0
|
|
25
26
|
_DNS_CACHE_TTL: float = 120.0
|
|
26
27
|
_DNS_CACHE: dict[str, tuple[bool, float]] = {}
|
|
27
28
|
_DNS_LOCKS: dict[str, asyncio.Lock] = {}
|
|
@@ -161,6 +162,7 @@ class ProxyServer:
|
|
|
161
162
|
upstream_proxy: str | None = None,
|
|
162
163
|
auth: str | None = None,
|
|
163
164
|
idle_timeout: float = DEFAULT_IDLE_TIMEOUT,
|
|
165
|
+
tls_handshake_timeout: float = DEFAULT_TLS_HANDSHAKE_TIMEOUT,
|
|
164
166
|
):
|
|
165
167
|
self.host = host
|
|
166
168
|
self.port = port
|
|
@@ -177,6 +179,7 @@ class ProxyServer:
|
|
|
177
179
|
self.auth_user, self.auth_pass = None, None
|
|
178
180
|
|
|
179
181
|
self.idle_timeout = idle_timeout
|
|
182
|
+
self.tls_handshake_timeout = tls_handshake_timeout
|
|
180
183
|
self.ca = CertificateAuthority(cert_dir)
|
|
181
184
|
self.engine = NativeEngine.get_instance()
|
|
182
185
|
self._server: asyncio.Server | None = None
|
|
@@ -414,16 +417,26 @@ class ProxyServer:
|
|
|
414
417
|
|
|
415
418
|
# Prepare SSL context before acknowledging SOCKS5 to ensure server is ready for TLS ClientHello
|
|
416
419
|
if target_port == 443:
|
|
417
|
-
ssl_ctx = self.ca.
|
|
420
|
+
ssl_ctx = await self.ca.get_ssl_context_for_host_async(target_host)
|
|
418
421
|
writer.write(b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00")
|
|
419
422
|
await writer.drain()
|
|
420
423
|
|
|
421
424
|
loop = asyncio.get_running_loop()
|
|
422
425
|
transport = writer.transport
|
|
423
426
|
protocol = transport.get_protocol()
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
+
try:
|
|
428
|
+
new_transport = await asyncio.wait_for(
|
|
429
|
+
loop.start_tls(transport, protocol, ssl_ctx, server_side=True),
|
|
430
|
+
timeout=self.tls_handshake_timeout,
|
|
431
|
+
)
|
|
432
|
+
except (asyncio.TimeoutError, Exception) as exc:
|
|
433
|
+
logger.debug(f"SOCKS5 TLS handshake failed or timed out for {target_host}: {exc}")
|
|
434
|
+
writer.close()
|
|
435
|
+
try:
|
|
436
|
+
await writer.wait_closed()
|
|
437
|
+
except Exception:
|
|
438
|
+
pass
|
|
439
|
+
return
|
|
427
440
|
writer._transport = new_transport
|
|
428
441
|
|
|
429
442
|
await self._run_tls_http_loop(reader, writer, target_authority, directive)
|
|
@@ -482,16 +495,26 @@ class ProxyServer:
|
|
|
482
495
|
directive, _ = extract_directives(headers)
|
|
483
496
|
|
|
484
497
|
if target_port == 443:
|
|
485
|
-
ssl_ctx = self.ca.
|
|
498
|
+
ssl_ctx = await self.ca.get_ssl_context_for_host_async(clean_host)
|
|
486
499
|
writer.write(b"HTTP/1.1 200 Connection Established\r\n\r\n")
|
|
487
500
|
await writer.drain()
|
|
488
501
|
|
|
489
502
|
loop = asyncio.get_running_loop()
|
|
490
503
|
transport = writer.transport
|
|
491
504
|
protocol = transport.get_protocol()
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
505
|
+
try:
|
|
506
|
+
new_transport = await asyncio.wait_for(
|
|
507
|
+
loop.start_tls(transport, protocol, ssl_ctx, server_side=True),
|
|
508
|
+
timeout=self.tls_handshake_timeout,
|
|
509
|
+
)
|
|
510
|
+
except (asyncio.TimeoutError, Exception) as exc:
|
|
511
|
+
logger.debug(f"CONNECT TLS handshake failed or timed out for {clean_host}: {exc}")
|
|
512
|
+
writer.close()
|
|
513
|
+
try:
|
|
514
|
+
await writer.wait_closed()
|
|
515
|
+
except Exception:
|
|
516
|
+
pass
|
|
517
|
+
return
|
|
495
518
|
writer._transport = new_transport
|
|
496
519
|
|
|
497
520
|
await self._run_tls_http_loop(reader, writer, target_authority, directive)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ja4proxy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: High-performance universal proxy server with browser TLS (JA4) and HTTP/2 impersonation
|
|
5
5
|
Author-email: cipherwire <dev@cipherwire.link>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -657,7 +657,7 @@ Run the full automated test suite covering CA generation, SOCKS5 state machines,
|
|
|
657
657
|
python -m unittest discover -s tests -p "test_*.py" -v
|
|
658
658
|
```
|
|
659
659
|
|
|
660
|
-
All
|
|
660
|
+
All 19 unit and live tests execute with zero warnings and zero regressions:
|
|
661
661
|
```text
|
|
662
662
|
test_ca_creation (test_certs.TestCertificateAuthority) ... ok
|
|
663
663
|
test_leaf_cert_generation (test_certs.TestCertificateAuthority) ... ok
|
|
@@ -667,6 +667,7 @@ test_live_post_httpbin (test_engine.TestNativeEngine) ... ok
|
|
|
667
667
|
test_bad_gateway_mapping_status_502 (test_live_proxy.TestLiveProxy) ... ok
|
|
668
668
|
test_chunked_request_upload (test_live_proxy.TestLiveProxy) ... ok
|
|
669
669
|
test_client_proxy_authentication (test_live_proxy.TestLiveProxy) ... ok
|
|
670
|
+
test_connect_tls_handshake_timeout (test_live_proxy.TestLiveProxy) ... ok
|
|
670
671
|
test_gateway_mode_gzip (test_live_proxy.TestLiveProxy) ... ok
|
|
671
672
|
test_safari_ios_headers (test_live_proxy.TestLiveProxy) ... ok
|
|
672
673
|
test_socks5_ingress_tls_peet (test_live_proxy.TestLiveProxy) ... ok
|
|
@@ -679,7 +680,7 @@ test_parse_credentials_proxy_with_hyphens (test_parser.TestParser) ... ok
|
|
|
679
680
|
test_parse_credentials_tags (test_parser.TestParser) ... ok
|
|
680
681
|
|
|
681
682
|
----------------------------------------------------------------------
|
|
682
|
-
Ran
|
|
683
|
+
Ran 19 tests in 6.421s
|
|
683
684
|
|
|
684
685
|
OK
|
|
685
686
|
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ja4proxy"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "High-performance universal proxy server with browser TLS (JA4) and HTTP/2 impersonation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "cipherwire", email = "dev@cipherwire.link" }]
|
|
@@ -271,7 +271,6 @@ class TestLiveProxy(unittest.IsolatedAsyncioTestCase):
|
|
|
271
271
|
# 2. Authorized request with valid credentials -> 200 / processed
|
|
272
272
|
reader, writer = await asyncio.open_connection("127.0.0.1", auth_server.port)
|
|
273
273
|
auth_token = "proxyuser:secretpass".encode("utf-8")
|
|
274
|
-
b64_auth = struct.__dict__.get("b64", None)
|
|
275
274
|
import base64
|
|
276
275
|
auth_header = base64.b64encode(auth_token).decode("utf-8")
|
|
277
276
|
|
|
@@ -291,6 +290,39 @@ class TestLiveProxy(unittest.IsolatedAsyncioTestCase):
|
|
|
291
290
|
finally:
|
|
292
291
|
await auth_server.close()
|
|
293
292
|
|
|
293
|
+
async def test_connect_tls_handshake_timeout(self):
|
|
294
|
+
"""Tests that a client stalling during TLS handshake is cleanly terminated by timeout."""
|
|
295
|
+
server = ProxyServer(
|
|
296
|
+
host="127.0.0.1",
|
|
297
|
+
port=0,
|
|
298
|
+
tls_handshake_timeout=0.5,
|
|
299
|
+
cert_dir=self.temp_dir,
|
|
300
|
+
allow_private_networks=True,
|
|
301
|
+
)
|
|
302
|
+
await server.start()
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
reader, writer = await asyncio.open_connection("127.0.0.1", server.port)
|
|
306
|
+
connect_req = (
|
|
307
|
+
f"CONNECT tls.peet.ws:443 HTTP/1.1\r\n"
|
|
308
|
+
f"Host: tls.peet.ws:443\r\n\r\n"
|
|
309
|
+
)
|
|
310
|
+
writer.write(connect_req.encode("utf-8"))
|
|
311
|
+
await writer.drain()
|
|
312
|
+
|
|
313
|
+
status_line = await reader.readline()
|
|
314
|
+
self.assertIn(b"200 Connection Established", status_line)
|
|
315
|
+
# Drain empty line after headers
|
|
316
|
+
await reader.readline()
|
|
317
|
+
|
|
318
|
+
# Client stalls without sending ClientHello; proxy should close on handshake timeout
|
|
319
|
+
data = await asyncio.wait_for(reader.read(100), timeout=2.0)
|
|
320
|
+
self.assertEqual(data, b"")
|
|
321
|
+
writer.close()
|
|
322
|
+
await writer.wait_closed()
|
|
323
|
+
finally:
|
|
324
|
+
await server.close()
|
|
325
|
+
|
|
294
326
|
|
|
295
327
|
if __name__ == "__main__":
|
|
296
328
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|