httputil

package
v1.0.0-alpha.24 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2026 License: AGPL-3.0 Imports: 10 Imported by: 0

Documentation

Overview

Package httputil provides the hardened egress HTTP client for every fetch of a URL a platform user can inject (schema ingestion today; social login and tenant webhooks later). Endpoints only the operator configures (telemetry collectors, the audit export sink) stay on standard-library clients: the guard exists against server-side request forgery, which requires a user-controlled URL.

The package deliberately imports nothing repo-specific so it can be promoted to a shared library by copying (see the egress-policy ADR).

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrResponseTooLarge is returned when the response body exceeds the configured limit.
	ErrResponseTooLarge = errors.New("response body exceeded maximum allowed size")
	// ErrTooManyRedirects is returned when the number of redirects exceeds the configured limit.
	ErrTooManyRedirects = errors.New("stopped after too many redirects")
	// ErrHTTPSDowngrade is returned when a redirect attempts to downgrade from https to http.
	ErrHTTPSDowngrade = errors.New("redirect downgrade from https to http is not allowed")
	// ErrCrossOriginRedirectWithBody is returned when following a redirect
	// would replay the request body on another origin (307/308).
	ErrCrossOriginRedirectWithBody = errors.New("refusing to replay a request body across origins on redirect")
)
View Source
var DefaultDenyList = []string{
	"localhost",
	"0.0.0.0/8",
	"10.0.0.0/8",
	"100.64.0.0/10",
	"127.0.0.0/8",
	"169.254.0.0/16",
	"172.16.0.0/12",
	"192.168.0.0/16",
	"198.18.0.0/15",
	"::/128",
	"::1/128",
	"fc00::/7",
	"fe80::/10",
	"64:ff9b:1::/48",
	"2002::/16",
	"2001::/32",
}

DefaultDenyList blocks loopback, private, link-local (cloud metadata), carrier-grade NAT, benchmark, and unspecified ranges, IPv4 and IPv6, plus the "localhost" hostname and the deprecated IPv4-embedding transition prefixes (6to4, Teredo, local-use NAT64). A superset of zitadel/zitadel's HTTPClient.DenyList (GHSA-29jh-8cfq-rr8x). The well-known NAT64 prefix 64:ff9b::/96 is deliberately absent: on DNS64/NAT64 networks every public IPv4 destination appears inside it, so instead the checker evaluates the embedded IPv4 address against these rules (see expandEmbeddedIPv4). The hostname entry is belt-and-braces for the URL layer; the CIDRs are what block at dial time, where every name is already resolved — do not remove one because the other looks equivalent.

Functions

func Get

func Get(ctx context.Context, url string, client *http.Client, acceptContentType string) ([]byte, error)

Types

type AddressDeniedError

type AddressDeniedError struct {
	// contains filtered or unexported fields
}

AddressDeniedError reports that an egress target was blocked by the deny list, naming the entry that matched so the failure is attributable in logs and to the caller who chose the URL.

func NewAddressDeniedError

func NewAddressDeniedError(deniedBy string) *AddressDeniedError

func (*AddressDeniedError) Error

func (e *AddressDeniedError) Error() string

type ClientConfig

type ClientConfig struct {
	// MaxBodySize caps each response body in bytes; 0 means unlimited.
	MaxBodySize int64 `mapstructure:"max_body_size"`
	// Timeout bounds each single request; 0 means unlimited.
	Timeout time.Duration `mapstructure:"timeout"`
	// MaxRedirects caps redirect hops; 0 refuses all redirects.
	MaxRedirects int `mapstructure:"max_redirects"`
	// AllowHTTPSDowngrade permits an https request to redirect to http.
	AllowHTTPSDowngrade bool `mapstructure:"allow_https_downgrade"`
	// DenyList entries (CIDR, IP, or hostname) block matching targets.
	DenyList []string `mapstructure:"deny_list"`
	// AllowList entries re-allow targets the deny list blocks, e.g.
	// "localhost" plus "127.0.0.0/8" for local development.
	AllowList []string `mapstructure:"allow_list"`
}

ClientConfig builds a hardened egress client. The zero value enforces nothing; defaults are the config loader's job.

func (ClientConfig) NewClient

func (c ClientConfig) NewClient() (*http.Client, error)

NewClient returns an *http.Client protected against DNS rebinding (the dial-time policy check on the resolved IP), hostname-denied targets (a pre-connection check on every request, redirect hops included), redirect abuse (hop cap, downgrade block), and oversized responses.

func (ClientConfig) Validate

func (c ClientConfig) Validate() error

Validate parses both lists so a malformed entry fails at startup, not at first fetch, and rejects negative limits: a negative max_body_size would silently disable the response cap.

type ContentTypeError

type ContentTypeError struct {
	// contains filtered or unexported fields
}

func (*ContentTypeError) Error

func (e *ContentTypeError) Error() string

type HostChecker

type HostChecker struct {
	Net    *net.IPNet
	IP     net.IP
	Domain string
}

HostChecker matches one policy entry: a CIDR network, a single IP, or a hostname (exact, case-insensitive — no wildcards or subdomain matching).

func NewHostChecker

func NewHostChecker(entry string) (*HostChecker, error)

NewHostChecker classifies entry by parse: CIDR, then IP, then hostname. An empty entry returns nil. An entry containing "/" that is not a valid CIDR is an error rather than a hostname: hostnames cannot contain "/", and the silent alternative would be a deny rule that never matches.

func (*HostChecker) Matches

func (c *HostChecker) Matches(ips []net.IP, address string) (rule string, ok bool)

Matches reports whether the address string or one of the candidate IPs matches this entry, returning the matching rule for attribution.

type Policy

type Policy struct {
	// contains filtered or unexported fields
}

Policy is the egress decision for one client: a target is allowed when it matches the allow list, denied when it matches the deny list, and allowed otherwise. The allow list is an exception list carved out of the deny list, not a lockdown mode.

func NewPolicy

func NewPolicy(denyList, allowList []string) (*Policy, error)

NewPolicy parses both lists, failing fast on malformed entries so a typo'd CIDR cannot silently weaken the deny list.

func (*Policy) Check

func (p *Policy) Check(ips []net.IP, address string) error

Check applies allow-then-deny to the candidate IPs and address string. At dial time ips holds exactly the IP about to be connected, so an allow entry never lets a sibling DNS answer through: each dial is checked alone.

func (*Policy) CheckAddress

func (p *Policy) CheckAddress(hostname string) error

CheckAddress applies the policy to a request's URL hostname before any connection is opened: hostname entries match by name, and a literal-IP host also matches IP and CIDR entries. Domains are deliberately not resolved here — the dial-time check in the transport owns resolved addresses, so a DNS answer cannot bypass anything by being checked twice. Consequence: a hostname allow entry only counters a hostname deny entry; a denial by IP range needs an IP or CIDR allow entry.

func (*Policy) Enforces

func (p *Policy) Enforces() bool

Enforces reports whether the policy denies anything at all. With an empty deny list the allow list has nothing to override and every target passes.

type StatusError

type StatusError struct {
	StatusCode int
	Body       []byte
}

func (*StatusError) Error

func (e *StatusError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL