package netutil import ( "crypto/tls" "net/http" "net/url" "time" "streamrip-go/internal/verbose" ) const defaultMaxConnsPerHost = 16 // NewHTTPClient builds an *http.Client whose transport is tuned for the // concurrent download workloads this app issues against single CDN hosts. // // maxConnsPerHost caps idle keep-alive sockets per host; pass <= 0 to use a // sensible default. The downloader and provider clients should pass the // configured concurrency so keep-alive sockets aren't evicted between workers. func NewHTTPClient(timeout time.Duration, verifySSL bool, maxConnsPerHost int) *http.Client { if maxConnsPerHost <= 0 { maxConnsPerHost = defaultMaxConnsPerHost } transport := http.DefaultTransport.(*http.Transport).Clone() if transport.TLSClientConfig == nil { transport.TLSClientConfig = &tls.Config{} } transport.TLSClientConfig.InsecureSkipVerify = !verifySSL transport.MaxIdleConnsPerHost = maxConnsPerHost if maxIdle := maxConnsPerHost * 4; maxIdle > transport.MaxIdleConns { transport.MaxIdleConns = maxIdle } if transport.MaxIdleConns < 100 { transport.MaxIdleConns = 100 } transport.MaxConnsPerHost = 0 transport.IdleConnTimeout = 90 * time.Second transport.WriteBufferSize = 64 * 1024 transport.ReadBufferSize = 64 * 1024 transport.ForceAttemptHTTP2 = true return &http.Client{ Timeout: timeout, Transport: &loggingTransport{base: transport}, } } // loggingTransport emits one verbose line per HTTP request when verbose // level >= VV. The check is per-call so toggling the level at runtime // affects subsequent requests without rebuilding clients. type loggingTransport struct { base http.RoundTripper } func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error) { if !verbose.Enabled(verbose.VV) { return t.base.RoundTrip(req) } start := time.Now() resp, err := t.base.RoundTrip(req) elapsed := time.Since(start).Round(time.Millisecond) target := redactURL(req.URL) if err != nil { verbose.Printf(verbose.VV, "http %s %s -> error %v (%s)\n", req.Method, target, err, elapsed) return resp, err } verbose.Printf(verbose.VV, "http %s %s -> %d (%s)\n", req.Method, target, resp.StatusCode, elapsed) return resp, err } // redactURL hides values for query parameters that commonly carry // credentials so -vv output is safe to paste in an issue. func redactURL(u *url.URL) string { if u == nil { return "" } if u.RawQuery == "" { return u.String() } q := u.Query() redacted := false for k := range q { if isSensitiveParam(k) { q.Set(k, "REDACTED") redacted = true } } if !redacted { return u.String() } cp := *u cp.RawQuery = q.Encode() return cp.String() } func isSensitiveParam(name string) bool { switch name { case "user_auth_token", "api_token", "access_token", "refresh_token", "request_sig", "signature", "password", "secret", "token", "code", "auth", "key": return true } return false }