Skip to content

Commit bda0774

Browse files
committed
Add connect timeout and cookie jar support to HTTP client
1 parent 304cd01 commit bda0774

4 files changed

Lines changed: 121 additions & 16 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ gURL is a modern HTTP client tool with support for HTTP/1.0, HTTP/1.1, HTTP/2, a
4444
| `--proxy-negotiate` | | Use HTTP Negotiate authentication on the proxy ||
4545
| **Cookie Management** |
4646
| `--cookie <data\|filename>` | `-b` | Send cookies from string/file ||
47-
| `--cookie-jar <filename>` | `-c` | Write cookies to filename after operation | |
47+
| `--cookie-jar <filename>` | `-c` | Write cookies to filename after operation | |
4848
| **Output Control** |
4949
| `--output <file>` | `-o` | Write to file instead of stdout ||
5050
| `--verbose` | `-v` | Make the operation more talkative ||
@@ -55,7 +55,7 @@ gURL is a modern HTTP client tool with support for HTTP/1.0, HTTP/1.1, HTTP/2, a
5555
| `--max-time <seconds>` | `-m` | Maximum time allowed for the transfer ||
5656
| `--max-redirs <num>` | | Maximum number of redirects allowed ||
5757
| `--location` | `-L` | Follow redirects ||
58-
| `--connect-timeout <seconds>` | | Maximum time allowed for connection | |
58+
| `--connect-timeout <seconds>` | | Maximum time allowed for connection | |
5959
| **SSL/TLS Options** |
6060
| `--cacert <file>` | | CA certificate to verify peer against ||
6161
| `--capath <dir>` | | CA directory to verify peer against ||

