@@ -4,13 +4,14 @@ import (
44 "bufio"
55 "crypto/tls"
66 "fmt"
7+ "io"
78 "log"
89 "net"
910 "net/http"
10- "net/http/httputil"
1111 "net/url"
1212 "os"
1313 "strings"
14+ "sync"
1415 "time"
1516
1617 "github.com/fatih/color"
@@ -19,6 +20,61 @@ import (
1920// ErrRateLimited is returned when the server responds with HTTP 429.
2021var ErrRateLimited = fmt .Errorf ("rate limited (HTTP 429)" )
2122
23+ // clientCacheKey identifies a unique HTTP client configuration.
24+ type clientCacheKey struct {
25+ proxy string
26+ timeout int
27+ redirect bool
28+ }
29+
30+ var clientCache sync.Map
31+
32+ // getClient returns a cached HTTP client for the given configuration,
33+ // creating one if needed. This enables connection pooling across requests.
34+ func getClient (proxy * url.URL , timeout int , redirect bool ) * http.Client {
35+ proxyStr := ""
36+ if proxy != nil {
37+ proxyStr = proxy .String ()
38+ }
39+ key := clientCacheKey {proxyStr , timeout , redirect }
40+
41+ if v , ok := clientCache .Load (key ); ok {
42+ return v .(* http.Client )
43+ }
44+
45+ timeoutDuration := time .Duration (timeout ) * time .Millisecond
46+ transport := & http.Transport {
47+ Proxy : http .ProxyURL (proxy ),
48+ TLSClientConfig : & tls.Config {
49+ InsecureSkipVerify : true ,
50+ },
51+ DialContext : (& net.Dialer {
52+ Timeout : timeoutDuration ,
53+ KeepAlive : 30 * time .Second ,
54+ }).DialContext ,
55+ MaxIdleConns : 100 ,
56+ MaxIdleConnsPerHost : 10 ,
57+ IdleConnTimeout : 90 * time .Second ,
58+ TLSHandshakeTimeout : timeoutDuration ,
59+ ResponseHeaderTimeout : timeoutDuration ,
60+ ExpectContinueTimeout : 1 * time .Second ,
61+ }
62+
63+ client := & http.Client {
64+ Transport : transport ,
65+ Timeout : timeoutDuration ,
66+ }
67+
68+ if ! redirect {
69+ client .CheckRedirect = func (req * http.Request , via []* http.Request ) error {
70+ return http .ErrUseLastResponse
71+ }
72+ }
73+
74+ clientCache .Store (key , client )
75+ return client
76+ }
77+
2278// parseFile reads a file given its filename and returns a list containing each of its lines.
2379func parseFile (filename string ) ([]string , error ) {
2480 file , err := os .Open (filename )
@@ -56,7 +112,8 @@ type header struct {
56112// requestWithRetry makes an HTTP request with retry logic and exponential backoff.
57113// It retries up to maxRetries times on transient errors (timeouts, connection errors).
58114// On HTTP 429, it retries with backoff if rateLimit is false; returns ErrRateLimited if rateLimit is true.
59- func requestWithRetry (method , uri string , headers []header , proxy * url.URL , rateLimit bool , timeout int , redirect bool ) (int , []byte , error ) {
115+ // Returns (statusCode, responseBodySize, error).
116+ func requestWithRetry (method , uri string , headers []header , proxy * url.URL , rateLimit bool , timeout int , redirect bool ) (int , int , error ) {
60117 const maxRetries = 2
61118 var lastErr error
62119
@@ -66,30 +123,25 @@ func requestWithRetry(method, uri string, headers []header, proxy *url.URL, rate
66123 time .Sleep (backoff )
67124 }
68125
69- statusCode , resp , err := request (method , uri , headers , proxy , timeout , redirect )
126+ statusCode , respSize , err := request (method , uri , headers , proxy , timeout , redirect )
70127 if err == nil {
71- // Handle rate limiting
72128 if statusCode == 429 {
73129 if rateLimit {
74- return statusCode , resp , ErrRateLimited
130+ return statusCode , respSize , ErrRateLimited
75131 }
76132 lastErr = fmt .Errorf ("HTTP 429 rate limited on attempt %d" , attempt + 1 )
77133 continue
78134 }
79- return statusCode , resp , nil
135+ return statusCode , respSize , nil
80136 }
81137
82138 lastErr = err
83- // Only retry on transient errors (timeouts, connection refused, etc.)
84139 if ! isTransientError (err ) {
85- return 0 , nil , err
86- }
87- if attempt < maxRetries {
88- log .Printf ("[!] Transient error (attempt %d/%d): %v" , attempt + 1 , maxRetries + 1 , err )
140+ return 0 , 0 , err
89141 }
90142 }
91143
92- return 0 , nil , fmt .Errorf ("request failed after %d attempts: %w" , maxRetries + 1 , lastErr )
144+ return 0 , 0 , fmt .Errorf ("request failed after %d attempts: %w" , maxRetries + 1 , lastErr )
93145}
94146
95147// isTransientError returns true for errors that are likely transient and worth retrying.
@@ -115,7 +167,8 @@ func isTransientError(err error) bool {
115167}
116168
117169// request makes a single HTTP request using headers `headers` and proxy `proxy`.
118- func request (method , uri string , headers []header , proxy * url.URL , timeout int , redirect bool ) (int , []byte , error ) {
170+ // Returns (statusCode, responseBodySize, error).
171+ func request (method , uri string , headers []header , proxy * url.URL , timeout int , redirect bool ) (int , int , error ) {
119172 if method == "" {
120173 method = "GET"
121174 }
@@ -124,47 +177,26 @@ func request(method, uri string, headers []header, proxy *url.URL, timeout int,
124177 proxy = nil
125178 }
126179
127- timeoutDuration := time .Duration (timeout ) * time .Millisecond
128- customTransport := & http.Transport {
129- Proxy : http .ProxyURL (proxy ),
130- TLSClientConfig : & tls.Config {
131- InsecureSkipVerify : true ,
132- },
133- DialContext : (& net.Dialer {
134- Timeout : timeoutDuration ,
135- KeepAlive : 30 * time .Second ,
136- }).DialContext ,
137- MaxIdleConns : 100 ,
138- IdleConnTimeout : 90 * time .Second ,
139- TLSHandshakeTimeout : timeoutDuration ,
140- ResponseHeaderTimeout : timeoutDuration ,
141- ExpectContinueTimeout : 1 * time .Second ,
142- }
143-
144- client := & http.Client {
145- Transport : customTransport ,
146- Timeout : timeoutDuration ,
147- }
148-
149- if ! redirect {
150- client .CheckRedirect = func (req * http.Request , via []* http.Request ) error {
151- return http .ErrUseLastResponse
152- }
153- }
180+ client := getClient (proxy , timeout , redirect )
154181
155182 parsedURL , err := url .Parse (uri )
156183 if err != nil || parsedURL == nil || parsedURL .Scheme == "" || parsedURL .Host == "" {
157- return 0 , nil , fmt .Errorf ("invalid URL: %q" , uri )
184+ // Fallback for non-standard encoding (e.g., %u002f unicode escapes)
185+ // that url.Parse rejects. Extract scheme/host manually and preserve
186+ // the raw path so the server receives it as-is.
187+ parsedURL , err = parseRawURL (uri )
188+ if err != nil {
189+ return 0 , 0 , fmt .Errorf ("invalid URL: %q" , uri )
190+ }
191+ } else {
192+ parsedURL .RawPath = parsedURL .EscapedPath ()
158193 }
159194
160- parsedURL .RawPath = parsedURL .EscapedPath ()
161-
162195 req := & http.Request {
163196 Method : method ,
164197 Host : parsedURL .Host ,
165198 URL : parsedURL ,
166199 Header : make (http.Header ),
167- Close : true ,
168200 }
169201
170202 for _ , header := range headers {
@@ -173,20 +205,18 @@ func request(method, uri string, headers []header, proxy *url.URL, timeout int,
173205
174206 res , err := client .Do (req )
175207 if err != nil {
176- return 0 , nil , err
208+ return 0 , 0 , err
177209 }
178210 defer func () {
179211 if cerr := res .Body .Close (); cerr != nil {
180212 log .Printf ("[!] Error closing response body: %v" , cerr )
181213 }
182214 }()
183215
184- resp , err := httputil .DumpResponse (res , true )
185- if err != nil {
186- return 0 , nil , err
187- }
216+ // Read and discard body to get size and allow connection reuse
217+ bodySize , _ := io .Copy (io .Discard , res .Body )
188218
189- return res .StatusCode , resp , nil
219+ return res .StatusCode , int ( bodySize ) , nil
190220}
191221
192222// loadFlagsFromRequestFile parse an HTTP request and configure the necessary flags for an execution
@@ -258,13 +288,13 @@ func runAutocalibrate(options RequestOptions) (int, int) {
258288 var lastStatusCode int
259289 for _ , path := range calibrationPaths {
260290 calibrationURI := baseURI + path
261- statusCode , response , err := requestWithRetry ("GET" , calibrationURI , options .headers , options .proxy , options .rateLimit , options .timeout , options .redirect )
291+ statusCode , respSize , err := requestWithRetry ("GET" , calibrationURI , options .headers , options .proxy , options .rateLimit , options .timeout , options .redirect )
262292 if err != nil {
263293 log .Printf ("[!] Error during calibration request (%s): %v\n " , path , err )
264294 continue
265295 }
266296 lastStatusCode = statusCode
267- samples = append (samples , len ( response ) )
297+ samples = append (samples , respSize )
268298 }
269299
270300 if len (samples ) == 0 {
@@ -303,3 +333,46 @@ func runAutocalibrate(options RequestOptions) (int, int) {
303333
304334 return avgCl , tolerance
305335}
336+
337+ // parseRawURL extracts scheme, host, and raw path from a URI without decoding
338+ // percent-encoded sequences. This allows non-standard encodings like %u002f
339+ // (IIS-style Unicode escapes) to be sent to the server as-is.
340+ func parseRawURL (rawURI string ) (* url.URL , error ) {
341+ idx := strings .Index (rawURI , "://" )
342+ if idx < 0 {
343+ return nil , fmt .Errorf ("missing scheme" )
344+ }
345+ scheme := rawURI [:idx ]
346+ if scheme != "http" && scheme != "https" {
347+ return nil , fmt .Errorf ("unsupported scheme %q" , scheme )
348+ }
349+ rest := rawURI [idx + 3 :]
350+
351+ slashIdx := strings .Index (rest , "/" )
352+ var host , rawPath string
353+ if slashIdx < 0 {
354+ host = rest
355+ rawPath = "/"
356+ } else {
357+ host = rest [:slashIdx ]
358+ rawPath = rest [slashIdx :]
359+ }
360+
361+ if host == "" {
362+ return nil , fmt .Errorf ("missing host" )
363+ }
364+
365+ // Split raw path and query
366+ rawQuery := ""
367+ if qIdx := strings .Index (rawPath , "?" ); qIdx >= 0 {
368+ rawQuery = rawPath [qIdx + 1 :]
369+ rawPath = rawPath [:qIdx ]
370+ }
371+
372+ return & url.URL {
373+ Scheme : scheme ,
374+ Host : host ,
375+ Opaque : rawPath ,
376+ RawQuery : rawQuery ,
377+ }, nil
378+ }
0 commit comments