199 lines
6.8 KiB
Go
199 lines
6.8 KiB
Go
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)
|
|
}
|