Skip to content

Commit 20f6e0f

Browse files
committed
Merge remote-tracking branch 'origin/main' into version/2.0.6
2 parents fb3e2c5 + 28f3d8b commit 20f6e0f

File tree

3 files changed

+229
-1
lines changed

3 files changed

+229
-1
lines changed

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,11 @@ const CycleTLS = require('cycletls').default;
111111
```
112112

113113
## JA4R (Raw) TLS Fingerprinting
114+
## JA4R (Raw) TLS Fingerprinting
115+
116+
> **Important:** Pass `ja4r` to configure the TLS ClientHello. JA4 (hash) is a report-only value; configuring with a JA4 hash will not change your fingerprint.
114117
118+
JA4R is the raw format of JA4 fingerprinting that allows explicit configuration of cipher suites, extensions, and signature algorithms:
115119
> **Important:** Pass `ja4r` to configure the TLS ClientHello. JA4 (hash) is a report-only value; configuring with a JA4 hash will not change your fingerprint.
116120
117121
JA4R is the raw format of JA4 fingerprinting that allows explicit configuration of cipher suites, extensions, and signature algorithms:
@@ -140,6 +144,7 @@ const CycleTLS = require('cycletls').default;
140144
})();
141145
```
142146

147+
### Golang JA4R Example
143148
### Golang JA4R Example
144149
```go
145150
package main
@@ -150,11 +155,14 @@ import (
150155
)
151156

152157
func main() {
158+
client := cycletls.Init(cycletls.WithRawBytes())
153159
client := cycletls.Init(cycletls.WithRawBytes())
154160
defer client.Close()
155161

162+
// Chrome JA4R fingerprint (raw format)
156163
// Chrome JA4R fingerprint (raw format)
157164
response, err := client.Do("https://tls.peet.ws/api/all", cycletls.Options{
165+
Ja4r: "t13d1516h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0000,0005,000a,000b,000d,0012,0017,001b,0023,002b,002d,0033,44cd,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601",
158166
Ja4r: "t13d1516h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0000,0005,000a,000b,000d,0012,0017,001b,0023,002b,002d,0033,44cd,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601",
159167
UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
160168
}, "GET")
@@ -163,6 +171,7 @@ func main() {
163171
log.Fatal(err)
164172
}
165173
log.Println("Response with JA4R:", response.Status)
174+
log.Println("Response with JA4R:", response.Status)
166175
}
167176
```
168177

