middleware

package
v0.0.1-alpha.30 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 30 Imported by: 0

Documentation

Overview

Package middleware provides HTTP middleware for the emulator.

CORS adds Cross-Origin Resource Sharing headers so that browser-based AWS SDK clients (e.g. the Overcast web UI) can talk directly to the emulator without going through a backend-for-frontend proxy.

This is deliberately permissive — the emulator is a local dev tool, not a security boundary. All origins, methods, and headers are allowed.

Package middleware contains HTTP middleware functions for the emulator's request pipeline. Each middleware is a standard net/http middleware — it takes a handler and returns a handler. This is identical to Express middleware in concept: (req, res, next) => void.

In Go:

func MyMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // do something before
        next.ServeHTTP(w, r)
        // do something after
    })
}

Index

Constants

View Source
const (
	LabelExecuteAPI         = "execute-api"
	LabelLambdaURL          = "lambda-url"
	LabelAppSyncAPI         = "appsync-api"
	LabelAppSyncRealtimeAPI = "appsync-realtime-api"
	LabelCloudFront         = "cloudfront"
	LabelELB                = "elb"
)

hostroute.go implements ONE general grammar + dispatch table for AWS services whose real invoke/control endpoints encode a resource ID (and usually a region) as a Host subdomain rather than in the request path:

{id}.{label}[.{region}].{base}[:port]

Real examples:

myapi123.execute-api.us-east-1.amazonaws.com   (API Gateway invoke, v1+v2)
{url-id}.lambda-url.us-east-1.on.aws           (Lambda function URLs)
myapi456.appsync-api.us-east-1.amazonaws.com   (AppSync GraphQL)

ParseHostRoute recognises this grammar against the fixed set of known `label` tokens in hostRouteLabels and returns the {id} (everything before the first recognised label — dot-joined, so IDs that themselves contain dots are supported) and {region} (the segment immediately after the label, only when it looks like an AWS region — otherwise region is "" and that segment is just the start of {base}).

hostRouteLabels is the single source of truth mapping a label token to the AWS service that owns it. HostAddressing (hostaddressing.go) reads it to classify requests, and detectService (logger.go) resolves the log label from the claim that classification produced, so a request's log label can never drift from what it was actually dispatched to.

---- Adding a new host-routed service ----

  1. Check the label cannot plausibly end a bucket name. See the guardrail below — this is a correctness requirement, not style.
  2. Add its "label" -> service-name entry to hostRouteLabels below.
  3. In router.go, append one middleware.HostRouteRow{Label: "...", Rewrite: ...} to the rows slice passed to middleware.HostAddressing. Keep Rewrite thin: string manipulation of r.URL.Path (and maybe a region context stamp) only, or a call into one small exported method on the owning service (e.g. apigwSvc.HostRouteRewrite) — never protocol/business logic here.

---- Guardrail: labels must not be plausible bucket-name segments ----

A bucket named "my.execute-api" is not addressable in the bare virtual-hosted form, because "my.execute-api.localhost" parses as an API Gateway invoke. That collision surface stays negligible only because every registered label is a hyphenated, AWS-specific data-plane token that nobody ends a bucket name with. Registering a bare common word ("logs", "events", "data") would widen it immediately.

Prefer the AWS data-plane hostname verbatim — "appsync-realtime-api", not "realtime". TestReservedHostLabels_areNotPlausibleBucketSuffixes enforces the hyphenation rule; docs/plans/host-routing-precedence.md §6 tracks making this evidence-based against the generated AWS operation manifest, so the set can only grow when AWS itself adds a service or hostname.

---- Also reads this table ----

  • internal/middleware/region.go's regionFromHost: extracts the region hint from this same grammar for SigV4-less requests, which is the only region evidence a host-routed invoke carries. It used to keep a third divergent label list; H5 folded it onto parseHostRouteName, so registering a label below is all a new host-routed service needs to resolve in the right region. It keeps a small disjoint set for regional service endpoints Overcast does NOT dispatch on ("s3" and friends), which must stay out of this table — see regionalEndpointLabels.

