Documentation
¶
Overview ¶
Package httpx owns the loopback-only HTTP server core and the DNS-rebind guard shared by magus's daemon-facing HTTP surfaces. The server binds 127.0.0.1 exclusively - serving to a network interface is never allowed, so the bind host is not configurable; only the port is taken from the caller's address.
Index ¶
- func BearerGuard(verify verifier, next http.Handler) http.Handler
- func BearerGuardWithQueryToken(verify verifier, next http.Handler) http.Handler
- func CORS(origin string) func(http.Handler) http.Handler
- func CORSAllow(origins ...string) func(http.Handler) http.Handler
- func GuardRebind(allowed AllowedSet, next http.Handler) http.Handler
- func Gzip(next http.Handler) http.Handler
- func ParseOrigin(base string) (string, error)
- func RequireLoopbackPeer(next http.Handler) http.Handler
- func SingleTokenVerifier(expected func() (string, error)) verifier
- func WithRecorder(ctx context.Context, rec *Recorder) context.Context
- type AllowedSet
- type BlobServer
- type Recorder
- type ServeOutcome
- type Server
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BearerGuard ¶
BearerGuard rejects any request whose token fails verify. The token is read ONLY from the `Authorization: Bearer <token>` header. This is the default and the right choice for every endpoint a non-browser client reaches (the MCP endpoint, plain fetch() clients): a bearer token must not travel in the URL, where it leaks into access logs, proxy logs, and browser history (RFC 6750 section 2.3). For the browser-EventSource endpoints that genuinely cannot set a header, use BearerGuardWithQueryToken instead - an explicit opt-in, so a new mount is header-only unless it deliberately widens the carrier.
verify is called on every request, so a rotate, create, or revoke takes effect without restarting the server; it must fail closed (return false) on any error. Failures return 401 with a WWW-Authenticate challenge and a generic body that does not distinguish a missing token from a wrong one.
func BearerGuardWithQueryToken ¶
BearerGuardWithQueryToken is BearerGuard that ALSO accepts the token from a `?token=<token>` query parameter, preferring the header when both are present. Use it ONLY for endpoints a browser EventSource connects to: EventSource cannot set an Authorization header, so the query carrier is the sole option. It is a deliberate, scoped exception to the header-only rule (RFC 6750 section 2.3) - keep it off the MCP endpoint, which every supported client reaches with a header.
func CORS ¶
CORS locks every wrapped response to a single site origin and answers the OPTIONS preflight here, so a route handler never touches CORS itself. The preflight advertises the GET, OPTIONS methods the loopback tool-page routes use and allows the Authorization header, since the live viewer's fetch-based SSE client sends the per-run bearer token as a non-simple Authorization header that requires a preflight allowance.
func CORSAllow ¶
CORSAllow reflects the request Origin only when it is one of the given allow-list origins, answering the OPTIONS preflight here so a route handler never touches CORS itself. It is the multi-origin sibling of CORS: the console allows the hosted explorer origin plus the two loopback origins derived from the server port, so an empty allow-list disables CORS entirely. Empty origins are ignored. On a matched preflight it advertises the GET, POST, OPTIONS methods, allows the Authorization and Content-Type headers plus the Connect protocol headers the browser Connect client sends (Connect-Protocol-Version, Connect-Timeout-Ms), and honors Chrome's Private Network Access probe (Access-Control-Request-Private-Network). POST and the Connect headers are needed by the dashboard's Connect MetricsService calls; the GET bridge routes ignore the widened list.
func GuardRebind ¶
func GuardRebind(allowed AllowedSet, next http.Handler) http.Handler
GuardRebind rejects requests that a browser could forge via DNS rebinding. It validates two headers on every request to /mcp:
- Host (always present): the parsed hostname must be in allowed.
- Origin (browser-only): if present, the parsed hostname must be in allowed. Absent Origin is allowed — non-browser MCP clients (a CLI, a desktop app, curl) do not send it.
Health routes (/livez, /readyz, /healthz) are mounted outside this middleware and are deliberately left unguarded.
func Gzip ¶
Gzip wraps next so a response BODY is gzip-compressed when the client advertises Accept-Encoding: gzip. It sets Content-Encoding and appends Accept-Encoding to Vary, and drops any Content-Length the inner handler set (the compressed length differs). Only 200 responses are compressed; a bodyless status (304/204) passes through untouched, so a conditional GET still returns an empty 304. An inner handler that already set an ETag keeps it - the tag is computed over the uncompressed body and is identical for both encodings.
func ParseOrigin ¶
ParseOrigin extracts the scheme://host[:port] origin from a page's base URL, for the loopback server's CORS Allow-Origin. An unparsable or non-absolute base is a user error worth surfacing rather than defaulting to a permissive value.
func RequireLoopbackPeer ¶
RequireLoopbackPeer wraps an HTTP handler (or a Connect handler mounted on one) so a request whose PEER (r.RemoteAddr) is not loopback is refused before it reaches the RPC. This checks the transport peer, not the Host header (that is GuardRebind's job). Applied once around the tool-page mux; the interceptor form for a pure Connect server checks the same via the peer address.
func SingleTokenVerifier ¶
SingleTokenVerifier returns a [verifier] that accepts exactly the one token yielded by expected. It compares the SHA-256 digests of the presented and expected tokens with subtle.ConstantTimeCompare: the digests are equal-length, so the comparison reveals neither the secret's bytes nor its length. (Hashing an attacker-controlled input is itself length-dependent, but that timing channel is independent of the secret.) A load error from expected fails closed. The ephemeral live and blob servers use this with their per-run token; the daemon uses a richer, per-surface verifier (auth.VerifyMCPBearer or auth.VerifyConsoleBearer) instead.
Types ¶
type AllowedSet ¶
type AllowedSet struct {
// contains filtered or unexported fields
}
AllowedSet is the accept-list used by GuardRebind. Loopback IPs (127.0.0.0/8, ::1) are always allowed via IsLoopback() so no static map is needed for them. extra holds an optional concrete non-loopback bind host that the operator has deliberately configured (zero = not set). Non-IP hostnames like "localhost" live in names.
func AllowedHosts ¶
func AllowedHosts(addr netip.AddrPort) AllowedSet
AllowedHosts builds the AllowedSet for GuardRebind from the server's already-parsed bind address. Loopback addresses are handled dynamically by isAllowedHost via IsLoopback(), so they need no explicit entry. When addr contains a concrete non-loopback, non-unspecified host it is stored in extra so operators who deliberately bind to a LAN IP can still reach /mcp.
func (AllowedSet) Allow ¶
func (a AllowedSet) Allow(host string) AllowedSet
Allow returns a copy of a that additionally accepts the given hostname, for a deliberately trusted cross-origin caller such as the hosted site the dashboard is served from. The receiver is not mutated (its names map is copied), so widening one route's accept-list never leaks into the shared set used by /mcp or the /api bridge. An empty or port-only host is ignored.
type BlobServer ¶
type BlobServer struct {
// contains filtered or unexported fields
}
BlobServer hands a single blob to a hosted page over a loopback Server, then STOPS - a one-shot handoff, not a standing service. It inherits the server's loopback bind and wraps its route in the same stack as every other loopback endpoint: RequireLoopbackPeer (defense in depth over the bind), CORS (locked to the single site origin), and BearerGuard with a per-run random token. `graph export --open --serve` uses it.
func StartBlob ¶
func StartBlob(origin, path, contentType string, raw []byte) (*BlobServer, error)
StartBlob starts a one-shot loopback server on an ephemeral 127.0.0.1 port that serves raw (as contentType) at path, CORS-locked to origin and gated by a per-run bearer token, in the background. The caller hands the browser BlobServer.SourceURL (which carries the token as a `?token=` query param), then calls BlobServer.WaitServed. path must begin with "/".
func (*BlobServer) Addr ¶
func (b *BlobServer) Addr() string
Addr is the loopback "127.0.0.1:PORT" the server bound.
func (*BlobServer) SourceURL ¶
func (b *BlobServer) SourceURL() string
SourceURL is the loopback URL the page fetches the blob from, with the per-run token in a `?token=` query param (http://127.0.0.1:PORT/path?token=...). The query param carrier lets a plain fetch() authenticate without a preflight-triggering Authorization header, and it survives being tucked into the explorer's `#src=` fragment.
func (*BlobServer) Token ¶
func (b *BlobServer) Token() string
Token is the per-run bearer token the page must present to fetch the blob.
func (*BlobServer) WaitServed ¶
func (b *BlobServer) WaitServed(ctx context.Context) ServeOutcome
WaitServed blocks until the page fetches the blob (then a short grace for a reload), or the max wait elapses, or ctx is canceled - and shuts the server down before returning the outcome.
type Recorder ¶ added in v0.4.0
type Recorder struct {
// contains filtered or unexported fields
}
Recorder accumulates time spent waiting on magus's own remote I/O for one unit of work. Safe for concurrent use: a target may fan out several fetches at once.
func RecorderFrom ¶ added in v0.4.0
RecorderFrom returns the Recorder on ctx, or nil when none is installed. A nil Recorder is usable: every method tolerates it, so uninstrumented paths cost nothing and need no guard.
type ServeOutcome ¶
type ServeOutcome int
ServeOutcome reports how a one-shot BlobServer ended, so the caller can print an accurate message.
const ( ServeCompleted ServeOutcome = iota // the page fetched the blob; server stopped after a grace window ServeTimedOut // nobody fetched within blobMaxWait (browser never opened?) ServeCanceled // ctx was canceled (Ctrl-C) )
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is the loopback-bound HTTP core: a 127.0.0.1 listener, a mux, and an *http.Server with a header-read timeout. It generalizes the inline server hand-rolled by callers that need a single loopback port with a few mounted routes and graceful, ctx-driven shutdown.
func NewServer ¶
NewServer binds a loopback listener on the given address's port (0 = first available ephemeral port) and prepares the mux. The address's host is ignored: the listener is always forced onto 127.0.0.1 regardless of what addr names, so the server can never be exposed on a network interface.
func (*Server) Addr ¶
Addr is the loopback address actually bound - the real port even when the caller requested port 0.