fapihttp

package
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package fapihttp provides the hardened HTTP transport used internally by client, server and resource: strict TLS verification, response-size limits, bounded (or disabled) redirects, endpoint origin validation, connection and body-read deadlines, SSRF restrictions for discovery and JWKS fetches, and content-type checks.

Callers configure a role via a narrow HTTPClient interface (a Do method, matching *http.Client); fapihttp wraps whatever is supplied with these protections rather than trusting the caller to have applied them. It has no role-specific behaviour of its own — every role differentiates its fetches (e.g. the server does not fetch client JWKS the way the client fetches AS discovery documents) through Client.Fetch's FetchRequest fields, such as ExpectedContentType, rather than through separate per-role types.

NewClient additionally builds an *http.Client with a hardened *http.Transport (SSRF-resistant dialing that validates a resolved address before connecting to it, TLS 1.2 minimum, no automatic redirect following) for callers who don't need to supply their own — see Client.Fetch for why a Fetch-level URL/origin check on its own cannot substitute for that, and why fapihttp owns bounded redirect handling itself instead of delegating it to the underlying HTTP client.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInsecureURL indicates a fetch target (the original URL or a
	// redirect destination) did not use https, and AllowLoopbackHTTP did
	// not except it.
	ErrInsecureURL = errors.New("fapihttp: url must use https")

	// ErrSSRFBlocked indicates every address a fetch target's host
	// resolved to was loopback, private, link-local, unspecified or
	// multicast, and AllowLoopbackHTTP did not except it.
	ErrSSRFBlocked = errors.New("fapihttp: target address is not allowed")

	// ErrTooManyRedirects indicates a fetch followed Config.MaxRedirects
	// redirect hops and the response was still a redirect.
	ErrTooManyRedirects = errors.New("fapihttp: too many redirects")

	// ErrRedirectOriginMismatch indicates a redirect's Location pointed
	// somewhere other than the original request's scheme and host.
	ErrRedirectOriginMismatch = errors.New("fapihttp: redirect target has a different origin than the request")

	// ErrResponseTooLarge indicates a response body exceeded
	// Config.MaxResponseBytes.
	ErrResponseTooLarge = errors.New("fapihttp: response body exceeds the configured size limit")

	// ErrUnexpectedContentType indicates a response's Content-Type header
	// did not match FetchRequest.ExpectedContentType.
	ErrUnexpectedContentType = errors.New("fapihttp: response has an unexpected content type")

	// ErrUnexpectedStatus indicates a non-redirect response's status code
	// was not 200.
	ErrUnexpectedStatus = errors.New("fapihttp: response has an unexpected status code")

	// ErrMissingTLS indicates an https request's response carried no TLS
	// connection state — a defense-in-depth check against a supplied
	// HTTPClient that silently downgraded or proxied the connection.
	ErrMissingTLS = errors.New("fapihttp: https response was not delivered over a verified TLS connection")
)

Functions

func NewClient

func NewClient(cfg TransportConfig) (*http.Client, error)

NewClient builds an *http.Client whose Transport resolves each host itself and validates every candidate address before dialing it — rejecting loopback (unless AllowLoopbackHTTP), private, link-local, unspecified and multicast addresses — and never follows a redirect on its own (CheckRedirect always returns http.ErrUseLastResponse), so Client.Fetch's own bounded, origin-checked redirect handling is the only place a redirect is ever followed.

Passing the result as New's HTTPClient argument is the recommended way to get SSRF protection that also holds under DNS rebinding: Fetch's own URL/origin validation happens before each round trip, but the name could resolve to a different, disallowed address by the time the underlying transport actually dials it. This transport closes that gap by resolving once, validating every candidate address, and dialing only an address it already validated — never re-resolving the hostname at connect time.

Types

type Client

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

Client performs hardened GET fetches for discovery documents, JWKS documents, and similar security-sensitive resources — the recommended building block for an embedder's own ClientKeySource or IssuerKeySource implementation that fetches JWKS live, and for client's own AS-discovery path. See ARCHITECTURE.md design rule 6. It is entirely unexported — construct one with New.

func New

func New(http HTTPClient, cfg Config) (*Client, error)

New validates cfg and returns a Client wrapping http.

func (*Client) Fetch

func (c *Client) Fetch(ctx context.Context, req FetchRequest) (FetchResponse, error)

Fetch performs req, following at most c.cfg.MaxRedirects same-origin redirects, and enforces every protection described in Config and FetchRequest along the way.

