Files
DenozordecandCursor f8e0520817 feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-02 17:23:59 +07:00

138 lines
4.3 KiB
Go

package pipeline
import (
"context"
"fmt"
"net"
"net/http"
"net/netip"
"time"
"evobgp/internal/httpclient"
)
type cdnRoundTripper struct {
transport *http.Transport
}
func (t *cdnRoundTripper) CloseIdleConnections() { t.transport.CloseIdleConnections() }
func (t *cdnRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if _, err := ValidateCDNURL(req.URL.String()); err != nil {
return nil, err
}
resp, err := t.transport.RoundTrip(req)
if err != nil {
return nil, err
}
// Transport has already decoded gzip here when it was negotiated by net/http.
resp.Body = &limitedCDNBody{ReadCloser: resp.Body, remaining: cdnBodyLimit}
return resp, nil
}
type cdnLookupIP func(context.Context, string) ([]net.IP, error)
type cdnDialContext func(context.Context, string, string) (net.Conn, error)
// cdnDial resolves every new connection and dials only an address from that checked answer.
// A mixed public/private DNS answer is rejected in full, regardless of address ordering.
func cdnDial(ctx context.Context, network, address string, lookup cdnLookupIP, dial cdnDialContext) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, fmt.Errorf("pipeline: cdn dial address: %w", err)
}
if isBlockedCDNHostname(host) {
return nil, fmt.Errorf("pipeline: cdn url blocked host")
}
var ips []net.IP
if addr, err := netip.ParseAddr(host); err == nil {
ips = []net.IP{net.IP(addr.AsSlice())}
} else {
resolveCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
ips, err = lookup(resolveCtx, host)
if err != nil {
return nil, fmt.Errorf("pipeline: cdn url dns lookup: %w", err)
}
}
if len(ips) == 0 {
return nil, fmt.Errorf("pipeline: cdn url dns lookup: no addresses")
}
checked := make([]netip.Addr, 0, len(ips))
for _, ip := range ips {
addr, ok := netip.AddrFromSlice(ip)
if !ok || isBlockedCDNIP(addr) {
return nil, fmt.Errorf("pipeline: cdn url resolves to blocked address")
}
checked = append(checked, addr.Unmap())
}
var dialErr error
for _, addr := range checked {
if err := ctx.Err(); err != nil {
return nil, err
}
conn, err := dial(ctx, network, net.JoinHostPort(addr.String(), port))
if err == nil {
return conn, nil
}
dialErr = err
}
return nil, fmt.Errorf("pipeline: cdn dial: %w", dialErr)
}
func secureCDNClient(hc *http.Client) (*http.Client, error) {
if hc == nil {
hc = httpclient.New(httpclient.DefaultTimeout)
}
client := *hc
if pools, ok := client.Transport.(*pipelineTransport); ok {
client.Transport = pools.cdn
}
if client.Timeout <= 0 {
client.Timeout = httpclient.DefaultTimeout
}
if _, ok := client.Transport.(*cdnRoundTripper); !ok {
base := http.DefaultTransport.(*http.Transport).Clone()
switch tr := client.Transport.(type) {
case nil:
case *http.Transport:
// Keep the caller's trust roots for TLS test servers. Other transport
// hooks (including custom protocols and dialers) are not inherited.
if tr.TLSClientConfig != nil {
base.TLSClientConfig = tr.TLSClientConfig.Clone()
}
default:
return nil, fmt.Errorf("pipeline: cdn client requires *http.Transport")
}
// A proxy connects to the destination outside this process, where its DNS answer
// cannot be checked. CDN requests therefore always connect directly.
base.Proxy = nil
base.DialTLSContext = nil
base.DialTLS = nil
base.TLSNextProto = nil
dialer := &net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}
base.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
return cdnDial(ctx, network, address, func(ctx context.Context, host string) ([]net.IP, error) {
return net.DefaultResolver.LookupIP(ctx, "ip", host)
}, dialer.DialContext)
}
client.Transport = &cdnRoundTripper{transport: base}
}
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
if _, err := ValidateCDNURL(req.URL.String()); err != nil {
return err
}
if len(via) >= 10 {
return fmt.Errorf("pipeline: too many cdn redirects")
}
return nil
}
return &client, nil
}
// NewCDNHTTPClient returns a reusable HTTPS client that pins each CDN connection
// to a checked DNS answer. Environment HTTP proxies are intentionally ignored.
func NewCDNHTTPClient() *http.Client {
client, _ := secureCDNClient(httpclient.New(httpclient.DefaultTimeout))
return client
}