180 lines
8.1 KiB
Go
180 lines
8.1 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestDownloaderCoverDNSFallbackUsesConfiguredProxy(t *testing.T) {
|
|
var calls atomic.Int32
|
|
proxy := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
calls.Add(1)
|
|
if request.Method != http.MethodConnect || request.Host != "dns.alidns.com:443" && request.Host != "dns.google:443" {
|
|
t.Error("unexpected DNS proxy destination", request.Method, request.Host)
|
|
}
|
|
http.Error(writer, "local DNS failure fixture", http.StatusBadGateway)
|
|
}))
|
|
defer proxy.Close()
|
|
cfg := defaultConfig()
|
|
cfg.dataDir, cfg.OutputDir, cfg.ProxyURL = t.TempDir(), t.TempDir(), proxy.URL
|
|
downloader := NewDownloader(cfg)
|
|
defer downloader.client.CloseIdleConnections()
|
|
transport := downloader.client.Transport.(*imageTransport)
|
|
transport.lookup = func(context.Context, string) ([]string, error) {
|
|
return []string{"198.18.0.42"}, nil
|
|
}
|
|
app := &UIApp{downloader: downloader}
|
|
ctx := context.WithValue(context.Background(), coverSourceKey{}, sourceHongguo)
|
|
_, err := app.loadCoverImage(ctx, "https://covers.example.org/fixture", nil)
|
|
if err == nil || !strings.Contains(err.Error(), "可信 DNS 重解析失败") || calls.Load() != 2 {
|
|
t.Fatal("registered cover did not use the default DNS fallback through its configured proxy", calls.Load(), err)
|
|
}
|
|
}
|
|
|
|
func TestDownloaderCoverDNSFallbackRecoversPublicOrigin(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
addresses []string
|
|
err error
|
|
dnsCalls int32
|
|
}{
|
|
{name: "public-dns", addresses: []string{"93.184.216.34"}},
|
|
{name: "mihomo-fake-ip", addresses: []string{"198.18.0.42"}, dnsCalls: 1},
|
|
{name: "docker-private-ip", addresses: []string{"172.18.0.2"}, dnsCalls: 1},
|
|
{name: "private-ipv6", addresses: []string{"fc00::1"}, dnsCalls: 1},
|
|
{name: "mapped-private-ipv4", addresses: []string{"::ffff:192.168.1.1"}, dnsCalls: 1},
|
|
{name: "mixed-dns", addresses: []string{"93.184.216.35", "198.18.0.42"}, dnsCalls: 1},
|
|
{name: "empty-dns", dnsCalls: 1},
|
|
{name: "dns-error", err: errors.New("local DNS unavailable"), dnsCalls: 1},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
var dnsCalls, targetCalls, dials atomic.Int32
|
|
target := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
targetCalls.Add(1)
|
|
if request.Host != "example.com" || request.TLS.ServerName != "example.com" || request.URL.RawQuery != "signature=%2F%2b%3D" || request.Header.Get("Referer") != "https://source.example.org/" {
|
|
t.Error("DNS recovery changed the original Host, SNI, signature or source header")
|
|
}
|
|
io.WriteString(writer, "local text transport fixture")
|
|
}))
|
|
defer target.Close()
|
|
dns := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
dnsCalls.Add(1)
|
|
if request.URL.Query().Get("name") != "example.com" {
|
|
t.Error("unexpected DNS hostname", request.URL)
|
|
}
|
|
writeDNSFixture(writer, "93.184.216.34")
|
|
}))
|
|
defer dns.Close()
|
|
cfg := defaultConfig()
|
|
cfg.dataDir, cfg.OutputDir, cfg.ProxyURL = t.TempDir(), t.TempDir(), "direct"
|
|
downloader := NewDownloader(cfg)
|
|
defer downloader.client.CloseIdleConnections()
|
|
transport := downloader.client.Transport.(*imageTransport)
|
|
transport.lookup = func(context.Context, string) ([]string, error) { return test.addresses, test.err }
|
|
transport.resolver.endpoints = []string{dns.URL}
|
|
transport.standard.TLSClientConfig = target.Client().Transport.(*http.Transport).TLSClientConfig.Clone()
|
|
transport.standard.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
dials.Add(1)
|
|
if address != "93.184.216.34:443" {
|
|
t.Error("attempted a connection outside the verified public destination", address)
|
|
return nil, errors.New("unexpected connection")
|
|
}
|
|
return (&net.Dialer{}).DialContext(ctx, network, target.Listener.Addr().String())
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
ctx = context.WithValue(context.WithValue(ctx, coverRequestKey{}, true), coverSourceKey{}, sourceHongguo)
|
|
for range 2 {
|
|
request, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com/fixture?signature=%2F%2b%3D", nil)
|
|
request.Header.Set("Referer", "https://source.example.org/")
|
|
response, err := downloader.client.Do(request)
|
|
if err != nil {
|
|
t.Fatal("default cover DNS recovery failed", err)
|
|
}
|
|
body, err := io.ReadAll(response.Body)
|
|
response.Body.Close()
|
|
if err != nil || string(body) != "local text transport fixture" || response.Request.URL.String() != request.URL.String() {
|
|
t.Fatal("DNS recovery changed the response or original URL", err)
|
|
}
|
|
}
|
|
if dnsCalls.Load() != test.dnsCalls || targetCalls.Load() != 2 || dials.Load() != 1 {
|
|
t.Fatal("unexpected DNS or connection reuse", dnsCalls.Load(), targetCalls.Load(), dials.Load())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDownloaderCoverDNSFallbackRejectsUnsafeDestinations(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
url string
|
|
source string
|
|
addresses []string
|
|
redirect bool
|
|
dnsCalls int32
|
|
}{
|
|
{name: "literal-private", url: "https://10.0.0.1/fixture", source: sourceHongguo},
|
|
{name: "literal-fake-ip", url: "https://198.18.0.42/fixture", source: sourceHongguo},
|
|
{name: "unregistered-source", url: "https://example.com/fixture"},
|
|
{name: "private-answer", url: "https://example.com/fixture", source: sourceHongguo, addresses: []string{"127.0.0.1"}, dnsCalls: 2},
|
|
{name: "mixed-answer", url: "https://example.com/fixture", source: sourceHongguo, addresses: []string{"93.184.216.34", "10.0.0.1"}, dnsCalls: 2},
|
|
{name: "private-redirect", url: "https://example.com/fixture", source: sourceHongguo, addresses: []string{"169.254.169.254"}, redirect: true, dnsCalls: 3},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
var dnsCalls, targetCalls atomic.Int32
|
|
target := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
targetCalls.Add(1)
|
|
http.Redirect(writer, request, "https://redirect.example.com/fixture", http.StatusFound)
|
|
}))
|
|
defer target.Close()
|
|
dns := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
dnsCalls.Add(1)
|
|
if test.redirect && request.URL.Query().Get("name") == "example.com" {
|
|
writeDNSFixture(writer, "93.184.216.34")
|
|
return
|
|
}
|
|
writeDNSFixture(writer, test.addresses...)
|
|
}))
|
|
defer dns.Close()
|
|
cfg := defaultConfig()
|
|
cfg.dataDir, cfg.OutputDir, cfg.ProxyURL = t.TempDir(), t.TempDir(), "direct"
|
|
downloader := NewDownloader(cfg)
|
|
defer downloader.client.CloseIdleConnections()
|
|
transport := downloader.client.Transport.(*imageTransport)
|
|
transport.lookup = func(context.Context, string) ([]string, error) { return []string{"198.18.0.42"}, nil }
|
|
transport.resolver.endpoints = []string{dns.URL + "/first", dns.URL + "/second"}
|
|
transport.standard.TLSClientConfig = target.Client().Transport.(*http.Transport).TLSClientConfig.Clone()
|
|
transport.standard.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
if !test.redirect || address != "93.184.216.34:443" {
|
|
t.Error("unsafe destination reached the dialer", address)
|
|
return nil, errors.New("unexpected connection")
|
|
}
|
|
return (&net.Dialer{}).DialContext(ctx, network, target.Listener.Addr().String())
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
ctx = context.WithValue(context.WithValue(ctx, coverRequestKey{}, true), coverSourceKey{}, test.source)
|
|
request, _ := http.NewRequestWithContext(ctx, http.MethodGet, test.url, nil)
|
|
response, err := downloader.client.Do(request)
|
|
if response != nil {
|
|
response.Body.Close()
|
|
}
|
|
wantTargets := int32(0)
|
|
if test.redirect {
|
|
wantTargets = 1
|
|
}
|
|
if err == nil || dnsCalls.Load() != test.dnsCalls || targetCalls.Load() != wantTargets {
|
|
t.Fatal("unsafe cover resolution or redirect was accepted", dnsCalls.Load(), targetCalls.Load(), err)
|
|
}
|
|
})
|
|
}
|
|
}
|