feat: complete remaining Go backend modules

This commit is contained in:
zn-admin committed 2026-08-14 00:02:43 +08:00
1 parent cea2751dc5
commit aef5a97165
145 files changed
+18376 -199

No files matched your search

+139
View File
@@ -0,0 +1,139 @@
package webhook
import (
"context"
"errors"
"net"
"net/netip"
"net/url"
"strings"
)
// IPResolver is the DNS seam used by PublicDestinationPolicy.
type IPResolver interface {
LookupIPAddr(context.Context, string) ([]net.IPAddr, error)
}
// ContextDialer is the network seam used after DNS answers have been checked.
type ContextDialer interface {
DialContext(context.Context, string, string) (net.Conn, error)
}
// DestinationPolicy validates an outbound HTTP destination.
type DestinationPolicy interface {
Validate(context.Context, *url.URL) error
}
// PublicDestinationValidator allows only HTTP(S) destinations whose complete DNS
// answer set contains public unicast addresses. DialContext resolves and dials
// one of those checked addresses directly, avoiding a second system DNS lookup.
type PublicDestinationValidator struct {
resolver IPResolver
dialer ContextDialer
}
func NewPublicDestinationPolicy(resolver IPResolver, dialer ContextDialer) *PublicDestinationValidator {
if resolver == nil {
resolver = net.DefaultResolver
}
if dialer == nil {
dialer = &net.Dialer{}
}
return &PublicDestinationValidator{resolver: resolver, dialer: dialer}
}
// PublicDestinationPolicy is the compatibility validator factory used by
// callers that provide their own bounded http.Client.
func PublicDestinationPolicy(resolver IPResolver) func(context.Context, *url.URL) error {
return NewPublicDestinationPolicy(resolver, nil).Validate
}
func (policy *PublicDestinationValidator) Validate(ctx context.Context, target *url.URL) error {
_, err := policy.resolve(ctx, target)
return err
}
func (policy *PublicDestinationValidator) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, errors.New("invalid outbound address")
}
target := &url.URL{Scheme: "http", Host: net.JoinHostPort(host, port)}
ips, err := policy.resolve(ctx, target)
if err != nil {
return nil, err
}
return policy.dialer.DialContext(ctx, network, net.JoinHostPort(ips[0].String(), port))
}
func (policy *PublicDestinationValidator) resolve(ctx context.Context, target *url.URL) ([]net.IP, error) {
if target == nil || (target.Scheme != "http" && target.Scheme != "https") || target.Host == "" || target.User != nil {
return nil, errors.New("outbound destination is invalid")
}
host := strings.TrimSuffix(strings.ToLower(target.Hostname()), ".")
if host == "" || host == "localhost" || strings.HasSuffix(host, ".localhost") {
return nil, errors.New("outbound destination is not public")
}
var ips []net.IP
if literal := net.ParseIP(host); literal != nil {
ips = []net.IP{literal}
} else {
answers, err := policy.resolver.LookupIPAddr(ctx, host)
if err != nil || len(answers) == 0 {
return nil, errors.New("outbound destination DNS lookup failed")
}
for _, answer := range answers {
ips = append(ips, answer.IP)
}
}
for _, ip := range ips {
if !isPublicUnicastIP(ip) {
return nil, errors.New("outbound destination is not public")
}
}
return ips, nil
}
func isPublicUnicastIP(ip net.IP) bool {
if ip == nil || !ip.IsGlobalUnicast() || ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsMulticast() || ip.IsUnspecified() {
return false
}
address, ok := netip.AddrFromSlice(ip)
if !ok {
return false
}
address = address.Unmap()
for _, prefix := range nonPublicSpecialUsePrefixes {
if prefix.Contains(address) {
return false
}
}
return true
}
// IsGlobalUnicast deliberately includes several IANA special-purpose ranges.
// These networks are not valid public callback destinations even though the Go
// standard library does not classify all of them as private or link-local.
var nonPublicSpecialUsePrefixes = []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/8"),
netip.MustParsePrefix("100.64.0.0/10"),
netip.MustParsePrefix("192.0.0.0/29"),
netip.MustParsePrefix("192.0.0.170/31"),
netip.MustParsePrefix("192.0.2.0/24"),
netip.MustParsePrefix("192.88.99.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:2::/48"),
netip.MustParsePrefix("2001:10::/28"),
netip.MustParsePrefix("2001:20::/28"),
netip.MustParsePrefix("2001:db8::/32"),
netip.MustParsePrefix("2002::/16"),
netip.MustParsePrefix("3fff::/20"),
netip.MustParsePrefix("5f00::/16"),
}
+82
View File
@@ -0,0 +1,82 @@
// Package webhook owns the generation callback wire and transport policy.
package webhook
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"net/url"
"time"
)
// NewPublicHTTPSender constructs the production sender with a transport that
// resolves, validates, and then directly dials the same public IP answer.
func NewPublicHTTPSender(timeout time.Duration, policy *PublicDestinationValidator) (*HTTPSender, error) {
if policy == nil || timeout <= 0 {
return nil, errors.New("webhook HTTP sender requires a bounded client and destination policy")
}
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.Proxy = nil
transport.DialContext = policy.DialContext
return NewHTTPSender(&http.Client{Timeout: timeout, Transport: transport}, policy.Validate)
}
// HTTPSender is a bounded callback transport. Destination policy remains
// injectable at composition time so deployments can enforce DNS/IP controls
// without coupling the byte/HMAC contract to one network environment.
type HTTPSender struct {
client *http.Client
validate func(context.Context, *url.URL) error
}
func NewHTTPSender(client *http.Client, validate func(context.Context, *url.URL) error) (*HTTPSender, error) {
if client == nil || client.Timeout <= 0 || validate == nil {
return nil, errors.New("webhook HTTP sender requires a bounded client and destination policy")
}
copy := *client
previous := copy.CheckRedirect
copy.CheckRedirect = func(request *http.Request, via []*http.Request) error {
if len(via) >= 5 {
return errors.New("webhook redirect limit exceeded")
}
if err := validate(request.Context(), request.URL); err != nil {
return err
}
if previous != nil {
return previous(request, via)
}
return nil
}
return &HTTPSender{client: &copy, validate: validate}, nil
}
func (sender *HTTPSender) Send(ctx context.Context, input Request) (Response, error) {
target, err := url.Parse(input.URL)
if err != nil || (target.Scheme != "http" && target.Scheme != "https") || target.Host == "" || target.User != nil {
return Response{}, errors.New("webhook destination is invalid")
}
if err := sender.validate(ctx, target); err != nil {
return Response{}, errors.New("webhook destination is not allowed")
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(input.Body))
if err != nil {
return Response{}, errors.New("build webhook request")
}
for key, value := range input.Headers {
request.Header.Set(key, value)
}
response, err := sender.client.Do(request)
if err != nil {
if ctx.Err() != nil {
return Response{}, ctx.Err()
}
return Response{}, errors.New("send webhook request")
}
defer response.Body.Close()
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 64<<10))
return Response{Status: response.StatusCode}, nil
}
var _ Sender = (*HTTPSender)(nil)
@@ -0,0 +1,197 @@
package webhook
import (
"context"
"errors"
"io"
"net"
"net/http"
"net/url"
"strings"
"testing"
"time"
)
type lookupIPFunc func(context.Context, string) ([]net.IPAddr, error)
func (function lookupIPFunc) LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error) {
return function(ctx, host)
}
type contextDialFunc func(context.Context, string, string) (net.Conn, error)
func (function contextDialFunc) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
return function(ctx, network, address)
}
type roundTripFunc func(*http.Request) (*http.Response, error)
func (function roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
return function(request)
}
func TestHTTPSenderPreservesSignedRequestAndReturnsStatus(t *testing.T) {
client := &http.Client{Timeout: time.Second, Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
body, _ := io.ReadAll(request.Body)
if request.Method != http.MethodPost || request.URL.String() != "https://hooks.example.test/job" || string(body) != `{"jobId":"job"}` || request.Header.Get("X-Zhinian-Signature") != "sha256=abc" {
t.Fatalf("request=%s %s body=%s headers=%v", request.Method, request.URL, body, request.Header)
}
return &http.Response{StatusCode: 204, Body: io.NopCloser(strings.NewReader("ignored")), Header: make(http.Header)}, nil
})}
sender, err := NewHTTPSender(client, func(context.Context, *url.URL) error { return nil })
if err != nil {
t.Fatal(err)
}
response, err := sender.Send(context.Background(), Request{URL: "https://hooks.example.test/job", Body: []byte(`{"jobId":"job"}`), Headers: map[string]string{"X-Zhinian-Signature": "sha256=abc"}})
if err != nil || response.Status != 204 {
t.Fatalf("Send=%#v,%v", response, err)
}
}
func TestHTTPSenderRejectsInvalidAndPolicyDeniedDestinations(t *testing.T) {
client := &http.Client{Timeout: time.Second, Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
t.Fatal("transport must not run")
return nil, nil
})}
sender, _ := NewHTTPSender(client, func(context.Context, *url.URL) error { return errors.New("private address") })
for _, target := range []string{"file:///etc/passwd", "https://user:secret@example.test", "https://127.0.0.1/hook"} {
if _, err := sender.Send(context.Background(), Request{URL: target}); err == nil || strings.Contains(err.Error(), "secret") {
t.Fatalf("target=%q error=%v", target, err)
}
}
}
func TestHTTPSenderRevalidatesEveryRedirect(t *testing.T) {
validated := make([]string, 0, 2)
client := &http.Client{Timeout: time.Second, Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
if request.URL.Hostname() != "public.test" {
t.Fatal("redirect target transport must not run")
}
return &http.Response{StatusCode: http.StatusTemporaryRedirect, Header: http.Header{"Location": []string{"https://private.test/hook"}}, Body: io.NopCloser(strings.NewReader("")), Request: request}, nil
})}
sender, err := NewHTTPSender(client, func(_ context.Context, target *url.URL) error {
validated = append(validated, target.Hostname())
if target.Hostname() == "private.test" {
return errors.New("private destination")
}
return nil
})
if err != nil {
t.Fatal(err)
}
if _, err := sender.Send(context.Background(), Request{URL: "https://public.test/hook"}); err == nil {
t.Fatal("policy-denied redirect was accepted")
}
if strings.Join(validated, ",") != "public.test,private.test" {
t.Fatalf("validated destinations = %v", validated)
}
}
func TestNewHTTPSenderRequiresBoundedClientAndPolicy(t *testing.T) {
if _, err := NewHTTPSender(nil, func(context.Context, *url.URL) error { return nil }); err == nil {
t.Fatal("nil client accepted")
}
if _, err := NewHTTPSender(&http.Client{}, func(context.Context, *url.URL) error { return nil }); err == nil {
t.Fatal("unbounded client accepted")
}
if _, err := NewHTTPSender(&http.Client{Timeout: time.Second}, nil); err == nil {
t.Fatal("nil policy accepted")
}
}
func TestPublicDestinationPolicyRejectsNonPublicDestinations(t *testing.T) {
tests := []struct {
name string
url string
ips []net.IPAddr
}{
{name: "userinfo", url: "https://user:secret@example.test/hook", ips: []net.IPAddr{{IP: net.ParseIP("93.184.216.34")}}},
{name: "localhost name", url: "https://localhost/hook", ips: []net.IPAddr{{IP: net.ParseIP("93.184.216.34")}}},
{name: "loopback", url: "https://example.test/hook", ips: []net.IPAddr{{IP: net.ParseIP("127.0.0.1")}}},
{name: "private", url: "https://example.test/hook", ips: []net.IPAddr{{IP: net.ParseIP("10.0.0.1")}}},
{name: "link local", url: "https://example.test/hook", ips: []net.IPAddr{{IP: net.ParseIP("169.254.169.254")}}},
{name: "multicast", url: "https://example.test/hook", ips: []net.IPAddr{{IP: net.ParseIP("224.0.0.1")}}},
{name: "unspecified", url: "https://example.test/hook", ips: []net.IPAddr{{IP: net.ParseIP("0.0.0.0")}}},
{name: "mixed answers", url: "https://example.test/hook", ips: []net.IPAddr{{IP: net.ParseIP("93.184.216.34")}, {IP: net.ParseIP("192.168.1.1")}}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
policy := NewPublicDestinationPolicy(lookupIPFunc(func(context.Context, string) ([]net.IPAddr, error) { return test.ips, nil }), nil)
u, _ := url.Parse(test.url)
if err := policy.Validate(context.Background(), u); err == nil || strings.Contains(err.Error(), "secret") {
t.Fatalf("Validate(%q) = %v", test.url, err)
}
})
}
}
func TestPublicDestinationPolicyRejectsSpecialUseUnicastAddresses(t *testing.T) {
addresses := []string{
"100.64.0.1", // shared address space (CGNAT)
"192.0.0.1", // IETF protocol assignments
"192.0.2.1", // documentation
"198.18.0.1", // benchmarking
"198.51.100.1", // documentation
"203.0.113.1", // documentation
"240.0.0.1", // reserved
"64:ff9b::a00:1", // IPv4/IPv6 translation can embed private IPv4
"100::1", // discard-only
"2001::1", // Teredo can embed non-public IPv4
"2001:2::1", // benchmarking
"2001:db8::1", // documentation
"2001:10::1", // deprecated ORCHID
"2001:20::1", // ORCHIDv2
}
for _, address := range addresses {
t.Run(address, func(t *testing.T) {
policy := NewPublicDestinationPolicy(nil, nil)
target, err := url.Parse("https://[" + address + "]/hook")
if net.ParseIP(address).To4() != nil {
target, err = url.Parse("https://" + address + "/hook")
}
if err != nil {
t.Fatal(err)
}
if err := policy.Validate(context.Background(), target); err == nil {
t.Fatalf("special-use address %s was accepted", address)
}
})
}
}
func TestPublicDestinationPolicyFailsClosedOnDNSFailure(t *testing.T) {
policy := NewPublicDestinationPolicy(lookupIPFunc(func(context.Context, string) ([]net.IPAddr, error) {
return nil, errors.New("resolver unavailable")
}), nil)
u, _ := url.Parse("https://hooks.example.test/job")
if err := policy.Validate(context.Background(), u); err == nil {
t.Fatal("DNS failure was accepted")
}
}
func TestPublicDestinationPolicyAllowsPublicAddress(t *testing.T) {
policy := NewPublicDestinationPolicy(lookupIPFunc(func(context.Context, string) ([]net.IPAddr, error) {
return []net.IPAddr{{IP: net.ParseIP("93.184.216.34")}}, nil
}), nil)
u, _ := url.Parse("https://hooks.example.test/job")
if err := policy.Validate(context.Background(), u); err != nil {
t.Fatalf("public destination rejected: %v", err)
}
}
func TestPublicDestinationPolicyDialsValidatedIPAddress(t *testing.T) {
var dialed string
policy := NewPublicDestinationPolicy(
lookupIPFunc(func(context.Context, string) ([]net.IPAddr, error) {
return []net.IPAddr{{IP: net.ParseIP("93.184.216.34")}}, nil
}),
contextDialFunc(func(_ context.Context, _, address string) (net.Conn, error) {
dialed = address
return nil, errors.New("dial stopped by test")
}),
)
_, _ = policy.DialContext(context.Background(), "tcp", "hooks.example.test:443")
if dialed != "93.184.216.34:443" {
t.Fatalf("dialed address = %q", dialed)
}
}