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.
Files changed (24) hide show
  1. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/PKG-INFO +4 -3
  2. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/README.md +3 -2
  3. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy/__init__.py +1 -1
  4. ja4proxy-0.3.0/ja4proxy/__main__.py +5 -0
  5. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy/server.py +31 -8
  6. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy.egg-info/PKG-INFO +4 -3
  7. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy.egg-info/SOURCES.txt +1 -0
  8. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/pyproject.toml +1 -1
  9. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/tests/test_live_proxy.py +33 -1
  10. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/LICENSE +0 -0
  11. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy/certs.py +0 -0
  12. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy/cli.py +0 -0
  13. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy/engine/bridge.py +0 -0
  14. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy/engine/loader.py +0 -0
  15. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy/parser.py +0 -0
  16. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy/profiles.py +0 -0
  17. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy.egg-info/dependency_links.txt +0 -0
  18. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy.egg-info/entry_points.txt +0 -0
  19. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy.egg-info/requires.txt +0 -0
  20. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/ja4proxy.egg-info/top_level.txt +0 -0
  21. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/setup.cfg +0 -0
  22. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/tests/test_certs.py +0 -0
  23. {ja4proxy-0.2.0 → ja4proxy-0.3.0}/tests/test_engine.py +0 -0
  24. {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.2.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 18 unit and live tests execute with zero warnings and zero regressions:
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 18 tests in 6.164s
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 18 unit and live tests execute with zero warnings and zero regressions:
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 18 tests in 6.164s
670
+ Ran 19 tests in 6.421s
670
671
 
671
672
  OK
672
673
  ```
@@ -1,6 +1,6 @@
1
1
  """ja4proxy: High-performance universal proxy server with browser TLS (JA4) and HTTP/2 impersonation."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
4
4
  __author__ = "cipherwire"
5
5
 
6
6
  from ja4proxy.engine.bridge import NativeEngine
@@ -0,0 +1,5 @@
1
+ """Entry point for python -m ja4proxy."""
2
+ from ja4proxy.cli import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -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.get_ssl_context_for_host(target_host)
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
- new_transport = await loop.start_tls(
425
- transport, protocol, ssl_ctx, server_side=True
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.get_ssl_context_for_host(clean_host)
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
- new_transport = await loop.start_tls(
493
- transport, protocol, ssl_ctx, server_side=True
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.2.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 18 unit and live tests execute with zero warnings and zero regressions:
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 18 tests in 6.164s
683
+ Ran 19 tests in 6.421s
683
684
 
684
685
  OK
685
686
  ```
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  ja4proxy/__init__.py
5
+ ja4proxy/__main__.py
5
6
  ja4proxy/certs.py
6
7
  ja4proxy/cli.py
7
8
  ja4proxy/parser.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ja4proxy"
7
- version = "0.2.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