mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-09 13:31:26 -10:00
Update the Go module path, imports, install scripts, docs, workflows, packaging and action defaults from Gitlawb/zero to Twigpine/zero. The npm scope (already @twigpine/zero on main, with its @gitlawb/zero migration notes), LICENSE, action author, the OpenGateway provider and historical CHANGELOG links are unchanged. The X-AIMLAPI-Integration-Repo header keeps Gitlawb/zero: it is what AIMLAPI sees for attribution, so changing it needs a maintainer's yes. docs/INSTALL.md keeps `go install github.com/Gitlawb/zero/...`, because every tagged release so far declares that module path. The Homebrew formula comment names the Twigpine/homebrew-tap tap. Co-authored-by: Claude Opus 5.5 <[email protected]>
164 lines
5.9 KiB
Go
164 lines
5.9 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Twigpine/zero/internal/proxydial"
|
|
)
|
|
|
|
// webFetchTransportWithProxy builds the guarded web_fetch round tripper and
|
|
// swaps in a proxy function, which is how the transport learns its route. The
|
|
// environment is left alone deliberately: http.ProxyFromEnvironment reads it
|
|
// once per process, so an environment-driven test would depend on test order.
|
|
func webFetchTransportWithProxy(t *testing.T, resolver webFetchResolver, proxyFor func(*http.Request) (*url.URL, error)) *proxydial.Transport {
|
|
t.Helper()
|
|
roundTripper := webFetchSafeTransport(nil, resolver)
|
|
wrapped, ok := roundTripper.(*proxydial.Transport)
|
|
if !ok {
|
|
t.Fatalf("transport = %T, want the route-recording wrapper; without it no dial can be recognised as the proxy dial", roundTripper)
|
|
}
|
|
wrapped.Proxy = proxyFor
|
|
return wrapped
|
|
}
|
|
|
|
// THE PROXY IS NOT THE TARGET. With HTTPS_PROXY pointing at a local forward
|
|
// proxy, the transport dials the proxy and tunnels the request through it. The
|
|
// dialer used to refuse that dial as a loopback address, so every fetch on a
|
|
// machine behind such a proxy failed with "loopback hosts are blocked" for a
|
|
// public target that had already been validated (#569).
|
|
func TestWebFetchTunnelsThroughTheConfiguredProxy(t *testing.T) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Close()
|
|
arrived := make(chan string, 1)
|
|
go func() {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
buffer := make([]byte, 256)
|
|
n, _ := conn.Read(buffer)
|
|
arrived <- string(buffer[:n])
|
|
}()
|
|
proxyURL, err := url.Parse("http://" + listener.Addr().String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// A proxied fetch never resolves the target on this side; the CONNECT
|
|
// carries the hostname and the proxy resolves it.
|
|
transport := webFetchTransportWithProxy(t,
|
|
webFetchResolverFunc(func(context.Context, string, string) ([]netip.Addr, error) {
|
|
return nil, errors.New("resolver must not be called for a proxied fetch")
|
|
}),
|
|
func(*http.Request) (*url.URL, error) { return proxyURL, nil })
|
|
client := &http.Client{Transport: transport, Timeout: 3 * time.Second}
|
|
|
|
_, err = client.Get("https://public.example/resource")
|
|
select {
|
|
case first := <-arrived:
|
|
if line := strings.SplitN(first, "\r\n", 2)[0]; line != "CONNECT public.example:443 HTTP/1.1" {
|
|
t.Fatalf("proxy received %q, want a CONNECT for the target", line)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("the proxy never received a connection; request error: %v", err)
|
|
}
|
|
}
|
|
|
|
// AND A DIRECT FETCH STAYS PINNED, EVEN WHEN ITS ORIGIN IS THE PROXY'S OWN
|
|
// ADDRESS.
|
|
//
|
|
// This is the NO_PROXY shape: the proxy function sends one host direct. An
|
|
// exemption derived from asking that function about a request of our own
|
|
// invention would answer "proxied" for the excluded host too, and this dial
|
|
// would skip the pin and reach whatever the name resolves to at connect time.
|
|
func TestWebFetchDirectRequestToTheProxyEndpointIsStillPinned(t *testing.T) {
|
|
proxyURL, err := url.Parse("http://rebind.example:80")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lookups := 0
|
|
transport := webFetchTransportWithProxy(t,
|
|
webFetchResolverFunc(func(_ context.Context, _ string, host string) ([]netip.Addr, error) {
|
|
lookups++
|
|
return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, nil
|
|
}),
|
|
func(request *http.Request) (*url.URL, error) {
|
|
if request.URL.Hostname() == "rebind.example" {
|
|
return nil, nil
|
|
}
|
|
return proxyURL, nil
|
|
})
|
|
client := &http.Client{Transport: transport, Timeout: 3 * time.Second}
|
|
|
|
_, err = client.Get("http://rebind.example/data")
|
|
|
|
if lookups == 0 {
|
|
t.Fatal("the direct dial skipped the address pin because its address matched the configured proxy endpoint")
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), "loopback hosts are blocked") {
|
|
t.Fatalf("err = %v, want the private answer to be refused", err)
|
|
}
|
|
}
|
|
|
|
// A proxy configured with a Unicode hostname is dialed by its IDNA-encoded
|
|
// name, and it is still the proxy.
|
|
func TestWebFetchUnicodeProxyHostnameIsStillTheProxy(t *testing.T) {
|
|
dialed := ""
|
|
transport := webFetchTransportWithProxy(t,
|
|
webFetchResolverFunc(func(context.Context, string, string) ([]netip.Addr, error) {
|
|
return nil, errors.New("resolver must not be called for a proxied fetch")
|
|
}),
|
|
func(*http.Request) (*url.URL, error) { return url.Parse("http://büro.example:3067") })
|
|
guarded := transport.DialContext
|
|
transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
dialed = address
|
|
return guarded(ctx, network, address)
|
|
}
|
|
client := &http.Client{Transport: transport, Timeout: 3 * time.Second}
|
|
|
|
_, err := client.Get("https://public.example/resource")
|
|
|
|
if dialed != "xn--bro-hoa.example:3067" {
|
|
t.Fatalf("SETUP INVALID: dialed %q, want the IDNA-encoded proxy endpoint", dialed)
|
|
}
|
|
if err != nil && strings.Contains(err.Error(), "hosts are blocked") {
|
|
t.Fatalf("the configured proxy was refused by the guard: %v", err)
|
|
}
|
|
}
|
|
|
|
// Without a proxy the guard is exactly what it was: loopback is refused and
|
|
// the dialer never runs.
|
|
func TestWebFetchSafeDialStillRefusesLoopbackWithoutAProxy(t *testing.T) {
|
|
dialCalled := false
|
|
dial := webFetchSafeDialContext(
|
|
webFetchResolverFunc(func(_ context.Context, network string, host string) ([]netip.Addr, error) {
|
|
t.Fatalf("resolver consulted for a loopback literal: network=%q host=%q", network, host)
|
|
return nil, nil
|
|
}),
|
|
webFetchDialFunc(func(context.Context, string, string) (net.Conn, error) {
|
|
dialCalled = true
|
|
return nil, errors.New("dial should not run")
|
|
}),
|
|
)
|
|
|
|
_, err := dial(context.Background(), "tcp", "127.0.0.1:3067")
|
|
if err == nil || !strings.Contains(err.Error(), "loopback hosts are blocked") {
|
|
t.Fatalf("expected loopback rejection, got %v", err)
|
|
}
|
|
if dialCalled {
|
|
t.Fatal("loopback was dialed with no proxy configured")
|
|
}
|
|
}
|