Skip to content

Commit d14f7a5

Browse files
committed
feat: Perfect Chrome 143 fingerprint matching with documentation sync
- Add Chrome-like HTTP/2 priority (weight=256, exclusive=1) in http2_logic.c - Remove zlib from certificate compression, use Brotli only (per Chrome behavior) - Add Chrome default headers to Session (sec-ch-ua, sec-fetch-*, priority) - Update all documentation to reflect Chrome 143 as default profile - Sync README and ReadTheDocs with actual code behavior: - Client and Session both default to http2=True - Certificate compression is Brotli only - Updated fingerprint characteristics (JA4, JA3N, Peetprint, Akamai) - Fix test names to match http2=True default behavior
1 parent ab6ffad commit d14f7a5

9 files changed

Lines changed: 119 additions & 70 deletions

File tree

README.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ httpmorph accurately mimics **Chrome 127-143** TLS and HTTP/2 fingerprints with:
124124
- **TLS 1.3** with correct cipher suites and extensions
125125
- **HTTP/2** with Chrome-specific SETTINGS frame and pseudo-header order
126126
- **Post-quantum cryptography** (X25519MLKEM768)
127-
- **Certificate compression** (Brotli, Zlib)
127+
- **Certificate compression** (Brotli)
128128

129129
**Verify your fingerprint:**
130130

@@ -148,19 +148,18 @@ All Chrome 127-143 profiles produce **exact JA4 matches** with real Chrome brows
148148
httpmorph supports HTTP/2 with an httpx-like API:
149149

150150
```python
151-
# Enable HTTP/2 for a client (default is False)
152-
client = httpmorph.Client(http2=True)
151+
# Both Client and Session default to HTTP/2 (http2=True) like Chrome
152+
client = httpmorph.Client()
153153
response = client.get('https://www.google.com')
154154
print(response.http_version) # '2.0'
155155

156-
# Enable HTTP/2 for a session
157-
session = httpmorph.Session(browser='chrome', http2=True)
156+
session = httpmorph.Session(browser='chrome')
158157
response = session.get('https://www.google.com')
159158
print(response.http_version) # '2.0'
160159

161-
# Per-request HTTP/2 override
162-
client = httpmorph.Client(http2=False) # Default disabled
163-
response = client.get('https://www.google.com', http2=True) # Enable for this request
160+
# Per-request HTTP/2 override (disable for specific request)
161+
client = httpmorph.Client() # Defaults to HTTP/2
162+
response = client.get('https://example.com', http2=False) # Disable for this request
164163
```
165164

166165
### Custom Headers
@@ -440,7 +439,7 @@ pytest tests/ -v
440439
- HTTP/2 support via nghttp2
441440
- Inspired by Python's requests and httpx libraries
442441
- Chrome 127-143 fingerprint matching with perfect JA4, JA3N, and HTTP/2 Akamai fingerprints
443-
- Certificate compression support for Cloudflare-protected sites
442+
- Certificate compression (Brotli) for Cloudflare-protected sites
444443

445444
## FAQ
446445

@@ -457,7 +456,7 @@ A: No, httpmorph is still in active development and not yet recommended for prod
457456
A: For most common use cases, yes! We've implemented the most widely-used requests API. Some advanced features may have slight differences.
458457

459458
**Q: Does it work with Cloudflare-protected sites?**
460-
A: Yes! httpmorph supports certificate compression (Brotli, Zlib) which is required for many Cloudflare-protected sites. We successfully tested with icanhazip.com and postman-echo.com.
459+
A: Yes! httpmorph supports certificate compression (Brotli) which is required for many Cloudflare-protected sites. We successfully tested with icanhazip.com and postman-echo.com.
461460

462461
**Q: How do I report a bug?**
463462
A: Please open an issue on GitHub with a minimal reproduction example and your environment details (OS, Python version, httpmorph version).

docs/source/advanced.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ TLS Fingerprinting
99
Browser-Specific Fingerprints
1010
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1111

