package httpclient
import (
"context"
"net"
"net/http"
"net/netip"
"net/url"
"time"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/utils/netguard"
)
type uaTransport struct {
base http.RoundTripper
}
func (t *uaTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if _, ok := req.Header["User-Agent"]; !ok {
req = req.Clone(req.Context())
req.Header.Set("User-Agent", consts.HTTPUserAgent)
}
return t.base.RoundTrip(req)
}
func NewTransport(base http.RoundTripper) http.RoundTripper {
if base == nil {
base = http.DefaultTransport
}
return &uaTransport{base: base}
}
func New(timeout time.Duration) *http.Client {
return &http.Client{Timeout: timeout, Transport: NewTransport(nil)}
}
var proxyFunc = http.ProxyFromEnvironment
type proxyAddrKey struct{}
func NewExternal(timeout time.Duration, allowed ...netip.Prefix) *http.Client {
t := http.DefaultTransport.(*http.Transport).Clone()
t.Proxy = proxyFunc
direct := net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}
guarded := direct
guarded.Control = netguard.DialControl(allowed...)
t.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
if proxy, ok := ctx.Value(proxyAddrKey{}).(string); ok && proxy == addr {
return direct.DialContext(ctx, network, addr)
}
return guarded.DialContext(ctx, network, addr)
}
return &http.Client{Timeout: timeout, Transport: NewTransport(&proxyTagger{base: t})}
}
type proxyTagger struct{ base http.RoundTripper }
func (p *proxyTagger) RoundTrip(req *http.Request) (*http.Response, error) {
if u, err := proxyFunc(req); err == nil && u != nil {
req = req.WithContext(context.WithValue(req.Context(), proxyAddrKey{}, proxyAddr(u)))
}
return p.base.RoundTrip(req)
}
func proxyAddr(u *url.URL) string {
port := u.Port()
if port == "" {
port = map[string]string{"http": "80", "https": "443", "socks5": "1080", "socks5h": "1080"}[u.Scheme]
}
return net.JoinHostPort(u.Hostname(), port)
}