http

package
v0.0.0-...-efbc44a Latest Latest
Warning

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

Go to latest
Published: Aug 2, 2026 License: GPL-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package http parses HTTP responses for the HTTP_GET and SSL_GET health checks.

This replaces lib/html.c: pure (pointer, length) parsing over network-supplied bytes, and the highest-value fuzz target in the tree.

Go's slices remove the memory-safety concerns of that shape for free. What still needs deciding is behaviour at the edges, where C's bounds handling leaves the result undefined and there is therefore nothing to be faithful to. This implementation treats the buffer as ending where it ends.

Index

Examples

Constants

View Source
const ContentLengthUnknown = -1

ContentLengthUnknown is returned when no usable Content-Length is present. C signals this with SIZE_MAX.

View Source
const DefaultMaxBody = 1 << 20 // 1 MiB

Streaming regex matching over a response body.

Why this is not a translation of the C

keepalived matches incrementally using PCRE2's partial-match mode (check_http.c:1176-1290). On PCRE2_ERROR_PARTIAL it keeps the tail that could still complete the match and discards the rest:

keep = ovector[0] > max_lookbehind ? ovector[0] - max_lookbehind : 0;
if (keep) {
        req->len -= keep;
        memmove(req->buffer, req->buffer + keep, req->len);
}

That lets it match a pattern anywhere in a response of unbounded length while holding a fixed buffer — as long as each partial match's start stays within the pattern's maximum lookbehind of what is retained.

Go's regexp has no partial-match mode. There is no way to ask it "could this still match if more input arrived", so the retention rule above has no equivalent to port. Rather than approximate it — a fixed overlap would give a matcher that silently misses matches longer than the overlap, which is worse than one with an honest bound — this accumulates up to a stated limit and matches over what it has.

The observable difference, stated plainly so it is not discovered later:

  • C can match a pattern occurring at any offset in an arbitrarily large response.
  • This matches within MaxBody bytes, and reports ErrBodyTooLarge rather than returning "no match" when the body outgrows that.

The second is the important half. C's own overflow path — the one where the partial match starts inside the lookbehind and the buffer is full — logs "Regex partial match preserve too large - discarding" and returns *no match*, which is indistinguishable from a healthy response that genuinely lacks the pattern. Reporting the condition instead is the one behaviour here worth not reproducing.

View Source
const MaxResponse = 4 << 20 // 4 MiB

MaxResponse bounds what is read from a backend.

The response body is attacker-controlled from the checker's point of view: a compromised or merely broken backend can send an unbounded stream, and a checker that read it all would be turned into a memory exhaustion by the thing it is checking.

Variables

View Source
var ErrBodyTooLarge = errors.New("http: response body exceeded the regex buffer")

ErrBodyTooLarge is returned when a body outgrows the matcher's bound before the pattern matched.

It is deliberately not "no match": the check could not be completed, which is a different fact from the pattern being absent.

View Source
var ErrTruncated = errors.New("http: response exceeded the read limit")

ErrTruncated reports a response that outgrew MaxResponse.

It is distinct from "the check failed", because it is not a statement about the backend's health — it says the check could not be completed. Conflating them is the same mistake the regex bound avoids; see regex.go.

Functions

func Body

func Body(b []byte) int

Body returns the offset of the response body: the byte after the CRLFCRLF that ends the headers, or -1 if the terminator is not present.

func ContentLength

func ContentLength(b []byte) int

ContentLength extracts the Content-Length header value.

It returns ContentLengthUnknown when the header is absent, or when the number is followed by anything other than end-of-buffer, CR or LF — C rejects a trailing garbage character rather than accepting a prefix, and a header like "Content-Length: 12x" is a malformed response, not a 12-byte body.

func Get

func Get(ctx context.Context, conn net.Conn, req Request) (bool, error)

Get performs one request and applies the configured expectations.

The connection is not reused and is left open for the caller to close, so a caller that wrapped it in TLS closes the right thing.

func StatusCode

func StatusCode(b []byte) int

StatusCode extracts the three-digit status code from a response line.