12-
httpmorph generates accurate Chrome 142 TLS fingerprints with perfect JA3N, JA4, and JA4_R matching:
12+
httpmorph generates accurate Chrome 143 TLS fingerprints with perfect JA3N, JA4, JA4_R, and Akamai matching:
1313

1414
.. code-block:: python
1515
16-
# Chrome 142 profile (default)
16+
# Chrome 143 profile (default)
1717
session = httpmorph.Session(browser='chrome')
1818
response = session.get('https://example.com')
1919
@@ -23,7 +23,7 @@ httpmorph generates accurate Chrome 142 TLS fingerprints with perfect JA3N, JA4,
2323
print('HTTP:', response.http_version)
2424
2525
# Output example:
26-
# JA3: 8e19337e7524d2573be54efb2b0784c9 (Chrome 142 normalized)
26+
# JA3: dcefaf3f0e71d260d19dc1d0749c9278 (Chrome 143 normalized)
2727
# TLS: TLSv1.3
2828
# Cipher: TLS_AES_128_GCM_SHA256
2929
# HTTP: 2.0
@@ -53,7 +53,7 @@ Customize the User-Agent for different operating systems:
5353
GREASE Values
5454
~~~~~~~~~~~~~
5555

56-
Chrome 142 uses GREASE (Generate Random Extensions And Sustain Extensibility) values that are randomized per request to maintain TLS ecosystem extensibility:
56+
Chrome 143 uses GREASE (Generate Random Extensions And Sustain Extensibility) values that are randomized per request to maintain TLS ecosystem extensibility:
5757

5858
.. code-block:: python
5959

docs/source/api.rst

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,13 @@ Client Class
9090

9191
.. code-block:: python
9292
93-
client = httpmorph.Client(http2=False)
93+
client = httpmorph.Client(http2=True)
9494
95-
HTTP client for making requests.
95+
HTTP client for making requests. Defaults to HTTP/2 to match Chrome behavior.
9696

9797
**Constructor Parameters:**
9898

99-
* ``http2`` (bool) - Enable HTTP/2. Default: ``False``
99+
* ``http2`` (bool) - Enable HTTP/2. Default: ``True``
100100

101101
**Methods:**
102102

@@ -127,15 +127,15 @@ Session Class
127127

128128
.. code-block:: python
129129
130-
session = httpmorph.Session(browser='chrome', os='macos', http2=False)
130+
session = httpmorph.Session(browser='chrome', os='macos', http2=True)
131131
132-
HTTP session with persistent cookies and headers.
132+
HTTP session with persistent cookies and headers. Sessions default to HTTP/2 to match Chrome browser behavior.
133133

134134
**Constructor Parameters:**
135135

136-
* ``browser`` (str) - Browser profile to mimic. Options: ``'chrome'``, ``'chrome142'``, ``'random'``. Default: ``'chrome'``
136+
* ``browser`` (str) - Browser profile to mimic. Options: ``'chrome'``, ``'chrome127'``-``'chrome143'``, ``'random'``. Default: ``'chrome'`` (Chrome 143)
137137
* ``os`` (str) - Operating system for User-Agent. Options: ``'macos'``, ``'windows'``, ``'linux'``. Default: ``'macos'``
138-
* ``http2`` (bool) - Enable HTTP/2. Default: ``False``
138+
* ``http2`` (bool) - Enable HTTP/2. Default: ``True`` (matches Chrome behavior)
139139

140140
**Attributes:**
141141

@@ -423,42 +423,55 @@ Browser Profiles
423423

424424
Available browser profiles for ``Session(browser=...)``:
425425

426-
Chrome 142
427-
~~~~~~~~~~
426+
Chrome 143 (Default)
427+
~~~~~~~~~~~~~~~~~~~~
428428

429-
The default and most accurate browser profile, mimicking Chrome 142:
429+
The default and most accurate browser profile, mimicking Chrome 143:
430430

431431
**Fingerprint Characteristics:**
432432

