Files
ggjuku/internal/app/network_images.go
T
2026-09-30 10:20:58 +08:00

265 lines
8.0 KiB
Go

package app
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
"sync"
"time"
)
type coverRequestKey struct{}
type coverSourceKey struct{}
var nonPublicImageNetworks = []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/8"), netip.MustParsePrefix("100.64.0.0/10"),
netip.MustParsePrefix("192.0.0.0/24"), netip.MustParsePrefix("192.0.2.0/24"),
netip.MustParsePrefix("198.18.0.0/15"), netip.MustParsePrefix("198.51.100.0/24"),
netip.MustParsePrefix("203.0.113.0/24"), netip.MustParsePrefix("240.0.0.0/4"),
netip.MustParsePrefix("64:ff9b::/96"), netip.MustParsePrefix("64:ff9b:1::/48"),
netip.MustParsePrefix("100::/64"), netip.MustParsePrefix("2001::/32"),
netip.MustParsePrefix("2001:db8::/32"), netip.MustParsePrefix("2002::/16"),
}
func publicImageAddress(value string) bool {
address, err := netip.ParseAddr(value)
if err != nil || address.Zone() != "" {
return false
}
address = address.Unmap()
if !address.IsGlobalUnicast() || address.IsPrivate() {
return false
}
for _, network := range nonPublicImageNetworks {
if network.Contains(address) {
return false
}
}
return true
}
func validImageURL(remote *url.URL) bool {
if remote == nil || remote.Scheme != "https" || remote.Opaque != "" || remote.User != nil ||
(remote.Port() != "" && remote.Port() != "443") {
return false
}
host := strings.ToLower(strings.TrimSuffix(remote.Hostname(), "."))
if _, err := netip.ParseAddr(host); err == nil {
return publicImageAddress(host)
}
if len(host) > 253 || !strings.Contains(host, ".") || strings.ContainsAny(host, ":%\\") {
return false
}
for _, suffix := range []string{"localhost", "local", "internal", "lan", "home", "invalid", "test", "example"} {
if host == suffix || strings.HasSuffix(host, "."+suffix) {
return false
}
}
for _, label := range strings.Split(host, ".") {
if label == "" || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' {
return false
}
for _, ch := range label {
if ch != '-' && (ch < 'a' || ch > 'z') && (ch < '0' || ch > '9') {
return false
}
}
}
return true
}
type imageTransport struct {
base http.RoundTripper
standard *http.Transport
resolver *dnsResolver
lookup func(context.Context, string) ([]string, error)
fallback func(context.Context, string) ([]string, error)
mu sync.Mutex
transports map[string]*http.Transport
}
func newImageTransport(base http.RoundTripper, standard *http.Transport) *imageTransport {
resolver := newDNSResolver(standard)
return &imageTransport{
base: base, standard: standard, resolver: resolver,
lookup: net.DefaultResolver.LookupHost, transports: map[string]*http.Transport{},
fallback: func(ctx context.Context, host string) ([]string, error) {
entry, err := resolver.lookupEntry(ctx, host, dnsAlternateSubnet)
return entry.addresses, err
},
}
}
func (transport *imageTransport) RoundTrip(request *http.Request) (*http.Response, error) {
if request.Context().Value(coverRequestKey{}) != true {
return transport.base.RoundTrip(request)
}
if !validImageURL(request.URL) || request.Method != http.MethodGet {
return nil, errors.New("封面地址不受支持")
}
host := strings.TrimSuffix(request.URL.Hostname(), ".")
fallbackUsed := false
fallback := func() ([]string, error) {
fallbackUsed = true
return transport.fallback(request.Context(), host)
}
addresses, err := transport.lookup(request.Context(), host)
if (err != nil || len(addresses) == 0) && transport.fallback != nil && request.Context().Err() == nil {
addresses, err = fallback()
}
if err != nil {
return nil, fmt.Errorf("封面域名解析失败: %w", err)
}
var proxyURL *url.URL
if transport.standard.Proxy != nil {
proxyURL, err = transport.standard.Proxy(request)
if err != nil {
return nil, err
}
}
tried := map[string]bool{}
lastErr := errors.New("封面没有可用的公网地址")
for attempt := 0; attempt < 2; attempt++ {
if addressErr := validateImageAddresses(addresses); addressErr != nil {
source, _ := request.Context().Value(coverSourceKey{}).(string)
if fallbackUsed || transport.fallback == nil || accountSourceGroup(source) == "" || net.ParseIP(host) != nil || request.Context().Err() != nil {
return nil, addressErr
}
addresses, err = fallback()
if err != nil {
return nil, fmt.Errorf("%v;可信 DNS 重解析失败:%w", addressErr, err)
}
if err = validateImageAddresses(addresses); err != nil {
return nil, fmt.Errorf("可信 DNS 未提供可用封面地址:%w", err)
}
}
for _, address := range addresses {
if tried[address] || len(tried) >= 4 {
continue
}
tried[address] = true
pinned := transport.pinned(host, address, proxyURL)
upstream := request.Clone(request.Context())
upstream.Host = request.URL.Host
upstream.URL.Host = net.JoinHostPort(address, "443")
response, roundTripErr := pinned.RoundTrip(upstream)
if response != nil {
response.Request = request
}
if roundTripErr == nil {
return response, nil
}
lastErr = roundTripErr
if request.Context().Err() != nil {
return nil, request.Context().Err()
}
}
if fallbackUsed || transport.fallback == nil || len(tried) >= 4 {
break
}
addresses, err = fallback()
if err != nil {
break
}
}
return nil, fmt.Errorf("封面 CDN 连接失败: %w", lastErr)
}
func validateImageAddresses(addresses []string) error {
if len(addresses) == 0 {
return errors.New("封面没有可用的公网地址")
}
for _, address := range addresses {
if !publicImageAddress(address) {
return fmt.Errorf("封面地址指向本机、内网或保留网络(%s)", address)
}
}
return nil
}
func (transport *imageTransport) pinned(host, address string, proxyURL *url.URL) *http.Transport {
key := host + "|" + address
if proxyURL != nil {
key += "|" + proxyURL.String()
}
transport.mu.Lock()
defer transport.mu.Unlock()
if existing := transport.transports[key]; existing != nil {
return existing
}
pinned := transport.standard.Clone()
if pinned.TLSClientConfig == nil {
pinned.TLSClientConfig = &tls.Config{}
}
pinned.TLSClientConfig.ServerName = host
pinned.Proxy = http.ProxyURL(proxyURL)
if proxyURL != nil && proxyURL.Scheme == "https" {
plain := *proxyURL
plain.Scheme = "http"
if plain.Port() == "" {
plain.Host = net.JoinHostPort(plain.Hostname(), "443")
}
pinned.Proxy = http.ProxyURL(&plain)
pinned.DialContext = transport.tlsProxyDialer(proxyURL.Hostname())
}
if len(transport.transports) >= 32 {
for key, stale := range transport.transports {
stale.CloseIdleConnections()
delete(transport.transports, key)
break
}
}
transport.transports[key] = pinned
return pinned
}
func (transport *imageTransport) tlsProxyDialer(host string) func(context.Context, string, string) (net.Conn, error) {
return func(ctx context.Context, network, address string) (net.Conn, error) {
dial := transport.standard.DialContext
if dial == nil {
dialer := &net.Dialer{Timeout: 10 * time.Second}
dial = dialer.DialContext
}
connection, err := dial(ctx, network, address)
if err != nil {
return nil, err
}
config := &tls.Config{}
if transport.standard.TLSClientConfig != nil {
config = transport.standard.TLSClientConfig.Clone()
}
config.ServerName, config.NextProtos = host, []string{"http/1.1"}
secured := tls.Client(connection, config)
if transport.standard.TLSHandshakeTimeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, transport.standard.TLSHandshakeTimeout)
defer cancel()
}
if err := secured.HandshakeContext(ctx); err != nil {
connection.Close()
return nil, err
}
return secured, nil
}
}
func (transport *imageTransport) CloseIdleConnections() {
transport.resolver.client.CloseIdleConnections()
if closer, ok := transport.base.(interface{ CloseIdleConnections() }); ok {
closer.CloseIdleConnections()
}
transport.mu.Lock()
defer transport.mu.Unlock()
for key, pinned := range transport.transports {
pinned.CloseIdleConnections()
delete(transport.transports, key)
}
}