Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <cursoragent@cursor.com>
138 lines
4.3 KiB
Go
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
|
|
}
|