433-
* **JA3N**: ``8e19337e7524d2573be54efb2b0784c9`` (perfect match)
434-
* **JA4**: ``t13d1516h2_8daaf6152771_d8a2da3f94cd`` (perfect match)
435-
* **JA4_R**: ``t13d1516h2_002f,0035,009c,...`` (perfect match)
433+
* **JA4**: ``t13d1516h2_8daaf6152771_e5627efa2ab1`` (perfect match)
434+
* **JA3N**: ``dcefaf3f0e71d260d19dc1d0749c9278`` (perfect match)
435+
* **Peetprint**: ``1d4ffe9b0e34acac0bd883fa7f79d7b5`` (perfect match)
436+
* **Akamai HTTP/2**: ``1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p`` (perfect match)
436437
* **TLS 1.3** with 15 cipher suites
437438
* **Post-quantum cryptography**: X25519MLKEM768 (curve 4588)
438-
* **Certificate compression**: Brotli, Zlib
439+
* **Certificate compression**: Brotli only
439440
* **GREASE**: Randomized per request
440-
* **HTTP/2**: Chrome-specific SETTINGS frame
441+
* **HTTP/2**: Chrome-specific SETTINGS frame, priority (weight=256, exclusive=1)
442+
* **Default headers**: sec-ch-ua, sec-fetch-*, accept-language, priority
441443
442444
**User-Agent Variants:**
443445

444-
* **macOS**: ``Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36``
445-
* **Windows**: ``Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36``
446-
* **Linux**: ``Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36``
446+
* **macOS**: ``Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36``
447+
* **Windows**: ``Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36``
448+
* **Linux**: ``Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36``
447449

448450
**Usage:**
449451

450452
.. code-block:: python
451453
452-
# Use Chrome 142 profile (default)
454+
# Use Chrome 143 profile (default)
453455
session = httpmorph.Session(browser='chrome')
454456
455-
# Explicitly use Chrome 142
456-
session = httpmorph.Session(browser='chrome142')
457+
# Explicitly use Chrome 143
458+
session = httpmorph.Session(browser='chrome143')
457459
458460
# With specific OS
459461
session = httpmorph.Session(browser='chrome', os='windows')
460462
463+
Chrome 127-142
464+
~~~~~~~~~~~~~~
465+
466+
Older Chrome profiles are also available for compatibility testing:
467+
468+
.. code-block:: python
469+
470+
session = httpmorph.Session(browser='chrome127')
471+
session = httpmorph.Session(browser='chrome135')
472+
# etc.
473+
461474
Random
462475
~~~~~~
463476

464-
Randomly selects a browser profile for each session. Currently only Chrome 142 is available.
477+
Randomly selects a browser profile for each session from available Chrome profiles.

docs/source/quickstart.rst

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,12 @@ Mimic Chrome browser with realistic fingerprints:
102102

103103
.. code-block:: python
104104
105-
# Chrome browser profile (defaults to Chrome 142)
105+
# Chrome browser profile (defaults to Chrome 143)
106106
session = httpmorph.Session(browser='chrome')
107107
response = session.get('https://example.com')
108108
109-
# Use specific Chrome version
110-
session = httpmorph.Session(browser='chrome142')
109+
# Use specific Chrome version (127-143 supported)
110+
session = httpmorph.Session(browser='chrome143')
111111
response = session.get('https://example.com')
112112
113113
# Random browser selection
@@ -118,9 +118,10 @@ The Chrome browser profile includes:
118118
* Chrome-specific User-Agent
119119
* Chrome-specific TLS cipher suites and extensions
120120
* Post-quantum cryptography (X25519MLKEM768)
121-
* Certificate compression (Brotli, Zlib)
122-
* Chrome-specific HTTP/2 settings
123-
* Perfect JA3N, JA4, and JA4_R fingerprint matching
121+
* Certificate compression (Brotli)
122+
* Chrome-specific HTTP/2 settings and priority
123+
* Perfect JA3N, JA4, JA4_R, and Akamai fingerprint matching
124+
* Chrome-like default headers (sec-ch-ua, sec-fetch-*, etc.)
124125
125126
OS-Specific User Agents
126127
~~~~~~~~~~~~~~~~~~~~~~~~
@@ -344,21 +345,22 @@ Upload files:
344345
HTTP/2
345346
------
346347