Host-route labels, exported so the services that MINT these URLs (serviceutil.HostRoutedURL callers) use the same string the router dispatches on. serviceutil cannot import this package — middleware imports serviceutil — so the label travels as a parameter; these constants are what stops the two sides drifting.

View Source
const DefaultSigV4Secret = defaultSigV4Secret

DefaultSigV4Secret is the fallback signing secret used when no SecretResolver is configured or when the resolver cannot find a secret for the given access key. The value "test" matches the default credentials used by the AWS SDK and CLI in local-dev workflows.

Variables

This section is empty.

Functions

func BucketNameReservedLabel

func BucketNameReservedLabel(bucket string) (string, bool)

BucketNameReservedLabel reports whether bucket carries a reserved host label as a second-or-later dot segment, which is the only position that collides: in "{bucket}.{base}" the bucket's first segment lands at hostname index 0, and ParseHostRoute requires a label at index >= 1 because {id} must be non-empty in every real AWS host-routed shape.

"execute-api"            -> "", false  (index 0, safe)
"execute-api.thing"      -> "", false  (index 0, safe)
"my.execute-api"         -> "execute-api", true
"my.execute-api.thing"   -> "execute-api", true

func CORS

func CORS(next http.Handler) http.Handler

CORS returns a middleware that sets permissive CORS headers on every response and handles preflight OPTIONS requests.

func ClientAddrFromContext

func ClientAddrFromContext(ctx context.Context) string

ClientAddrFromContext returns the caller's IP address as stamped by the ClientEndpoint middleware, or "" when the request had none. It travels with the origin through internal dispatch — CloudFormation resolving a `Fn::GetAtt` runs on the deploying caller's context — so a resource attribute is rendered for whoever asked for the stack, not for the emulator.

func ClientEndpoint

func ClientEndpoint(next http.Handler) http.Handler

ClientEndpoint records the origin — scheme://host[:port] — that the caller used to reach Overcast, so handlers can mint resource URLs the caller can actually dial back.

This matters because several AWS SDKs resolve a service endpoint from a resource URL rather than from client configuration. The clearest case is SQS: @aws-sdk/middleware-sdk-sqs replaces the resolved endpoint with the QueueUrl's origin whenever the two differ and no explicit `endpoint` was passed to the client — and AWS_ENDPOINT_URL does not count, because it is resolved through the endpoint ruleset's Endpoint parameter and never lands on config.endpoint. .NET and Java v1 use the queue URL as the request URI for the same historical reason (the query protocol addressed queues by URL).

A single server-wide origin therefore cannot serve every caller: "localhost" is right for a host CLI and wrong inside a sibling Lambda container, and a compose service name is the reverse. Minting per request keeps every URL dialable by whoever asked for it.

Requests arriving on a real AWS hostname fall through: the context is left unset and handlers use the configured external origin (OVERCAST_HOSTNAME).

The caller's own address is stamped alongside the origin, because a name is not the whole answer for services whose resource is a *container* rather than Overcast itself: an RDS instance answers on its engine port inside the Docker network and on a published port on the host, so which port to hand back depends on which side of that boundary the caller is. The origin cannot say — a split-horizon hostname is used by both sides — but the source address can. See serviceutil.CallerIsSiblingContainer.

func ClientEndpointFromContext

func ClientEndpointFromContext(ctx context.Context) string

ClientEndpointFromContext returns the origin stored by the ClientEndpoint middleware, or "" when the request had none (background work, internal callers, or a request on a real AWS hostname). Callers fall back to the configured external origin.

func ContextWithClientAddr

func ContextWithClientAddr(ctx context.Context, addr string) context.Context

ContextWithClientAddr returns a child context carrying the caller's address, the companion of ContextWithClientEndpoint.

func ContextWithClientEndpoint

func ContextWithClientEndpoint(ctx context.Context, origin string) context.Context

ContextWithClientEndpoint returns a child context carrying origin, for background goroutines that mint resource URLs outside a request context.

func ContextWithRegion

func ContextWithRegion(ctx context.Context, region string) context.Context

ContextWithRegion returns a child context carrying region, suitable for background goroutines that need to access region-scoped stores outside a request context.