The shape required is strict, and deliberately so: RFC 2616 §6.1 puts the code between the version and the reason phrase, separated by single spaces. C requires exactly three digits with a space on each side, and anything else yields 0 — which the caller treats as "no status", not as status zero.

Example
fmt.Println(StatusCode([]byte("HTTP/1.1 404 Not Found\r\n")))
Output:
404

Types

type Matcher

type Matcher struct {

	// MaxBody bounds what is retained. Zero means DefaultMaxBody.
	MaxBody int
	// MinOffset and MaxOffset restrict where a match counts, mirroring
	// keepalived's regex_min_offset and regex_max_offset. Zero means
	// unrestricted.
	MinOffset int
	MaxOffset int
	// Invert makes a match a failure, for regex_no_match.
	Invert bool
	// contains filtered or unexported fields
}

Matcher accumulates a response body and matches a pattern over it.

func NewMatcher

func NewMatcher(pattern string) (*Matcher, error)

NewMatcher compiles a pattern.

func (*Matcher) Matched

func (m *Matcher) Matched() bool

Matched reports whether the pattern has been found.

It evaluates lazily rather than relying on Write having run, because a body can legitimately be empty and a pattern can legitimately match emptiness — "^" and "a*" both do. Found by FuzzMatcherChunking: the whole-body path called Write once with an empty slice while the chunked path never called it at all, so the verdict depended on how the caller happened to loop rather than on the body.

func (*Matcher) Reset

func (m *Matcher) Reset()

Reset clears the accumulated body for the next check.

func (*Matcher) Result

func (m *Matcher) Result() (healthy bool, err error)

Result reports the health verdict.

It returns ErrBodyTooLarge when the body outgrew the bound without matching, because "we could not tell" is not the same as "the pattern was absent" — the distinction C loses.

func (*Matcher) Write

func (m *Matcher) Write(p []byte) (int, error)

Write appends a chunk of body.

Once the pattern has matched, further input is discarded: C stops checking too ("If we have already matched the regex, there is no point in checking any further"), and continuing would only cost memory.

type Request

type Request struct {
	// Path is the URL path. Empty means "/".
	Path string
	// Virtualhost is the Host header. Empty sends the connected address,
	// which is what a backend with a default vhost expects.
	Virtualhost string
	// StatusCodes are the acceptable statuses. Empty accepts any 2xx, which
	// is C's behaviour when neither a status nor a digest is configured.
	StatusCodes []int
	// Digest is the expected MD5 of the body, as 32 hex characters.
	//
	// MD5 is not a security property here and is not treated as one: it
	// detects a page that changed, not a page that was tampered with. C uses
	// it and the digests are in deployed configurations, so it is
	// reproduced.
	Digest string
	// Regex is matched against the body.
	Regex string
	// RegexNoMatch inverts the match, for `regex_no_match`.
	RegexNoMatch bool
	// RegexMinOffset and RegexMaxOffset restrict where in the body a match
	// counts. Zero is unrestricted.
	//
	// They exist for pages that legitimately contain the pattern in a place
	// that does not mean what the check means — a navigation link naming an
	// error page, say — where matching anywhere would report healthy for
	// the wrong reason.
	RegexMinOffset int
	RegexMaxOffset int
	// RegexStack bounds what the matcher retains. Zero means
	// DefaultMaxBody.
	RegexStack int

	// Logf reports what C reports and does not fail the check on: a
	// Content-Length that disagrees with the bytes that arrived. Optional.
	Logf func(format string, args ...any)
}

Request is one configured check.

type StatusMatcher

type StatusMatcher struct {
	Codes []int
}

StatusMatcher decides whether a status code is acceptable.

keepalived allows several status_code entries per url, so this is a set rather than a single value. An empty set accepts any code, which is C's behaviour when no status_code is configured — the check then only verifies that a response arrived at all.

func (StatusMatcher) Accepts

func (m StatusMatcher) Accepts(code int) bool

Accepts reports whether a status code satisfies the matcher.

Jump to

Keyboard shortcuts

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