feat: complete remaining Go backend modules
This commit is contained in:
1 parent
cea2751dc5
commit
aef5a97165
145 files changed
+18376
-199
No files matched your search
@@ -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"),
|
||||
}
|
||||
Reference in new issue
Block a user