type Config

type Config struct {
	// MaxResponseBytes bounds how much of a response body Fetch reads
	// before failing — applied regardless of any Content-Length header,
	// which is untrusted until the body has actually been read.
	MaxResponseBytes int64

	// RequestTimeout bounds the total time a single Fetch call may take,
	// including every redirect hop it follows.
	RequestTimeout time.Duration

	// MaxRedirects bounds how many redirect hops Fetch will follow. Zero
	// means a redirect response is never followed — Fetch returns
	// ErrTooManyRedirects instead of the redirect body.
	MaxRedirects int

	// AllowLoopbackHTTP permits an http:// scheme when the host is a
	// loopback address, matching fapi.AllowLoopbackHTTP. It exists for
	// local development only.
	AllowLoopbackHTTP bool
}

Config bounds every fetch a Client performs. None of these have an implicit default — New rejects a zero (or, for MaxRedirects, negative) value.

type FetchRequest

type FetchRequest struct {
	// URL is the endpoint to fetch. The caller is responsible for having
	// already chosen it validly (e.g. a fapi.URL, or a JWKS URI resolved
	// from a discovery document under the same rules) — Fetch re-checks
	// scheme and host shape itself, on the initial request and on every
	// redirect hop, and additionally makes a best-effort check that the
	// host's resolved addresses aren't loopback/private/link-local/etc.
	// That best-effort check is pre-dial validation only: it does not by
	// itself defeat DNS rebinding, and it is not authoritative — full
	// IP-level SSRF protection, including under DNS rebinding, is
	// provided only by the transport NewClient builds (see HTTPClient
	// and NewClient's doc comments). Passing any other HTTPClient —
	// including http.DefaultClient — disables that protection and, if it
	// follows redirects itself, also bypasses Fetch's bounded
	// same-origin redirect handling.
	URL *url.URL

	// ExpectedContentType is the media type (ignoring parameters such as
	// charset, compared case-insensitively per RFC 7231 §3.1.1.1) the
	// response's Content-Type header must declare. Required, so a
	// caller can never accidentally accept an arbitrary content type
	// for a security-sensitive fetch.
	ExpectedContentType string

	// AlternateContentTypes are additional media types accepted the
	// same way as ExpectedContentType — for a resource whose own
	// registered media type (e.g. RFC 7517 §8.5.1's
	// "application/jwk-set+json" for a JWKS document) differs from
	// what most real deployments actually serve. Optional; most callers
	// leave this empty and rely on ExpectedContentType alone.
	AlternateContentTypes []string
}

FetchRequest describes one outbound GET fetch. Fetch exists for discovery-document and JWKS retrieval, both GET-only per the specs that define them — it is not a general-purpose HTTP client, and never hands a caller a way to build an arbitrary request.

type FetchResponse

type FetchResponse struct {
	Body       []byte
	StatusCode int
}

FetchResponse is a successfully fetched, size- and content-type- checked response body.

type HTTPClient

type HTTPClient interface {
	Do(*http.Request) (*http.Response, error)
}

HTTPClient is the narrow interface Client wraps — it matches *http.Client's Do method, so a caller can supply either an *http.Client (ideally one built by NewClient) or a purpose-built implementation, e.g. one that adds mTLS client certificates or routes through a corporate proxy.

Strongly prefer an *http.Client built by NewClient. Its transport resolves each host itself, validates every candidate address before dialing, and pins the dial to an address it already validated — the only defense here that holds under DNS rebinding (see NewClient's doc comment). Fetch's own IP check (see FetchRequest.URL) is best-effort pre-dial validation, not a substitute: passing any other HTTPClient, including http.DefaultClient, means an actual round trip is made with whatever address that client's own transport resolves at connect time, which Fetch cannot see or control.

type TransportConfig

type TransportConfig struct {
	// DialTimeout bounds how long a single TCP connection attempt may
	// take.
	DialTimeout time.Duration

	// TLSHandshakeTimeout bounds how long the TLS handshake may take.
	TLSHandshakeTimeout time.Duration

	// AllowLoopbackHTTP permits dialing a loopback address, for local
	// development only — see fapi.AllowLoopbackHTTP. It never exempts
	// any other private, link-local, unspecified or multicast address.
	AllowLoopbackHTTP bool
}

TransportConfig bounds the hardened transport NewClient builds. None of these have an implicit default — NewClient rejects a zero value.

Jump to

Keyboard shortcuts

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