347-
Enable HTTP/2 support:
348+
Both Client and Session default to HTTP/2 to match Chrome behavior:
348349

349350
.. code-block:: python
350351
351-
# For all requests in a client
352-
client = httpmorph.Client(http2=True)
352+
# Both Client and Session default to HTTP/2 (http2=True)
353+
client = httpmorph.Client()
353354
response = client.get('https://www.google.com')
355+
print(response.http_version) # '2.0'
354356
355-
# For all requests in a session
356-
session = httpmorph.Session(browser='chrome', http2=True)
357+
session = httpmorph.Session(browser='chrome')
357358
response = session.get('https://www.google.com')
359+
print(response.http_version) # '2.0'
358360
359-
# Per-request override
360-
client = httpmorph.Client(http2=False)
361-
response = client.get('https://www.google.com', http2=True)
361+
# Per-request override (disable HTTP/2 for specific request)
362+
client = httpmorph.Client() # Defaults to HTTP/2
363+
response = client.get('https://example.com', http2=False)
362364
363365
Check HTTP version:
364366

src/core/http2_logic.c

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -348,17 +348,22 @@ int httpmorph_http2_request(SSL *ssl, const httpmorph_request_t *request,
348348
data_prd_ptr = &data_prd;
349349
}
350350

351-
/* Set up priority spec if priority is configured */
351+
/* Set up priority spec - Chrome uses weight=256, exclusive=1, depends_on=0 by default */
352352
nghttp2_priority_spec pri_spec;
353353
nghttp2_priority_spec *pri_spec_ptr = NULL;
354354