cmd/requests.go

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"fmt"
55
"io"
6+
"net/url"
67
"os"
78
"strings"
89
"time"
@@ -22,6 +23,8 @@ var (
2223
followRedirects bool
2324
maxRedirects int
2425
timeout int
26+
connectTimeout int
27+
cookieJar string
2528
userAgent string
2629
referer string
2730
insecure bool
@@ -127,6 +130,11 @@ func executeRequest(httpMethod, url string) {
127130
c.SetTimeout(time.Duration(timeout) * time.Second)
128131
}
129132

133+
// Apply connect timeout if specified
134+
if connectTimeout > 0 {
135+
c.SetConnectTimeout(time.Duration(connectTimeout) * time.Second)
136+
}
137+
130138
// Set HTTP version
131139
if http10 {
132140
c.SetHTTPVersion("1.0")
@@ -256,10 +264,10 @@ func executeRequest(httpMethod, url string) {
256264
os.Exit(1)
257265
}
258266

259-
handleResponse(response, httpMethod)
267+
handleResponse(response, httpMethod, url)
260268
}
261269

262-
func handleResponse(response *src.Response, httpMethod string) {
270+
func handleResponse(response *src.Response, httpMethod string, requestURL string) {
263271
var output io.Writer = os.Stdout
264272

265273
// Handle output file
@@ -304,4 +312,48 @@ func handleResponse(response *src.Response, httpMethod string) {
304312
if verbose && !silent && outputFile == "" {
305313
fmt.Fprintf(os.Stderr, "HTTP Status: %d\n", response.StatusCode)
306314
}
315+
316+
// Write cookies to cookie jar file if specified
317+
if cookieJar != "" {
318+
saveCookiesToFile(response, cookieJar, requestURL)
319+
}
320+
}
321+
322+
// saveCookiesToFile writes response cookies to the specified file
323+
func saveCookiesToFile(response *src.Response, filename, requestURL string) {
324+
if len(response.Cookie.Mapper) == 0 {
325+
return // No cookies to save
326+
}
327+
328+
file, err := os.Create(filename)
329+
if err != nil {
330+
if !silent {
331+
fmt.Fprintf(os.Stderr, "Error creating cookie jar file: %v\n", err)
332+
}
333+
return
334+
}
335+
defer file.Close()
336+
337+
// Extract domain from URL
338+
parsedURL, err := url.Parse(requestURL)
339+
domain := ".example.com" // fallback
340+
if err == nil && parsedURL.Host != "" {
341+
domain = parsedURL.Host
342+
// Add leading dot for domain cookies
343+
if !strings.HasPrefix(domain, ".") {
344+
domain = "." + domain
345+
}
346+
}
347+
348+
// Write Netscape cookie file format header
349+
fmt.Fprintln(file, "# Netscape HTTP Cookie File")
350+
fmt.Fprintln(file, "# This is a generated file! Do not edit.")
351+
fmt.Fprintln(file, "")
352+
353+
// Write cookies in Netscape format:
354+
// domain flag path secure expiration name value
355+
for name, value := range response.Cookie.Mapper {
356+
// Use Netscape format: domain, flag, path, secure, expiration, name, value
357+
fmt.Fprintf(file, "%s\tTRUE\t/\tFALSE\t0\t%s\t%s\n", domain, name, value)
358+
}
307359
}

cmd/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ func Execute() {
122122

123123
// Cookie flags
124124
rootCmd.PersistentFlags().StringSliceVarP(&cookies, "cookie", "b", []string{}, "Pass the data to the HTTP server in the Cookie header")
125+
rootCmd.PersistentFlags().StringVarP(&cookieJar, "cookie-jar", "c", "", "Write cookies to filename after operation")
125126

126127
// HTTP request flags
127128
rootCmd.PersistentFlags().StringSliceVarP(&headers, "header", "H", []string{}, "Pass custom header(s) to server")
@@ -133,6 +134,7 @@ func Execute() {
133134
rootCmd.PersistentFlags().BoolVarP(&followRedirects, "location", "L", false, "Follow redirects")
134135
rootCmd.PersistentFlags().IntVarP(&maxRedirects, "max-redirs", "", 50, "Maximum number of redirects allowed")
135136
rootCmd.PersistentFlags().IntVarP(&timeout, "max-time", "m", 0, "Maximum time allowed for the transfer")
137+
rootCmd.PersistentFlags().IntVar(&connectTimeout, "connect-timeout", 0, "Maximum time allowed for connection")
136138
rootCmd.PersistentFlags().StringVarP(&userAgent, "user-agent", "A", "", "Send User-Agent <name> to server")
137139
rootCmd.PersistentFlags().StringVarP(&referer, "referer", "e", "", "Referrer URL")
138140
rootCmd.PersistentFlags().BoolVarP(&insecure, "insecure", "k", false, "Allow insecure server connections when using SSL")

src/client.go

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"fmt"
1111
"io"
1212
"mime/multipart"
13+
"net"
1314
"net/http"
1415
"net/url"
1516
"os"
@@ -41,12 +42,13 @@ var (
4142
)
4243

4344
type Client struct {
44-
proxy string // set to all requests
45-
timeout time.Duration
46-
crt *tls.Certificate
47-
opts *requestOptions
48-
httpVersion string // "1.0", "1.1", "2", "3"
49-
insecure bool // allow insecure SSL
45+
proxy string // set to all requests
46+
timeout time.Duration
47+
connectTimeout time.Duration // connection timeout separate from request timeout
48+
crt *tls.Certificate
49+
opts *requestOptions
50+
httpVersion string // "1.0", "1.1", "2", "3"
51+
insecure bool // allow insecure SSL
5052
// Authentication fields
5153
authType string // "basic", "digest", "ntlm", "negotiate"
5254
username string
@@ -85,6 +87,11 @@ func (c *Client) SetTimeout(duration time.Duration) *Client {
8587
return c
8688
}
8789

90+
func (c *Client) SetConnectTimeout(duration time.Duration) *Client {
91+
c.connectTimeout = duration
92+
return c
93+
}
94+
8895
func (c *Client) SetHTTPVersion(version string) *Client {
8996
c.httpVersion = version
9097
return c
@@ -464,6 +471,28 @@ func (c *Client) callFastHTTP(url, method string, headers requestHeaders, body [
464471
client := &fasthttp.Client{
465472
ReadTimeout: c.timeout,
466473
}
474+
475+
// Set connect timeout if specified
476+
if c.connectTimeout > 0 {
477+
client.WriteTimeout = c.connectTimeout
478+
client.ReadTimeout = c.timeout
479+
// Create a custom dialer with connect timeout
480+
if c.proxy != "" {
481+
// Use proxy dialer with connect timeout
482+
client.Dial = func(addr string) (net.Conn, error) {
483+
proxyDialer := fasthttpproxy.FasthttpHTTPDialer(c.proxy)
484+
return proxyDialer(addr)
485+
}
486+
} else {
487+
// Use direct dialer with connect timeout
488+
client.Dial = func(addr string) (net.Conn, error) {
489+
return net.DialTimeout("tcp", addr, c.connectTimeout)
490+
}
491+
}
492+
} else if c.proxy != "" {
493+
client.Dial = fasthttpproxy.FasthttpHTTPDialer(c.proxy)
494+
}
495+
467496
if c.crt != nil {
468497
client.TLSConfig = &tls.Config{
469498
InsecureSkipVerify: c.insecure,
@@ -474,9 +503,6 @@ func (c *Client) callFastHTTP(url, method string, headers requestHeaders, body [
474503
InsecureSkipVerify: true,
475504
}
476505
}
477-
if c.proxy != "" {
478-
client.Dial = fasthttpproxy.FasthttpHTTPDialer(c.proxy)
479-
}
480506

481507
if err := client.Do(req, resp); err != nil {
482508
return nil, err
@@ -490,13 +516,28 @@ func (c *Client) callFastHTTP(url, method string, headers requestHeaders, body [
490516
}
491517
resp.Header.VisitAll(func(key, value []byte) {
492518
ret.Header.Set(string(key), string(value))
493-
})
494-
resp.Header.VisitAllCookie(func(key, value []byte) {
495-
ret.Cookie.Set(string(key), string(value))
519+
// Parse Set-Cookie headers manually
520+
if strings.ToLower(string(key)) == "set-cookie" {
521+
parseCookieFromSetCookie(string(value), ret.Cookie)
522+
}
496523
})
497524
return ret, nil
498525
}
499526

527+
// parseCookieFromSetCookie parses a Set-Cookie header value and extracts the cookie name and value
528+
func parseCookieFromSetCookie(setCookieValue string, cookies RequestCookies) {
529+
// Simple parsing: extract name=value from "name=value; Path=/; ..."
530+
parts := strings.Split(setCookieValue, ";")
531+
if len(parts) > 0 {
532+
nameValue := strings.TrimSpace(parts[0])
533+
if idx := strings.Index(nameValue, "="); idx > 0 {
534+
name := strings.TrimSpace(nameValue[:idx])
535+
value := strings.TrimSpace(nameValue[idx+1:])
536+
cookies.Set(name, value)
537+
}
538+
}
539+
}
540+
500541
func (c *Client) callHTTP2OrHTTP3(url, method string, headers requestHeaders, body []byte) (*Response, error) {
501542
var client *http.Client
502543

@@ -523,6 +564,16 @@ func (c *Client) callHTTP2OrHTTP3(url, method string, headers requestHeaders, bo
523564
TLSClientConfig: tlsConfig,
524565
}
525566

567+
// Set dial timeout if connectTimeout is specified
568+
if c.connectTimeout > 0 {
569+
transport.DialTLS = func(network, addr string, cfg *tls.Config) (net.Conn, error) {
570+
dialer := &net.Dialer{
571+
Timeout: c.connectTimeout,
572+
}
573+
return tls.DialWithDialer(dialer, network, addr, cfg)
574+
}
575+
}
576+
526577
client = &http.Client{
527578
Transport: transport,
528579
Timeout: c.timeout,

0 commit comments

Comments
 (0)