func DrainBody

func DrainBody(next http.Handler) http.Handler

DrainBody returns a middleware that:

  1. Drains and closes the request body after the handler returns.
  2. Buffers the response body so that a Content-Length header is always set before the first byte reaches the client.

Together these prevent the Go AWS SDK v2 warning:

"WARN failed to close HTTP response body, this may affect connection reuse"

The warning fires on the CLIENT side when resp.Body.Close() fails. The root cause is a server response without Content-Length: the client relies on chunked transfer or connection close to detect the end of the body, and any framing issue makes Close() return an error. Setting Content-Length on every response eliminates the problem.

If the handler calls http.Flusher.Flush() (streaming / SSE) or the buffered body exceeds maxResponseBuffer, the middleware switches to direct pass-through so large or streaming responses are never fully buffered.

func HostAddressing

func HostAddressing(configuredHostname string, rows *[]HostRouteRow, logger *zap.Logger) func(http.Handler) http.Handler

HostAddressing returns the middleware that applies the precedence rule. It replaces the former S3VirtualHostFor + HostDispatch pair; because one classification drives both rewrites, they are mutually exclusive branches of one switch and can no longer both fire.

rows is read through the pointer at request time, so callers can register this middleware early (chi requires all r.Use calls before any route registration) and populate the rows later, once the owning services exist.

func HostRouteServiceFor

func HostRouteServiceFor(m HostRouteMatch) (service string, ok bool)

HostRouteServiceFor reports the detectService() label owning a parsed host-route match — e.g. "apigateway" for an execute-api Host. detectService resolves it from the claim HostAddressing stamped on the request, so a request's log label is what actually routed it rather than a re-derivation that could disagree.

func IAMEnforce

func IAMEnforce(enabled bool, st state.Store, logger *zap.Logger) func(http.Handler) http.Handler

IAMEnforce enforces opt-in IAM authorization.

func InvalidateIAMEnforceCache

func InvalidateIAMEnforceCache()

InvalidateIAMEnforceCache marks every compiled-policy cache stale. Called by the IAM service after any operation that changes what a principal is allowed to do.

func Logger

func Logger(logger *zap.Logger, clk clock.Clock) func(http.Handler) http.Handler

