package httpclient import ( "net" "net/http" "time" ) // Service provides an HTTP client with proper timeouts. // This addresses OWASP security concern B1: using http.DefaultClient which has // no timeouts and can be vulnerable to slowloris attacks and resource exhaustion. // // Note: TLS/SSL is handled by Caddy reverse proxy in production (see OWASP report // A04-4.1 "Certificate Pinning Not Required" - BY DESIGN). This service focuses // on adding timeouts, not TLS configuration. // // For large file downloads, use DoDownloadNoTimeout() which relies on the request's // context for cancellation instead of a fixed timeout. This allows multi-gigabyte // files to download without timeout issues while still being cancellable. type Service struct { // client is the configured HTTP client for API requests client *http.Client // downloadClient is a separate client for file downloads with longer timeouts downloadClient *http.Client // noTimeoutClient is for large file downloads where context controls cancellation noTimeoutClient *http.Client } // Config holds configuration options for the HTTP client service type Config struct { // RequestTimeout is the overall timeout for API requests (default: 30s) RequestTimeout time.Duration // DownloadTimeout is the overall timeout for file downloads (default: 10m) DownloadTimeout time.Duration // ConnectTimeout is the timeout for establishing connections (default: 10s) ConnectTimeout time.Duration // TLSHandshakeTimeout is the timeout for TLS handshake (default: 10s) TLSHandshakeTimeout time.Duration // IdleConnTimeout is how long idle connections stay in the pool (default: 90s) IdleConnTimeout time.Duration // MaxIdleConns is the max number of idle connections (default: 100) MaxIdleConns int // MaxIdleConnsPerHost is the max idle connections per host (default: 10) MaxIdleConnsPerHost int } // DefaultConfig returns sensible default configuration values func DefaultConfig() Config { return Config{ RequestTimeout: 30 * time.Second, DownloadTimeout: 10 * time.Minute, ConnectTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second, IdleConnTimeout: 90 * time.Second, MaxIdleConns: 100, MaxIdleConnsPerHost: 10, } } // ProvideService creates a new HTTP client service with secure defaults func ProvideService() *Service { return NewService(DefaultConfig()) } // NewService creates a new HTTP client service with the given configuration func NewService(cfg Config) *Service { // Create transport with timeouts and connection pooling // Note: We don't set TLSClientConfig - Go's defaults are secure and // production uses Caddy for TLS termination anyway transport := &http.Transport{ DialContext: (&net.Dialer{ Timeout: cfg.ConnectTimeout, KeepAlive: 30 * time.Second, }).DialContext, TLSHandshakeTimeout: cfg.TLSHandshakeTimeout, IdleConnTimeout: cfg.IdleConnTimeout, MaxIdleConns: cfg.MaxIdleConns, MaxIdleConnsPerHost: cfg.MaxIdleConnsPerHost, ExpectContinueTimeout: 1 * time.Second, ForceAttemptHTTP2: true, } // Create the main client for API requests client := &http.Client{ Transport: transport, Timeout: cfg.RequestTimeout, } // Create a separate transport for downloads with longer timeouts downloadTransport := &http.Transport{ DialContext: (&net.Dialer{ Timeout: cfg.ConnectTimeout, KeepAlive: 30 * time.Second, }).DialContext, TLSHandshakeTimeout: cfg.TLSHandshakeTimeout, IdleConnTimeout: cfg.IdleConnTimeout, MaxIdleConns: cfg.MaxIdleConns, MaxIdleConnsPerHost: cfg.MaxIdleConnsPerHost, ExpectContinueTimeout: 1 * time.Second, ForceAttemptHTTP2: true, // Disable compression for downloads to avoid decompression overhead DisableCompression: true, } // Create the download client with longer timeout downloadClient := &http.Client{ Transport: downloadTransport, Timeout: cfg.DownloadTimeout, } // Create a no-timeout transport for large file downloads // This client has no overall timeout - cancellation is controlled via request context // Connection and TLS handshake still have timeouts to prevent hanging on initial connect noTimeoutTransport := &http.Transport{ DialContext: (&net.Dialer{ Timeout: cfg.ConnectTimeout, KeepAlive: 30 * time.Second, }).DialContext, TLSHandshakeTimeout: cfg.TLSHandshakeTimeout, IdleConnTimeout: cfg.IdleConnTimeout, MaxIdleConns: cfg.MaxIdleConns, MaxIdleConnsPerHost: cfg.MaxIdleConnsPerHost, ExpectContinueTimeout: 1 * time.Second, ForceAttemptHTTP2: true, DisableCompression: true, } // No timeout - relies on context cancellation for large file downloads noTimeoutClient := &http.Client{ Transport: noTimeoutTransport, Timeout: 0, // No timeout } return &Service{ client: client, downloadClient: downloadClient, noTimeoutClient: noTimeoutClient, } } // Client returns the HTTP client for API requests (30s timeout) func (s *Service) Client() *http.Client { return s.client } // DownloadClient returns the HTTP client for file downloads (10m timeout) func (s *Service) DownloadClient() *http.Client { return s.downloadClient } // Do executes an HTTP request using the API client func (s *Service) Do(req *http.Request) (*http.Response, error) { return s.client.Do(req) } // DoDownload executes an HTTP request using the download client (longer timeout) func (s *Service) DoDownload(req *http.Request) (*http.Response, error) { return s.downloadClient.Do(req) } // Get performs an HTTP GET request using the API client func (s *Service) Get(url string) (*http.Response, error) { return s.client.Get(url) } // GetDownload performs an HTTP GET request using the download client (longer timeout) func (s *Service) GetDownload(url string) (*http.Response, error) { return s.downloadClient.Get(url) } // DoLargeDownload executes an HTTP request for large file downloads. // This client has NO overall timeout - cancellation must be handled via the request's context. // Use this for multi-gigabyte files that may take hours to download. // The connection establishment and TLS handshake still have timeouts. // // Example usage: // // ctx, cancel := context.WithCancel(context.Background()) // defer cancel() // Call cancel() to abort the download // req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) // resp, err := httpClient.DoLargeDownload(req) func (s *Service) DoLargeDownload(req *http.Request) (*http.Response, error) { return s.noTimeoutClient.Do(req) } // GetLargeDownload performs an HTTP GET request for large file downloads. // This client has NO overall timeout - the download can run indefinitely. // Use this for multi-gigabyte files. To cancel, use DoLargeDownload with a context. func (s *Service) GetLargeDownload(url string) (*http.Response, error) { return s.noTimeoutClient.Get(url) }