🏡 index : quine-global/quintodrome.git

// Package httpclient provides a shared http.Client factory that identifies
// Navidrome via the User-Agent header on all outgoing requests.
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)
}

// NewTransport wraps base (or http.DefaultTransport if nil) to set the
// Navidrome User-Agent on requests that don't have one.
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)}
}

// proxyFunc resolves the proxy for a request; tests replace it.
var proxyFunc = http.ProxyFromEnvironment

type proxyAddrKey struct{}

// NewExternal is New for URLs from untrusted sources: it refuses to dial private, loopback,
// link-local and unspecified addresses, except those covered by allowed.
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) {
		// Exempt the hop to the proxy, which relays the request and is operator config. A URL
		// aimed at the proxy's own address is not proxied, so it stays guarded.
		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})}
}

// proxyTagger records the proxy each request resolves to, so the dialer can tell a hop to the
// proxy from a dial to the URL's own host.
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)
}

// proxyAddr mirrors how net/http addresses a proxy connection.
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)
}