Logger logs every request with structured fields: real AWS API calls and other requests at INFO, internal health/readiness and /_debug/* polling at TRACE (see isOperationalPollPath). When stdout is a terminal, each line is prefixed with the service badge and (when known) an operation badge so log lines are easy to scan at a glance. Failed requests (5xx) are logged at ERROR level regardless of path.

func NotReady

func NotReady(store state.Store) func(http.Handler) http.Handler

NotReady rejects a request with a 503 while the storage backend is still completing a one-time startup migration (see internal/state/migrate.go), instead of letting the request observe whatever the store would otherwise do during that window: persistent mode blocks the request indefinitely inside ensureReady, and hybrid mode's TierHot reads silently return "not found" for data that exists once migration finishes, because the post-migration seed hasn't populated memory yet (see state.NotReadyReporter and HybridStore.NotReady for the precise window this covers).

Internal Overcast endpoints (any path starting with "/_" — /_debug, /_health, /_/info, /_overcast/*, ...) are exempt, so operators can still check status, inspect debug state, or poll init-hook progress while a migration is in flight. No real AWS API request path starts with "/_".

store is checked once per request via a non-blocking type assertion to state.NotReadyReporter — stores that don't implement it (MemoryStore, WALStore) are always treated as ready, the same convention state.ReadyAwaiter already uses.

func Protocol

func Protocol(identifiers []codec.Identifier) func(http.Handler) http.Handler

Protocol is the wire-protocol detection middleware. It walks a list of codec.Identifiers in precision order; on the first match it stashes the codec and operation name in the request context (retrievable via codec.FromContext) and forwards the request unchanged.

The middleware NEVER:

  • consumes the request body,
  • rejects a request, or
  • alters the request in any way other than adding context values.

On no match it forwards the request unchanged, so legacy handlers continue to function exactly as before. Rejection of unsupported protocols for opted-in services happens at the dispatcher boundary, not here.

This middleware is always-on as of Phase 6 completion.

func Recovery

func Recovery(logger *zap.Logger) func(http.Handler) http.Handler

Recovery catches any panic from a handler, logs it with a stack trace, and returns a 500 InternalError response. Without this, a panic in one handler would crash the entire server process.

In Go, a "panic" is like an uncaught exception — recover() is the equivalent of a catch-all try/catch block.

func Region

func Region(next http.Handler) http.Handler

Region extracts the AWS region from each request and stores it in the context. Resolution order (first non-empty wins):

  1. X-Overcast-Region header (internal override used by the CloudFormation provisioner)
  2. SigV4 Authorization header Credential scope: AKID/DATE/REGION/SERVICE/aws4_request
  3. Host header subdomain: <id>.execute-api.<region>.<base> — the canonical AWS API Gateway invoke URL shape (also supported by LocalStack).

If none yield a region the context is left unchanged and handlers fall back to cfg.Region (OVERCAST_DEFAULT_REGION, default "us-east-1"). This mirrors how LocalStack resolves region: always from the request, never from a server-wide setting.

func RegionFromContext

func RegionFromContext(ctx context.Context, fallback string) string

RegionFromContext returns the per-request region stored by the Region middleware. If absent, returns fallback.

func RequestEvents

func RequestEvents(busPtr **events.Bus, clk clock.Clock) func(http.Handler) http.Handler

RequestEvents publishes a request:Received event onto the bus for every incoming HTTP request. The bus is injected via a pointer-to-pointer so it can be set after middleware registration (the bus is created late in router.New). If the bus is nil at request time, publishing is skipped.

This middleware intentionally mirrors the Logger middleware's responseWriter + detectService/detectOperation pattern — both intercept the request lifecycle to capture the same metadata for different purposes (logging vs event publishing).

Performance: each event is enqueued on the bus's 4096-capacity buffered worker pool. Publish returns immediately in the common case; it only waits if all 4096 slots are occupied by in-flight work items and 16 workers haven't caught up yet. When no SSE client is connected there are no wildcard subscribers, so zero work items are enqueued (zero overhead).

func RequestID

func RequestID(next http.Handler) http.Handler

RequestID attaches a unique request ID to every request context and response header. All subsequent middleware and handlers retrieve it via protocol.RequestIDFromContext(r.Context()).

func ReservedHostLabels

func ReservedHostLabels() []string

ReservedHostLabels returns the host labels Overcast dispatches on, sorted for stable output. These are "reserved" only in the routing sense: a bucket whose name carries one as a second-or-later dot segment is not addressable in the bare virtual-hosted form (see BucketNameReservedLabel). Bucket creation is never refused — real AWS accepts such names, and both path-style and tier-A addressing keep working.

This is deliberately NOT the set of all AWS endpoint prefixes. Reserving all of them would make names like "my.logs" or "my.events" collide for no benefit, since Overcast does not host-route those services.

func ServiceFromCredential

func ServiceFromCredential(r *http.Request) string

ServiceFromCredential extracts the service name (e.g. "appsync", "apigateway") from the SigV4 Authorization header's Credential scope. Returns "" if not parseable.

func SigV4

func SigV4(validate bool, secretResolver SecretResolver, logger *zap.Logger, clk clock.Clock) func(http.Handler) http.Handler

SigV4 validates AWS SigV4 signed requests when validation is enabled. Unsigned requests still pass through so emulator-internal endpoints and local no-auth workflows remain usable.

secretResolver optionally resolves per-access-key secrets from IAM. When nil or when it returns no match the middleware falls back to DefaultSigV4Secret ("test") for backward compatibility.

Types

type HostClaim

type HostClaim struct {
	Kind   HostClaimKind
	Bucket string         // set when Kind == HostClaimS3
	Route  HostRouteMatch // set when Kind == HostClaimHostRoute
}

HostClaim is the single verdict for a request's Host header. Exactly one of Bucket / Route is meaningful, selected by Kind — the two can never both be set, which is the invariant the old two-middleware arrangement violated.

func HostClaimFromContext

func HostClaimFromContext(ctx context.Context) (HostClaim, bool)

HostClaimFromContext returns the claim stamped by HostAddressing. Only host-routed claims are stamped: S3 and unclaimed requests both end at the S3 handler, which is already detectService's default, so stamping them would allocate a context on the hottest path for no benefit.

type HostClaimKind

type HostClaimKind uint8

HostClaimKind identifies which addressing scheme owns a request.

const (
	// HostClaimNone means no scheme claimed the Host; the request keeps its
	// path and falls through to S3 path-style routing.
	HostClaimNone HostClaimKind = iota
	// HostClaimS3 means the Host carries an S3 bucket as a subdomain.
	HostClaimS3
	// HostClaimHostRoute means the Host matches a registered host-routed
	// service (execute-api, lambda-url, appsync-api).
	HostClaimHostRoute
)

type HostClassifier

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

HostClassifier applies the precedence rule above. Construct it once (it precomputes the recognised base list) and call Classify per request.

func NewHostClassifier

func NewHostClassifier(configuredHostname string) *HostClassifier

NewHostClassifier returns a classifier recognising the built-in wildcard-DNS bases plus configuredHostname (OVERCAST_HOSTNAME) when set. The configured hostname is additive, never a replacement.

func (*HostClassifier) Classify

func (c *HostClassifier) Classify(host string) HostClaim

Classify returns the single owner of host (which may include a port). Allocation-free.

type HostRouteMatch

type HostRouteMatch struct {
	// Label is the recognised host segment, e.g. "execute-api". Always a key
	// of hostRouteLabels.
	Label string
	// ID is the subdomain segment(s) before Label, dot-joined.
	ID string
	// Region is the AWS region parsed from the segment after Label, or ""
	// if that segment doesn't look like a region (e.g. the base hostname
	// starts right after the label, as with a bare "localhost" base).
	Region string
}

HostRouteMatch is a successfully parsed Host-based AWS endpoint address.

func ParseHostRoute

func ParseHostRoute(host string) (HostRouteMatch, bool)

ParseHostRoute parses host (which may include a port) against the AWS `{id}.{label}[.{region}].{base}` grammar using the labels registered in hostRouteLabels. Returns ok=false for path-style requests, IP literals, or hosts that don't contain a registered label. It considers the grammar in isolation. Callers that must also account for S3 virtual-hosted addressing — i.e. anything on the request path — should use HostClassifier.Classify instead, which applies the full precedence rule.

type HostRouteRow

type HostRouteRow struct {
	// Label must be a key of hostRouteLabels.
	Label string
	// Rewrite mutates r (typically r.URL.Path/RawPath, and optionally the
	// request context, e.g. to stamp a region hint) in place so the request
	// matches a route already registered for the owning service. Called
	// once, synchronously, before chi's router dispatches on the (possibly
	// now-different) path. Rewrite should always mutate on a recognised ID
	// — even one that turns out not to exist — and let the owning service's
	// own handler produce the AWS-shaped not-found/forbidden error; only a
	// Host that doesn't match the grammar at all should fall through
	// untouched (see AGENTS.md "Routing fallthrough is S3").
	Rewrite func(r *http.Request, m HostRouteMatch)
}

HostRouteRow binds one recognised label to the rewrite that adapts a Host-routed request into the owning service's existing path-style route. See the package doc above for the recipe to add a new row.

type SecretResolver

type SecretResolver interface {
	ResolveSecret(ctx context.Context, accessKeyID string) (secret string, found bool, err error)
}

SecretResolver resolves the secret access key for a given access key ID. The default implementation looks up IAM user access keys and STS role session credentials stored in the emulator's state store.

If no secret is found, implementations should return ("", false, nil) so the middleware can fall back to the default secret.

func NewSecretResolver

func NewSecretResolver(st state.Store) SecretResolver

NewSecretResolver returns a SecretResolver backed by the emulator's state store. When st is nil the returned resolver never finds a secret so the middleware falls back to the hardcoded default ("test").

Jump to

Keyboard shortcuts

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