@@ -2,14 +2,36 @@ package cycletls
22
33import (
44 "context"
5+ "crypto/sha256"
6+ "fmt"
57 fhttp "github.com/Danny-Dasilva/fhttp"
8+ "sync"
69 "time"
710
811 "github.com/gorilla/websocket"
912 "golang.org/x/net/proxy"
1013 utls "github.com/refraction-networking/utls"
1114)
1215
16+ // Global client pool for connection reuse
17+ var (
18+ clientPool = make (map [string ]fhttp.Client )
19+ clientPoolMutex = sync.RWMutex {}
20+ )
21+
22+ // ClientPoolEntry represents a cached client with metadata
23+ type ClientPoolEntry struct {
24+ Client fhttp.Client
25+ CreatedAt time.Time
26+ LastUsed time.Time
27+ }
28+
29+ // Global client pool with metadata
30+ var (
31+ advancedClientPool = make (map [string ]* ClientPoolEntry )
32+ advancedClientPoolMutex = sync.RWMutex {}
33+ )
34+
1335type Browser struct {
1436 // TLS fingerprinting options
1537 JA3 string
@@ -127,14 +149,86 @@ func NewTransportWithProxy(ja3 string, useragent string, proxy proxy.ContextDial
127149 }, proxy )
128150}
129151
152+ // generateClientKey creates a unique key for client pooling based on browser configuration
153+ func generateClientKey (browser Browser , timeout int , disableRedirect bool , proxyURL string ) string {
154+ // Create a hash of the configuration that affects connection behavior
155+ configStr := fmt .Sprintf ("ja3:%s|ja4:%s|http2:%s|quic:%s|ua:%s|proxy:%s|timeout:%d|redirect:%t|skipverify:%t|forcehttp1:%t|forcehttp3:%t" ,
156+ browser .JA3 ,
157+ browser .JA4 ,
158+ browser .HTTP2Fingerprint ,
159+ browser .QUICFingerprint ,
160+ browser .UserAgent ,
161+ proxyURL ,
162+ timeout ,
163+ disableRedirect ,
164+ browser .InsecureSkipVerify ,
165+ browser .ForceHTTP1 ,
166+ browser .ForceHTTP3 ,
167+ )
168+
169+ // Generate SHA256 hash for the key
170+ hash := sha256 .Sum256 ([]byte (configStr ))
171+ return fmt .Sprintf ("%x" , hash [:16 ]) // Use first 16 bytes for shorter key
172+ }
130173
174+ // getOrCreateClient retrieves a client from the pool or creates a new one
175+ func getOrCreateClient (browser Browser , timeout int , disableRedirect bool , userAgent string , enableConnectionReuse bool , proxyURL ... string ) (fhttp.Client , error ) {
176+ // If connection reuse is disabled, always create a new client
177+ if ! enableConnectionReuse {
178+ return createNewClient (browser , timeout , disableRedirect , userAgent , proxyURL ... )
179+ }
180+
181+ proxy := ""
182+ if len (proxyURL ) > 0 {
183+ proxy = proxyURL [0 ]
184+ }
185+
186+ clientKey := generateClientKey (browser , timeout , disableRedirect , proxy )
187+
188+ // Try to get existing client from pool
189+ advancedClientPoolMutex .RLock ()
190+ if entry , exists := advancedClientPool [clientKey ]; exists {
191+ // Update last used time
192+ entry .LastUsed = time .Now ()
193+ client := entry .Client
194+ advancedClientPoolMutex .RUnlock ()
195+ return client , nil
196+ }
197+ advancedClientPoolMutex .RUnlock ()
198+
199+ // Create new client if not found in pool
200+ advancedClientPoolMutex .Lock ()
201+ defer advancedClientPoolMutex .Unlock ()
202+
203+ // Double-check in case another goroutine created it while we were waiting for the write lock
204+ if entry , exists := advancedClientPool [clientKey ]; exists {
205+ entry .LastUsed = time .Now ()
206+ return entry .Client , nil
207+ }
208+
209+ // Create new client
210+ client , err := createNewClient (browser , timeout , disableRedirect , userAgent , proxyURL ... )
211+ if err != nil {
212+ return fhttp.Client {}, err
213+ }
214+
215+ // Add to pool
216+ now := time .Now ()
217+ advancedClientPool [clientKey ] = & ClientPoolEntry {
218+ Client : client ,
219+ CreatedAt : now ,
220+ LastUsed : now ,
221+ }
222+
223+ return client , nil
224+ }
131225
132- // newClient creates a new http client
133- func newClient (browser Browser , timeout int , disableRedirect bool , UserAgent string , proxyURL ... string ) (fhttp.Client , error ) {
226+ // createNewClient creates a new HTTP client (internal function)
227+ func createNewClient (browser Browser , timeout int , disableRedirect bool , userAgent string , proxyURL ... string ) (fhttp.Client , error ) {
134228 var dialer proxy.ContextDialer
135229 if len (proxyURL ) > 0 && len (proxyURL [0 ]) > 0 {
136230 var err error
137- dialer , err = newConnectDialer (proxyURL [0 ], UserAgent )
231+ dialer , err = newConnectDialer (proxyURL [0 ], userAgent )
138232 if err != nil {
139233 return fhttp.Client {
140234 Timeout : time .Duration (timeout ) * time .Second ,
@@ -148,6 +242,30 @@ func newClient(browser Browser, timeout int, disableRedirect bool, UserAgent str
148242 return clientBuilder (browser , dialer , timeout , disableRedirect ), nil
149243}
150244
245+ // cleanupClientPool removes old unused clients from the pool
246+ func cleanupClientPool (maxAge time.Duration ) {
247+ advancedClientPoolMutex .Lock ()
248+ defer advancedClientPoolMutex .Unlock ()
249+
250+ now := time .Now ()
251+ for key , entry := range advancedClientPool {
252+ if now .Sub (entry .LastUsed ) > maxAge {
253+ delete (advancedClientPool , key )
254+ }
255+ }
256+ }
257+
258+ // newClient creates a new http client (backward compatibility - defaults to no connection reuse)
259+ func newClient (browser Browser , timeout int , disableRedirect bool , UserAgent string , proxyURL ... string ) (fhttp.Client , error ) {
260+ // Backward compatibility: default to no connection reuse for existing code
261+ return getOrCreateClient (browser , timeout , disableRedirect , UserAgent , false , proxyURL ... )
262+ }
263+
264+ // newClientWithReuse creates a new http client with configurable connection reuse
265+ func newClientWithReuse (browser Browser , timeout int , disableRedirect bool , UserAgent string , enableConnectionReuse bool , proxyURL ... string ) (fhttp.Client , error ) {
266+ return getOrCreateClient (browser , timeout , disableRedirect , UserAgent , enableConnectionReuse , proxyURL ... )
267+ }
268+
151269// WebSocketConnect establishes a WebSocket connection
152270func (browser Browser ) WebSocketConnect (ctx context.Context , urlStr string ) (* websocket.Conn , * fhttp.Response , error ) {
153271 // Create TLS config from browser settings
@@ -210,8 +328,8 @@ func (browser Browser) WebSocketConnect(ctx context.Context, urlStr string) (*we
210328
211329// SSEConnect establishes an SSE connection
212330func (browser Browser ) SSEConnect (ctx context.Context , urlStr string ) (* SSEResponse , error ) {
213- // Create HTTP client
214- httpClient , err := newClient (browser , 30 , false , browser .UserAgent )
331+ // Create HTTP client with connection reuse enabled
332+ httpClient , err := newClientWithReuse (browser , 30 , false , browser .UserAgent , true )
215333 if err != nil {
216334 return nil , err
217335 }
0 commit comments