@@ -547,6 +556,49 @@ func main() {
547556

548557
**Note:** Use `Init()` for standard compatibility with `chan Response`. Use `Init(cycletls.WithRawBytes())` when you need the performance benefits of handling raw `[]byte` responses directly.
549558

559+
#### Performance Enhancement: Raw Bytes Option
560+
561+
The default `Init()` method provides the standard v1 API with `chan Response`. For performance-critical applications that can handle raw bytes, use the `WithRawBytes()` option:
562+
563+
```go
564+
package main
565+
566+
import (
567+
"encoding/json"
568+
"fmt"
569+
"github.com/Danny-Dasilva/CycleTLS/cycletls"
570+
)
571+
572+
func main() {
573+
// Use WithRawBytes() option for performance enhancement
574+
client := cycletls.Init(cycletls.WithRawBytes())
575+
defer client.Close()
576+
577+
// Queue a request
578+
go func() {
579+
client.Queue("https://ja3er.com/json", cycletls.Options{
580+
Ja3: "771,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-51-57-47-53-10,0-23-65281-10-11-35-16-5-51-43-13-45-28-21,29-23-24-25-256-257,0",
581+
UserAgent: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:87.0) Gecko/20100101 Firefox/87.0",
582+
}, "GET")
583+
}()
584+
585+
// Performance pattern: receive raw bytes from RespChanV2
586+
select {
587+
case responseBytes := <-client.RespChanV2:
588+
var response cycletls.Response
589+
json.Unmarshal(responseBytes, &response)
590+
fmt.Printf("Status: %d\n", response.Status)
591+
fmt.Printf("Body: %s\n", response.Body)
592+
// Alternative: still supports v1 pattern via RespChan
593+
case response := <-client.RespChan:
594+
fmt.Printf("Status: %d\n", response.Status)
595+
fmt.Printf("Body: %s\n", response.Body)
596+
}
597+
}
598+
```
599+
600+
**Note:** Use `Init()` for standard compatibility with `chan Response`. Use `Init(cycletls.WithRawBytes())` when you need the performance benefits of handling raw `[]byte` responses directly.
601+
550602
## Creating an instance
551603

552604
In order to create a `CycleTLS` instance, you can run the following:
@@ -868,6 +920,8 @@ client := &http.Client{Transport: transport}
868920
ja3: '771,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-51-57-47-53-10,0-23-65281-10-11-35-16-5-51-43-13-45-28-21,29-23-24-25-256-257,0',
869921
// JA4R token for enhanced fingerprinting (raw format)
870922
ja4r: 't13d1516h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0000,0005,000a,000b,000d,0012,0017,001b,0023,002b,002d,0033,44cd,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601',
923+
// JA4R token for enhanced fingerprinting (raw format)
924+
ja4r: 't13d1516h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0000,0005,000a,000b,000d,0012,0017,001b,0023,002b,002d,0033,44cd,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601',
871925
// User agent for request
872926
userAgent: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:87.0) Gecko/20100101 Firefox/87.0',
873927
// Proxy to send request through (supports http, socks4, socks5, socks5h)
@@ -2789,6 +2843,7 @@ type Browser struct {
27892843
UserAgent string
27902844
JA3 string
27912845
JA4r string
2846+
JA4r string
27922847
HTTP2Fingerprint string
27932848
QUICFingerprint string
27942849
InsecureSkipVerify bool
@@ -2813,12 +2868,17 @@ type SSEEvent struct {
28132868
28142869
</details>
28152870
2871+
### How do I use JA4R fingerprinting?
28162872
### How do I use JA4R fingerprinting?
28172873
28182874
<details>
28192875
28202876
> **Note:** Pass `ja4r` (raw format) to configure fingerprints. JA4 hashes are for observation only.
2877+
> **Note:** Pass `ja4r` (raw format) to configure fingerprints. JA4 hashes are for observation only.
2878+
2879+
JA4R is the raw format for configuring TLS fingerprints with explicit cipher suites and extensions.
28212880
2881+
### Golang JA4R Fingerprinting
28222882
JA4R is the raw format for configuring TLS fingerprints with explicit cipher suites and extensions.
28232883
28242884
### Golang JA4R Fingerprinting
@@ -2835,10 +2895,12 @@ func main() {
28352895
client := cycletls.Init()
28362896
defer client.Close()
28372897

2898+
// Use both JA3 and JA4R fingerprints
28382899
// Use both JA3 and JA4R fingerprints
28392900
response, err := client.Do("https://tls.peet.ws/api/clean", cycletls.Options{
28402901
Ja3: "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0",
28412902
Ja4r: "t13d1516h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0000,0005,000a,000b,000d,0012,0017,001b,0023,002b,002d,0033,44cd,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601", // JA4R fingerprint (raw format)
2903+
Ja4r: "t13d1516h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0000,0005,000a,000b,000d,0012,0017,001b,0023,002b,002d,0033,44cd,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601", // JA4R fingerprint (raw format)
28422904
UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36",
28432905
}, "GET")
28442906

@@ -2847,6 +2909,7 @@ func main() {
28472909
}
28482910

28492911
log.Println("Response with JA4R:", response.Status)
2912+
log.Println("Response with JA4R:", response.Status)
28502913
}
28512914
```
28522915

cycletls/tests/integration/tls13_auto_retry_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ func TestTLS13ForceRenegotiation(t *testing.T) {
6363
t.Errorf("❌ CRITICAL: Force TLS 1.3 renegotiation failed: %s", err.Error())
6464
} else {
6565
t.Logf("🎉 SUCCESS: Force TLS 1.3 renegotiation worked! Status: %d, Duration: %v", response.Status, duration)
66-
6766
// Check if response indicates TLS 1.3 was used
6867
if strings.Contains(string(response.Body), "TLS 1.3") {
6968
t.Logf("✅ CONFIRMED: Server response indicates TLS 1.3 was negotiated")

tests/test-utils.js

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
const initCycleTLS = require("../dist/index.js");
2+
3+
// Track all active instances for emergency cleanup
4+
const activeInstances = new Set();
5+
6+
/**
7+
* Wrapper function that ensures CycleTLS instance cleanup even if test fails.
8+
* Use this for individual test cases that need a single CycleTLS instance.
9+
*
10+
* @param {number|object} portOrOptions - Port number or full options object
11+
* @param {Function} testFn - Test function that receives the CycleTLS instance
12+
* @returns {Promise} Promise that resolves with test function result
13+
*
14+
* @example
15+
* test("Should handle timeout", async () => {
16+
* await withCycleTLS(9117, async (cycleTLS) => {
17+
* const response = await cycleTLS('https://example.com');
18+
* expect(response.status).toBe(200);
19+
* });
20+
* });
21+
*/
22+
async function withCycleTLS(portOrOptions, testFn) {
23+
const options = typeof portOrOptions === 'number'
24+
? { port: portOrOptions }
25+
: portOrOptions;
26+
27+
const cycleTLS = await initCycleTLS(options);
28+
activeInstances.add(cycleTLS);
29+
30+
try {
31+
return await testFn(cycleTLS);
32+
} finally {
33+
activeInstances.delete(cycleTLS);
34+
await cycleTLS.exit();
35+
}
36+
}
37+
38+
/**
39+
* Alternative approach for tests that need multiple CycleTLS instances
40+
* or want more control over instance lifecycle.
41+
*
42+
* @param {object} options - CycleTLS initialization options
43+
* @returns {Promise} CycleTLS instance that's tracked for cleanup
44+
*
45+
* @example
46+
* test("Multiple instances", async () => {
47+
* const cycleTLS1 = await createSafeCycleTLS({ port: 9001 });
48+
* const cycleTLS2 = await createSafeCycleTLS({ port: 9002 });
49+
*
50+
* try {
51+
* // Test logic here
52+
* } finally {
53+
* await cleanupCycleTLS(cycleTLS1);
54+
* await cleanupCycleTLS(cycleTLS2);
55+
* }
56+
* });
57+
*/
58+
async function createSafeCycleTLS(options) {
59+
const cycleTLS = await initCycleTLS(options);
60+
activeInstances.add(cycleTLS);
61+
return cycleTLS;
62+
}
63+
64+
/**
65+
* Manual cleanup function for instances created with createSafeCycleTLS
66+
*
67+
* @param {object} instance - CycleTLS instance to cleanup
68+
*/
69+
async function cleanupCycleTLS(instance) {
70+
if (activeInstances.has(instance)) {
71+
activeInstances.delete(instance);
72+
await instance.exit();
73+
}
74+
}
75+
76+
/**
77+
* Wrapper for test suites that share a single CycleTLS instance
78+
* Use this with Jest's beforeAll/afterAll hooks
79+
*
80+
* @param {object} options - CycleTLS initialization options
81+
* @returns {object} Object with instance and cleanup function
82+
*
83+
* @example
84+
* describe("Test Suite", () => {
85+
* let cycleTLS;
86+
* let cleanup;
87+
*
88+
* beforeAll(async () => {
89+
* ({ instance: cycleTLS, cleanup } = await createSuiteInstance({ port: 9001 }));
90+
* });
91+
*
92+
* afterAll(async () => {
93+
* await cleanup();
94+
* });
95+
*
96+
* test("test 1", async () => {
97+
* // Use cycleTLS here
98+
* });
99+
* });
100+
*/
101+
async function createSuiteInstance(options) {
102+
const instance = await createSafeCycleTLS(options);
103+
104+
const cleanup = async () => {
105+
await cleanupCycleTLS(instance);
106+
};
107+
108+
return { instance, cleanup };
109+
}
110+
111+
/**
112+
* Get count of active CycleTLS instances (useful for debugging)
113+
*/
114+
function getActiveInstanceCount() {
115+
return activeInstances.size;
116+
}
117+
118+
/**
119+
* Emergency cleanup of all active instances
120+
* This is called automatically on process exit
121+
*/
122+
async function cleanupAll() {
123+
if (activeInstances.size > 0) {
124+
console.warn(`⚠️ Cleaning up ${activeInstances.size} orphaned CycleTLS instances`);
125+
const instances = [...activeInstances];
126+
await Promise.all(instances.map(async (instance) => {
127+
try {
128+
await instance.exit();
129+
} catch (error) {
130+
console.error('Error cleaning up CycleTLS instance:', error);
131+
}
132+
}));
133+
activeInstances.clear();
134+
}
135+
}
136+
137+
// Global cleanup hooks to prevent process hanging
138+
// These will catch instances that weren't properly cleaned up
139+
process.on('beforeExit', async () => {
140+
await cleanupAll();
141+
});
142+
143+
process.on('SIGTERM', async () => {
144+
await cleanupAll();
145+
process.exit(0);
146+
});
147+
148+
process.on('SIGINT', async () => {
149+
await cleanupAll();
150+
process.exit(0);
151+
});
152+
153+
// Jest global cleanup hook
154+
if (typeof afterAll !== 'undefined') {
155+
afterAll(async () => {
156+
await cleanupAll();
157+
});
158+
}
159+
160+
module.exports = {
161+
withCycleTLS,
162+
createSafeCycleTLS,
163+
cleanupCycleTLS,
164+
createSuiteInstance,
165+
getActiveInstanceCount
166+
};

0 commit comments

Comments
 (0)