Documentation
¶
Overview ¶
Package httpx is the server's HTTP plumbing: the hardened http.Server, the middleware every route wears, and the decoders that read a request body.
It exists because the settings in Harden are the ones that get forgotten. A timeout that is not set is a slowloris; a body that is not capped is a memory limit; a JSON decoder that ignores unknown fields turns a client's typo into a silent default. This package is every one of those decided once and applied centrally, so that no route can be created without them.
Routing is net/http.ServeMux and nothing else. Go 1.22's method and wildcard patterns cover the whole API, and a framework on the request path would be one more dependency between a driver's lap and the database.
Index ¶
- Constants
- Variables
- func APIError(w http.ResponseWriter, code wire.Code, message string)
- func BearerToken(r *http.Request) (string, bool)
- func Chain(h http.Handler, ms ...Middleware) http.Handler
- func ClientHost(r *http.Request) string
- func DecodeJSON(w http.ResponseWriter, r *http.Request, dst any, limit int64) error
- func DecodeJSONFingerprint(w http.ResponseWriter, r *http.Request, dst any, limit int64) ([]byte, error)
- func Harden(s *http.Server) *http.Server
- func JSON(w http.ResponseWriter, status int, body any)
- func Limit(w http.ResponseWriter, r *http.Request, limit int64)
- func NewServer(addr string, h http.Handler) *http.Server
- func NoStore(next http.Handler) http.Handler
- func ParseBearer(header string) (string, bool)
- func ParseForm(w http.ResponseWriter, r *http.Request, limit int64) error
- func ParseUpload(w http.ResponseWriter, r *http.Request, limit int64) error
- func Problem(w http.ResponseWriter, r *http.Request, status int, message string)
- func SecureHeaders(next http.Handler) http.Handler
- func Serve(ctx context.Context, srv *http.Server, ln net.Listener) error
- type Limiter
- type Middleware
Constants ¶
const ( // ReadHeaderTimeout is the slowloris guard: a client that opens a // connection and dribbles a header has five seconds to finish. ReadHeaderTimeout = 5 * time.Second // ReadTimeout and WriteTimeout bound a whole request and a whole response. ReadTimeout = 30 * time.Second WriteTimeout = 30 * time.Second // IdleTimeout is how long a kept-alive connection may sit unused. IdleTimeout = 120 * time.Second // MaxHeaderBytes caps the header block. MaxHeaderBytes = 16 << 10 // ShutdownTimeout is how long a graceful shutdown waits for requests in // flight before the process stops anyway. ShutdownTimeout = 20 * time.Second )
The D-3 hardening table, as constants. Changing one of these is a decision, so it is made here rather than at a call site.
const FormLimit int64 = 64 << 10
FormLimit is the body cap on an HTML form post. The wizard and the admin panel send a handful of short fields; sixty-four kilobytes is generous for that and nowhere near enough to be a memory problem.
Variables ¶
var ErrMalformed = errors.New("httpx: the request body is not readable")
ErrMalformed is returned when a body is not what it claimed to be.
var ErrTooLarge = errors.New("httpx: the request body is too large")
ErrTooLarge is returned when a body is longer than the route's cap.
Functions ¶
func APIError ¶
func APIError(w http.ResponseWriter, code wire.Code, message string)
APIError writes the v1 error envelope for one code and message, with the status the contract pairs with that code. message is shown to the driver as written, so it must be a complete sentence with no jargon.
func BearerToken ¶ added in v0.2.0
BearerToken pulls the token out of a request's Authorization header, or reports that there is no Bearer credential — a Basic one, or none at all.
func Chain ¶
func Chain(h http.Handler, ms ...Middleware) http.Handler
Chain wraps h in every middleware, outermost first, so Chain(h, a, b) reads in the order the request travels: a, then b, then h.
func ClientHost ¶
ClientHost is [clientHost] for the packages that rate-limit by address.
It deliberately does not read X-Forwarded-For. Behind the operator's own proxy that header is trustworthy and in front of one it is whatever the caller typed, and this package cannot tell which it is in. Rate limiting the proxy's own address is wrong but safe; trusting a forged header is neither.
func DecodeJSON ¶
DecodeJSON reads one JSON document from the body into dst.
Three things are true of it and none of them are the standard library's defaults: the body is capped before a byte is allocated, an unknown field is an error rather than a silent default, and trailing content after the document is an error rather than being ignored. The first is a memory limit, and the other two turn a client's typo into a 422 it can see in a log rather than into a value that quietly went missing.
func DecodeJSONFingerprint ¶
func DecodeJSONFingerprint(w http.ResponseWriter, r *http.Request, dst any, limit int64) ([]byte, error)
DecodeJSONFingerprint reads one JSON document into dst and returns a fingerprint of the request that carried it, for the idempotency table.
The fingerprint covers the method, the path and the body bytes, so the same key presented on two different endpoints is a conflict rather than a replay of the wrong answer. It is computed as the body streams past the decoder, so no second copy of the payload is made and the cap is still enforced before anything is allocated.
func Harden ¶
Harden applies the D-3 table to a server. Every http.Server this binary creates goes through it.
func JSON ¶
func JSON(w http.ResponseWriter, status int, body any)
JSON writes one JSON document with a status. It is the only place a success body is encoded, so the header and the body cannot disagree about what is being sent.
func Limit ¶
func Limit(w http.ResponseWriter, r *http.Request, limit int64)
Limit caps the body of one route, per D-3: a cap belongs to an endpoint, not to the server, because the endpoint is the only place that knows what a reasonable body looks like.
func NoStore ¶
NoStore stops a browser and any proxy from keeping a page. Every page the admin panel serves is either a form with a token in it or a view of private data, so none of them may be cached.
func ParseBearer ¶ added in v0.2.0
ParseBearer is BearerToken on the header's value. The scheme is matched case-insensitively because RFC 7235 says it is case-insensitive, and a client that sends "bearer" is not wrong.
func ParseForm ¶
ParseForm reads an HTML form body, capped at max. It is the form twin of DecodeJSON and exists so no handler calls r.ParseForm without a cap.
func ParseUpload ¶
ParseUpload reads a form that carries a file.
It is separate from ParseForm because the two need different caps. An ordinary form is a few text fields and is held to FormLimit; a form with a file on it is as large as the file, and the caller says how large that may be. The whole body is still capped, so an upload that claims to be small and keeps arriving is cut off rather than kept.
Nothing is spooled to disk: the in-memory allowance is the same limit, so a file inside it never reaches the file system and a file outside it is refused before any of it does.
which is the bound the rule is asking for; the analyser cannot see through the helper.
func Problem ¶
Problem answers a request that cannot be served. A caller that wants JSON gets the v1 error envelope; a browser gets one line of plain text, because the alternative is an error page that says less.
func SecureHeaders ¶
SecureHeaders sets the response headers that cost nothing and close off a whole class of problem. The content security policy is strict because the admin panel is server-rendered with no framework and no content delivery network: there is no inline script to allow and nothing to fetch from elsewhere.
Types ¶
type Limiter ¶
type Limiter struct {
// contains filtered or unexported fields
}
Limiter is a token bucket per key, plus a ceiling over all keys.
It is built for the setup token endpoint, which needs it first: an unauthenticated form that compares a secret has to cost an attacker something. The same type carries the per-device limits, which is why the limits are values rather than constants.
func NewLimiter ¶
NewLimiter builds a limiter allowing burst attempts immediately and then one every 1/rate seconds, per key, with globalBurst and globalRate the same over every key at once.
type Middleware ¶
Middleware is the ordinary shape. Nothing in this server needs another.
func LogRequests ¶
func LogRequests(log *slog.Logger) Middleware
LogRequests writes one line per request. The path is logged and the query string is not, because a query string is where a token ends up when someone takes a shortcut.
func Measure ¶
Measure reports how one route finished, for the metrics port.
The route is given rather than taken from the request, because it is the ServeMux pattern and not the path: a label per driver would be a metric per driver, and a time series that grows with the roster is one nobody can query. It follows that Measure is applied where a route is registered, which is the only place the pattern is known.
A nil report is no middleware at all, so a server with no metrics port pays nothing for the ones it is not collecting.
func Recover ¶
func Recover(log *slog.Logger) Middleware
Recover turns a panic in a handler into a 500 and a log line, rather than into a dead process. A panic mid-session would otherwise lose every driver's laps that were in flight, which makes this correctness rather than polish.