orama/core/pkg/serverless/raw_http.go
anonpenguin23 f41242538e feat(serverless): add raw http response mode and secrets encryption
- Add `raw_http_response` configuration to functions to allow verbatim HTTP responses
- Implement cluster-wide secrets encryption key generation and distribution for serverless functions
- Update documentation with UnifiedPush support for ntfy on Android/GrapheneOS
2026-06-09 13:01:02 +03:00

143 lines
5.1 KiB
Go

package serverless
import (
"context"
"fmt"
"sync"
)
// Raw-HTTP-response mode (bugboard #835).
//
// A function deployed with RawHTTPResponse=true can emit a verbatim HTTP
// response (status + headers + body) instead of the JSON/Ack-wrapped output
// the stateless invoke handler normally produces. This lets a namespace app
// proxy an upstream RPC (Helius / Alchemy) transparently — the function reads
// the request, calls the upstream, and replays the upstream's status, headers,
// and body byte-for-byte back to its own caller.
//
// The primitive provided here is ONLY the response carrier + the host-call
// validation. Per-user-JWT quota gating (which the ticket mentions) is the
// APP's responsibility: the function can call oh.GetCallerJwtSubject() and
// decide whether to serve. The gateway does not implement quota here.
const (
// rawHTTPMaxHeaders caps how many response headers a function may set.
// Generous for a proxy use-case (upstream RPCs return well under this)
// while bounding the per-invocation allocation a hostile function could
// force.
rawHTTPMaxHeaders = 64
// rawHTTPMaxBodyBytes caps the verbatim response body a function may set.
// 8 MiB comfortably covers JSON-RPC responses (even large getBlock /
// getProgramAccounts payloads) without letting a function buffer an
// unbounded body in gateway memory.
rawHTTPMaxBodyBytes = 8 << 20
// rawHTTPMinStatus / rawHTTPMaxStatus bound a valid HTTP status code.
rawHTTPMinStatus = 100
rawHTTPMaxStatus = 599
)
// RawHTTPResult is a verbatim HTTP response set by a RawHTTPResponse function.
// Set is true once the function has called set_http_response at least once;
// the invoke handler only takes the raw path when Set is true (otherwise it
// falls back to the normal JSON/Ack-wrapped behavior).
type RawHTTPResult struct {
Status int
Headers map[string]string
Body []byte
Set bool
}
// rawHTTPCollector is the mutable per-invocation sink the set_http_response
// host function writes to. It rides the invocation's context (same per-call
// propagation model as the publish counter and log buffer) so concurrent
// invocations never cross-write each other's response.
type rawHTTPCollector struct {
mu sync.Mutex
result RawHTTPResult
}
// rawHTTPKey is the unexported context-value key for the raw-HTTP collector.
type rawHTTPKey struct{}
// WithRawHTTPCollector returns a derived ctx carrying a FRESH per-invocation
// raw-HTTP response collector. The engine attaches this before executing a
// RawHTTPResponse function so the set_http_response host call has somewhere to
// write; for non-raw functions the collector is absent and the host call is a
// validated no-op.
func WithRawHTTPCollector(ctx context.Context) context.Context {
return context.WithValue(ctx, rawHTTPKey{}, &rawHTTPCollector{})
}
// rawHTTPCollectorFromCtx extracts the collector attached via
// WithRawHTTPCollector, or nil if none is present (non-raw function, or an
// untracked code path).
func rawHTTPCollectorFromCtx(ctx context.Context) *rawHTTPCollector {
if ctx == nil {
return nil
}
c, _ := ctx.Value(rawHTTPKey{}).(*rawHTTPCollector)
return c
}
// SetRawHTTPResponse records a verbatim HTTP response on the invocation's
// collector. Returns an error if no collector is attached (the function was
// not deployed with RawHTTPResponse), or if the status / header count / body
// size fail validation. Headers may be nil. The body is copied so the caller
// (which reads it out of guest WASM memory) may reuse its buffer.
func SetRawHTTPResponse(ctx context.Context, status int, headers map[string]string, body []byte) error {
c := rawHTTPCollectorFromCtx(ctx)
if c == nil {
return fmt.Errorf("set_http_response: function is not deployed with raw_http_response enabled")
}
if status < rawHTTPMinStatus || status > rawHTTPMaxStatus {
return fmt.Errorf("set_http_response: status %d out of range [%d,%d]", status, rawHTTPMinStatus, rawHTTPMaxStatus)
}
if len(headers) > rawHTTPMaxHeaders {
return fmt.Errorf("set_http_response: too many headers (%d > %d)", len(headers), rawHTTPMaxHeaders)
}
if len(body) > rawHTTPMaxBodyBytes {
return fmt.Errorf("set_http_response: body too large (%d bytes > %d)", len(body), rawHTTPMaxBodyBytes)
}
bodyCopy := make([]byte, len(body))
copy(bodyCopy, body)
var hdrCopy map[string]string
if len(headers) > 0 {
hdrCopy = make(map[string]string, len(headers))
for k, v := range headers {
hdrCopy[k] = v
}
}
c.mu.Lock()
c.result = RawHTTPResult{
Status: status,
Headers: hdrCopy,
Body: bodyCopy,
Set: true,
}
c.mu.Unlock()
return nil
}
// TakeRawHTTPResponse returns the raw HTTP response recorded on the ctx's
// collector and whether one was set. Returns (zero, false) when no collector
// is attached or the function never called set_http_response. The engine calls
// this after Execute to surface the response on the InvokeResponse.
func TakeRawHTTPResponse(ctx context.Context) (RawHTTPResult, bool) {
c := rawHTTPCollectorFromCtx(ctx)
if c == nil {
return RawHTTPResult{}, false
}
c.mu.Lock()
res := c.result
c.mu.Unlock()
if !res.Set {
return RawHTTPResult{}, false
}
return res, true
}