355-
if (request->http2_stream_dependency != 0 || request->http2_priority_weight != 16) {
356-
/* Priority is configured - use it */
355+
if (request->http2_stream_dependency != 0 || request->http2_priority_weight != 16 ||
356+
request->http2_priority_exclusive) {
357+
/* Priority is explicitly configured - use it */
357358
nghttp2_priority_spec_init(&pri_spec,
358359
request->http2_stream_dependency,
359360
request->http2_priority_weight,
360361
request->http2_priority_exclusive ? 1 : 0);
361362
pri_spec_ptr = &pri_spec;
363+
} else {
364+
/* Use Chrome default priority: weight=256, exclusive=1, depends_on=0 */
365+
nghttp2_priority_spec_init(&pri_spec, 0, 256, 1);
366+
pri_spec_ptr = &pri_spec;
362367
}
363368

364369
/* Submit request with priority spec and data provider */
@@ -563,17 +568,22 @@ int httpmorph_http2_request_pooled(struct pooled_connection *conn,
563568
data_prd_ptr = &data_prd;
564569
}
565570

566-
/* Set up priority spec if priority is configured */
571+
/* Set up priority spec - Chrome uses weight=256, exclusive=1, depends_on=0 by default */
567572
nghttp2_priority_spec pri_spec;
568573
nghttp2_priority_spec *pri_spec_ptr = NULL;
569574

570-
if (request->http2_stream_dependency != 0 || request->http2_priority_weight != 16) {
571-
/* Priority is configured - use it */
575+
if (request->http2_stream_dependency != 0 || request->http2_priority_weight != 16 ||
576+
request->http2_priority_exclusive) {
577+
/* Priority is explicitly configured - use it */
572578
nghttp2_priority_spec_init(&pri_spec,
573579
request->http2_stream_dependency,
574580
request->http2_priority_weight,
575581
request->http2_priority_exclusive ? 1 : 0);
576582
pri_spec_ptr = &pri_spec;
583+
} else {
584+
/* Use Chrome default priority: weight=256, exclusive=1, depends_on=0 */
585+
nghttp2_priority_spec_init(&pri_spec, 0, 256, 1);
586+
pri_spec_ptr = &pri_spec;
577587
}
578588

579589
/* Submit request with stream-specific user data
@@ -764,17 +774,22 @@ int httpmorph_http2_request_concurrent(struct pooled_connection *conn,
764774
data_prd_ptr = &data_prd;
765775
}
766776

767-
/* Set up priority spec if priority is configured */
777+
/* Set up priority spec - Chrome uses weight=256, exclusive=1, depends_on=0 by default */
768778
nghttp2_priority_spec pri_spec;
769779
nghttp2_priority_spec *pri_spec_ptr = NULL;
770780

771-
if (request->http2_stream_dependency != 0 || request->http2_priority_weight != 16) {
772-
/* Priority is configured - use it */
781+
if (request->http2_stream_dependency != 0 || request->http2_priority_weight != 16 ||
782+
request->http2_priority_exclusive) {
783+
/* Priority is explicitly configured - use it */
773784
nghttp2_priority_spec_init(&pri_spec,
774785
request->http2_stream_dependency,
775786
request->http2_priority_weight,
776787
request->http2_priority_exclusive ? 1 : 0);
777788
pri_spec_ptr = &pri_spec;
789+
} else {
790+
/* Use Chrome default priority: weight=256, exclusive=1, depends_on=0 */
791+
nghttp2_priority_spec_init(&pri_spec, 0, 256, 1);
792+
pri_spec_ptr = &pri_spec;
778793
}
779794

780795
/* Submit stream to session manager (non-blocking) */

src/core/tls.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,11 @@ int httpmorph_configure_ssl_ctx(SSL_CTX *ctx, const browser_profile_t *profile)
144144
}
145145

146146
/* Enable compress_certificate extension (0x001b) only if profile includes it.
147-
* Chrome advertises brotli (2) and zlib (1) decompression support.
148-
* We provide actual decompression functions for servers that send compressed certs.
147+
* Chrome 143+ only advertises brotli (2) decompression support, NOT zlib.
148+
* We provide actual decompression function for servers that send compressed certs.
149149
* The compress function is NULL since clients don't compress certificates. */
150150
if (has_compress_cert) {
151151
SSL_CTX_add_cert_compression_alg(ctx, TLSEXT_cert_compression_brotli, NULL, cert_decompress_brotli);
152-
SSL_CTX_add_cert_compression_alg(ctx, TLSEXT_cert_compression_zlib, NULL, cert_decompress_zlib);
153152
}
154153

155154
/* Force AES hardware preference to match Chrome's cipher order (AES-GCM before ChaCha20)

src/httpmorph/_client_c.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -654,14 +654,35 @@ def parse_set_cookie(self, set_cookie_header):
654654
class Session:
655655
"""HTTP session with persistent fingerprint"""
656656

657+
# Chrome-like default headers for fingerprint matching
658+
_CHROME_DEFAULT_HEADERS = {
659+
# Client Hints (Chrome 143)
660+
"sec-ch-ua": '"Chromium";v="143", "Google Chrome";v="143", "Not-A.Brand";v="24"',
661+
"sec-ch-ua-mobile": "?0",
662+
"sec-ch-ua-platform": '"macOS"',
663+
# Fetch Metadata
664+
"sec-fetch-dest": "document",
665+
"sec-fetch-mode": "navigate",
666+
"sec-fetch-site": "none",
667+
"sec-fetch-user": "?1",
668+
# Standard headers
669+
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
670+
"accept-language": "en-US,en;q=0.9",
671+
"cache-control": "max-age=0",
672+
"upgrade-insecure-requests": "1",
673+
# HTTP/2 Priority header (Chrome 143 style)
674+
"priority": "u=0, i",
675+
}
676+
657677
def __init__(self, browser="chrome", http2=True, os="macos"):
658678
if not HAS_C_EXTENSION:
659679
raise RuntimeError("C extension not available")
660680
self._session = _httpmorph.Session(browser=browser, os=os)
661681
self.browser = browser
662682
self.os = os
663683
self.http2 = http2 # HTTP/2 enabled flag
664-
self.headers = {} # Persistent headers
684+
# Initialize with Chrome-like default headers
685+
self.headers = self._CHROME_DEFAULT_HEADERS.copy()
665686
self._cookies = CookieDict(self._session.cookie_jar)
666687

667688
def __del__(self):

0 commit comments

Comments
 (0)