Refactor RDP proxy handling and update related tests

This commit is contained in:
2026-05-17 20:38:35 +03:00
parent 8e9402580f
commit d551e57fd5
172 changed files with 22117 additions and 2509 deletions
@@ -0,0 +1,190 @@
package webingress
import (
"context"
"encoding/json"
"net/http"
"strings"
"time"
)
const AdminRuntimeResponseSchema = "rap.web_ingress.admin_runtime_response.v1"
const ControlAPIProjectionRequestSchema = "rap.web_ingress.control_api_projection_request.v1"
const ControlAPIProjectionResponseSchema = "rap.web_ingress.control_api_projection_response.v1"
type AdminRuntimeDispatcher struct {
ProjectionClient ControlAPIProjectionClient
Now func() time.Time
}
type ControlAPIProjectionClient interface {
Project(ctx context.Context, request ControlAPIProjectionRequest) (ControlAPIProjectionResponse, error)
}
type ControlAPIProjectionRequest struct {
SchemaVersion string `json:"schema_version"`
Method string `json:"method"`
Path string `json:"path"`
Query string `json:"query,omitempty"`
Host string `json:"host,omitempty"`
Scope string `json:"scope"`
ServiceClass string `json:"service_class"`
ObservedAt string `json:"observed_at"`
}
type ControlAPIProjectionResponse struct {
SchemaVersion string `json:"schema_version"`
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
StatusCode int `json:"status_code"`
Headers map[string]string `json:"headers,omitempty"`
Body json.RawMessage `json:"body,omitempty"`
}
type AdminRuntimeJSONResponse struct {
SchemaVersion string `json:"schema_version"`
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
Scope string `json:"scope,omitempty"`
ServiceClass string `json:"service_class,omitempty"`
Path string `json:"path,omitempty"`
Manifest map[string]any `json:"manifest,omitempty"`
ObservedAt string `json:"observed_at"`
}
func (d AdminRuntimeDispatcher) HandleFabricRequest(ctx context.Context, request FabricRequest) (FabricResponse, error) {
method := strings.ToUpper(strings.TrimSpace(request.Method))
path := normalizeRuntimePath(request.Path)
if method == "" {
method = http.MethodGet
}
if !allowedAdminRuntimeScope(strings.TrimSpace(request.Scope), strings.TrimSpace(request.ServiceClass)) {
return d.json(http.StatusForbidden, request, "blocked", "admin_runtime_scope_rejected", nil), nil
}
switch {
case method == http.MethodGet && (path == "/healthz" || path == "/readyz"):
return d.json(http.StatusOK, request, "ready", "admin_runtime_ready", nil), nil
case d.ProjectionClient != nil && (method == http.MethodGet || method == http.MethodHead):
return d.project(ctx, request)
case method == http.MethodGet && (path == "/ui-manifest" || strings.HasSuffix(path, "/ui-manifest")):
return d.json(http.StatusOK, request, "ready", "ui_manifest_ready", d.manifest(request)), nil
case method != http.MethodGet && method != http.MethodHead:
return d.json(http.StatusForbidden, request, "blocked", "control_api_mutation_binding_not_implemented", nil), nil
default:
return d.json(http.StatusNotImplemented, request, "blocked", "control_api_projection_binding_not_implemented", nil), nil
}
}
func allowedAdminRuntimeScope(scope string, serviceClass string) bool {
switch serviceClass {
case "platform_admin":
return scope == "platform"
case "cluster_admin":
return scope == "cluster"
case "organization_portal":
return scope == "organization"
case "user_portal":
return scope == "user" || scope == "organization"
default:
return false
}
}
func (d AdminRuntimeDispatcher) project(ctx context.Context, request FabricRequest) (FabricResponse, error) {
response, err := d.ProjectionClient.Project(ctx, ControlAPIProjectionRequest{
SchemaVersion: ControlAPIProjectionRequestSchema,
Method: strings.ToUpper(strings.TrimSpace(request.Method)),
Path: normalizeRuntimePath(request.Path),
Query: request.Query,
Host: request.Host,
Scope: request.Scope,
ServiceClass: request.ServiceClass,
ObservedAt: d.observedAt(),
})
if err != nil {
return d.json(http.StatusBadGateway, request, "blocked", "control_api_projection_failed", nil), nil
}
if response.SchemaVersion != ControlAPIProjectionResponseSchema {
return d.json(http.StatusBadGateway, request, "blocked", "control_api_projection_invalid_response", nil), nil
}
headers := http.Header{"Content-Type": []string{"application/json"}}
for key, value := range response.Headers {
if safeResponseHeader(key) && strings.TrimSpace(value) != "" {
headers.Set(key, value)
}
}
statusCode := response.StatusCode
if statusCode < 100 || statusCode > 599 {
statusCode = http.StatusOK
}
return FabricResponse{StatusCode: statusCode, Headers: headers, Body: append([]byte(nil), response.Body...)}, nil
}
func (d AdminRuntimeDispatcher) json(statusCode int, request FabricRequest, status string, reason string, manifest map[string]any) FabricResponse {
payload, _ := json.Marshal(AdminRuntimeJSONResponse{
SchemaVersion: AdminRuntimeResponseSchema,
Status: status,
Reason: reason,
Scope: request.Scope,
ServiceClass: request.ServiceClass,
Path: request.Path,
Manifest: manifest,
ObservedAt: d.observedAt(),
})
return FabricResponse{
StatusCode: statusCode,
Headers: http.Header{"Content-Type": []string{"application/json"}},
Body: payload,
}
}
func (d AdminRuntimeDispatcher) manifest(request FabricRequest) map[string]any {
serviceClass := strings.TrimSpace(request.ServiceClass)
sections := []string{}
actions := []string{}
switch serviceClass {
case "platform_admin":
sections = []string{"clusters", "nodes", "roles", "fabric", "workloads", "audit"}
actions = []string{"read_platform_summary", "read_cluster_summaries", "read_node_status"}
case "cluster_admin":
sections = []string{"cluster", "nodes", "fabric", "workloads", "audit"}
actions = []string{"read_cluster_summary", "read_node_status"}
case "organization_portal":
sections = []string{"organization", "sessions", "resources", "audit"}
actions = []string{"read_organization_summary", "read_sessions"}
case "user_portal":
sections = []string{"profile", "sessions", "resources"}
actions = []string{"read_profile", "read_sessions"}
default:
sections = []string{"status"}
actions = []string{"read_status"}
}
return map[string]any{
"schema_version": "rap.web_ingress.ui_manifest.v1",
"scope": request.Scope,
"service_class": serviceClass,
"sections": sections,
"allowed_actions": actions,
"mutation_enabled": false,
"projection_binding": "control_api_not_bound",
}
}
func (d AdminRuntimeDispatcher) observedAt() string {
now := time.Now().UTC()
if d.Now != nil {
now = d.Now().UTC()
}
return now.Format(time.RFC3339Nano)
}
func normalizeRuntimePath(path string) string {
path = strings.TrimSpace(path)
if path == "" {
return "/"
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return path
}
@@ -0,0 +1,212 @@
package webingress
import (
"context"
"encoding/json"
"net/http"
"testing"
)
func TestAdminRuntimeDispatcherReturnsHealthAndManifest(t *testing.T) {
dispatcher := AdminRuntimeDispatcher{Now: fixedEnvelopeNow}
health, err := dispatcher.HandleFabricRequest(context.Background(), FabricRequest{
Method: http.MethodGet,
Path: "/readyz",
Scope: "platform",
ServiceClass: "platform_admin",
})
if err != nil {
t.Fatalf("health: %v", err)
}
if health.StatusCode != http.StatusOK {
t.Fatalf("health = %+v", health)
}
manifest, err := dispatcher.HandleFabricRequest(context.Background(), FabricRequest{
Method: http.MethodGet,
Path: "/platform-admin/ui-manifest",
Scope: "platform",
ServiceClass: "platform_admin",
})
if err != nil {
t.Fatalf("manifest: %v", err)
}
var payload AdminRuntimeJSONResponse
if err := json.Unmarshal(manifest.Body, &payload); err != nil {
t.Fatalf("decode manifest: %v", err)
}
if manifest.StatusCode != http.StatusOK ||
payload.SchemaVersion != AdminRuntimeResponseSchema ||
payload.Status != "ready" ||
payload.Reason != "ui_manifest_ready" ||
payload.Manifest["schema_version"] != "rap.web_ingress.ui_manifest.v1" ||
payload.Manifest["mutation_enabled"] != false {
t.Fatalf("payload = %+v status=%d", payload, manifest.StatusCode)
}
}
func TestAdminRuntimeDispatcherBlocksMutationsAndUnknownProjection(t *testing.T) {
dispatcher := AdminRuntimeDispatcher{Now: fixedEnvelopeNow}
mutation, err := dispatcher.HandleFabricRequest(context.Background(), FabricRequest{
Method: http.MethodPost,
Path: "/platform-admin/nodes",
Scope: "platform",
ServiceClass: "platform_admin",
})
if err != nil {
t.Fatalf("mutation: %v", err)
}
var mutationPayload AdminRuntimeJSONResponse
if err := json.Unmarshal(mutation.Body, &mutationPayload); err != nil {
t.Fatalf("decode mutation: %v", err)
}
if mutation.StatusCode != http.StatusForbidden || mutationPayload.Reason != "control_api_mutation_binding_not_implemented" {
t.Fatalf("mutation payload = %+v status=%d", mutationPayload, mutation.StatusCode)
}
projection, err := dispatcher.HandleFabricRequest(context.Background(), FabricRequest{
Method: http.MethodGet,
Path: "/platform-admin/nodes",
Scope: "platform",
ServiceClass: "platform_admin",
})
if err != nil {
t.Fatalf("projection: %v", err)
}
var projectionPayload AdminRuntimeJSONResponse
if err := json.Unmarshal(projection.Body, &projectionPayload); err != nil {
t.Fatalf("decode projection: %v", err)
}
if projection.StatusCode != http.StatusNotImplemented || projectionPayload.Reason != "control_api_projection_binding_not_implemented" {
t.Fatalf("projection payload = %+v status=%d", projectionPayload, projection.StatusCode)
}
}
func TestAdminRuntimeDispatcherRejectsInvalidScopeClassPair(t *testing.T) {
dispatcher := AdminRuntimeDispatcher{ProjectionClient: &recordingProjectionClient{}, Now: fixedEnvelopeNow}
response, err := dispatcher.HandleFabricRequest(context.Background(), FabricRequest{
Method: http.MethodGet,
Path: "/platform-admin/ui-manifest",
Scope: "organization",
ServiceClass: "platform_admin",
})
if err != nil {
t.Fatalf("projection: %v", err)
}
var payload AdminRuntimeJSONResponse
if err := json.Unmarshal(response.Body, &payload); err != nil {
t.Fatalf("decode response: %v", err)
}
if response.StatusCode != http.StatusForbidden || payload.Reason != "admin_runtime_scope_rejected" {
t.Fatalf("payload = %+v status=%d", payload, response.StatusCode)
}
}
func TestAdminRuntimeDispatcherUsesControlAPIProjectionClientForReadRequests(t *testing.T) {
client := &recordingProjectionClient{
response: ControlAPIProjectionResponse{
SchemaVersion: ControlAPIProjectionResponseSchema,
Status: "ready",
StatusCode: http.StatusOK,
Headers: map[string]string{"X-RAP-Projection": "control-api", "Set-Cookie": "blocked"},
Body: json.RawMessage(`{"schema_version":"control.projection.v1","ok":true}`),
},
}
dispatcher := AdminRuntimeDispatcher{ProjectionClient: client, Now: fixedEnvelopeNow}
response, err := dispatcher.HandleFabricRequest(context.Background(), FabricRequest{
Method: http.MethodGet,
Path: "/platform-admin/nodes",
Query: "limit=10",
Host: "admin.example.test",
Scope: "platform",
ServiceClass: "platform_admin",
})
if err != nil {
t.Fatalf("projection: %v", err)
}
if response.StatusCode != http.StatusOK ||
response.Headers.Get("X-RAP-Projection") != "control-api" ||
response.Headers.Get("Set-Cookie") != "" ||
string(response.Body) != `{"schema_version":"control.projection.v1","ok":true}` {
t.Fatalf("response = %+v body=%s", response, string(response.Body))
}
if client.request.Path != "/platform-admin/nodes" ||
client.request.Query != "limit=10" ||
client.request.Scope != "platform" ||
client.request.ServiceClass != "platform_admin" {
t.Fatalf("request = %+v", client.request)
}
}
func TestAdminRuntimeDispatcherReportsProjectionClientFailure(t *testing.T) {
dispatcher := AdminRuntimeDispatcher{ProjectionClient: failingProjectionClient{}, Now: fixedEnvelopeNow}
response, err := dispatcher.HandleFabricRequest(context.Background(), FabricRequest{
Method: http.MethodGet,
Path: "/platform-admin/nodes",
Scope: "platform",
ServiceClass: "platform_admin",
})
if err != nil {
t.Fatalf("projection: %v", err)
}
var payload AdminRuntimeJSONResponse
if err := json.Unmarshal(response.Body, &payload); err != nil {
t.Fatalf("decode response: %v", err)
}
if response.StatusCode != http.StatusBadGateway || payload.Reason != "control_api_projection_failed" {
t.Fatalf("payload = %+v status=%d", payload, response.StatusCode)
}
}
func TestAdminRuntimeDispatcherRejectsInvalidProjectionResponseSchema(t *testing.T) {
dispatcher := AdminRuntimeDispatcher{
ProjectionClient: &recordingProjectionClient{
response: ControlAPIProjectionResponse{
SchemaVersion: "wrong.schema",
Status: "ready",
StatusCode: http.StatusOK,
Body: json.RawMessage(`{"ok":true}`),
},
},
Now: fixedEnvelopeNow,
}
response, err := dispatcher.HandleFabricRequest(context.Background(), FabricRequest{
Method: http.MethodGet,
Path: "/platform-admin/nodes",
Scope: "platform",
ServiceClass: "platform_admin",
})
if err != nil {
t.Fatalf("projection: %v", err)
}
var payload AdminRuntimeJSONResponse
if err := json.Unmarshal(response.Body, &payload); err != nil {
t.Fatalf("decode response: %v", err)
}
if response.StatusCode != http.StatusBadGateway || payload.Reason != "control_api_projection_invalid_response" {
t.Fatalf("payload = %+v status=%d", payload, response.StatusCode)
}
}
type recordingProjectionClient struct {
request ControlAPIProjectionRequest
response ControlAPIProjectionResponse
}
func (c *recordingProjectionClient) Project(_ context.Context, request ControlAPIProjectionRequest) (ControlAPIProjectionResponse, error) {
c.request = request
return c.response, nil
}
type failingProjectionClient struct{}
func (failingProjectionClient) Project(context.Context, ControlAPIProjectionRequest) (ControlAPIProjectionResponse, error) {
return ControlAPIProjectionResponse{}, errTestProjectionFailure{}
}
type errTestProjectionFailure struct{}
func (errTestProjectionFailure) Error() string { return "projection failed" }
@@ -0,0 +1,151 @@
package webingress
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"net/http"
"sort"
"strings"
"time"
)
const FabricServiceChannelEnvelopeSchema = "rap.web_ingress.fabric_service_channel_envelope.v1"
var (
ErrFabricEnvelopeSignerRequired = errors.New("web ingress fabric envelope signer required")
ErrFabricEnvelopeSenderRequired = errors.New("web ingress fabric envelope sender required")
ErrFabricEnvelopeScopeRequired = errors.New("web ingress fabric envelope scope required")
ErrFabricEnvelopeClassRequired = errors.New("web ingress fabric envelope service class required")
)
type EnvelopeSigner interface {
Sign(ctx context.Context, canonical []byte) (FabricEnvelopeSignature, error)
}
type EnvelopeSender interface {
Send(ctx context.Context, envelope SignedFabricServiceChannelEnvelope) (FabricResponse, error)
}
type DefaultFabricBinder struct {
Signer EnvelopeSigner
Sender EnvelopeSender
Now func() time.Time
}
type FabricServiceChannelEnvelope struct {
SchemaVersion string `json:"schema_version"`
RequestSchema string `json:"request_schema"`
Method string `json:"method"`
Path string `json:"path"`
Query string `json:"query,omitempty"`
Host string `json:"host"`
ServiceType string `json:"service_type"`
Scope string `json:"scope"`
ServiceClass string `json:"service_class"`
Headers map[string][]string `json:"headers,omitempty"`
BodyBase64 string `json:"body_b64,omitempty"`
ObservedAt string `json:"observed_at"`
EnvelopedAt string `json:"enveloped_at"`
}
type FabricEnvelopeSignature struct {
KeyID string `json:"key_id"`
Alg string `json:"alg"`
Signature string `json:"signature"`
SignedAt string `json:"signed_at,omitempty"`
}
type SignedFabricServiceChannelEnvelope struct {
SchemaVersion string `json:"schema_version"`
Envelope FabricServiceChannelEnvelope `json:"envelope"`
Signature FabricEnvelopeSignature `json:"signature"`
Canonical []byte `json:"-"`
}
func (b DefaultFabricBinder) Forward(ctx context.Context, request FabricRequest) (FabricResponse, error) {
if b.Signer == nil {
return FabricResponse{}, ErrFabricEnvelopeSignerRequired
}
if b.Sender == nil {
return FabricResponse{}, ErrFabricEnvelopeSenderRequired
}
if strings.TrimSpace(request.Scope) == "" {
return FabricResponse{}, ErrFabricEnvelopeScopeRequired
}
if strings.TrimSpace(request.ServiceClass) == "" {
return FabricResponse{}, ErrFabricEnvelopeClassRequired
}
envelope := b.envelope(request)
canonical, err := json.Marshal(envelope)
if err != nil {
return FabricResponse{}, err
}
signature, err := b.Signer.Sign(ctx, canonical)
if err != nil {
return FabricResponse{}, err
}
return b.Sender.Send(ctx, SignedFabricServiceChannelEnvelope{
SchemaVersion: SignedFabricServiceChannelEnvelopeSchema,
Envelope: envelope,
Signature: signature,
Canonical: canonical,
})
}
func (b DefaultFabricBinder) envelope(request FabricRequest) FabricServiceChannelEnvelope {
now := time.Now().UTC()
if b.Now != nil {
now = b.Now().UTC()
}
observedAt := request.ObservedAt.UTC()
if observedAt.IsZero() {
observedAt = now
}
return FabricServiceChannelEnvelope{
SchemaVersion: FabricServiceChannelEnvelopeSchema,
RequestSchema: strings.TrimSpace(request.SchemaVersion),
Method: strings.ToUpper(strings.TrimSpace(request.Method)),
Path: request.Path,
Query: request.Query,
Host: strings.TrimSpace(request.Host),
ServiceType: strings.TrimSpace(request.ServiceType),
Scope: strings.TrimSpace(request.Scope),
ServiceClass: strings.TrimSpace(request.ServiceClass),
Headers: canonicalHeaders(request.Headers),
BodyBase64: base64.StdEncoding.EncodeToString(request.Body),
ObservedAt: observedAt.Format(time.RFC3339Nano),
EnvelopedAt: now.Format(time.RFC3339Nano),
}
}
func canonicalHeaders(headers http.Header) map[string][]string {
if len(headers) == 0 {
return nil
}
out := map[string][]string{}
for key, values := range headers {
canonicalKey := http.CanonicalHeaderKey(strings.TrimSpace(key))
if canonicalKey == "" || !safeRequestHeader(canonicalKey) {
continue
}
copied := make([]string, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value != "" {
copied = append(copied, value)
}
}
if len(copied) == 0 {
continue
}
sort.Strings(copied)
out[canonicalKey] = copied
}
if len(out) == 0 {
return nil
}
return out
}
@@ -0,0 +1,163 @@
package webingress
import (
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"testing"
"time"
)
func TestDefaultFabricBinderBuildsSignedEnvelopeAndSendsIt(t *testing.T) {
signer := &recordingEnvelopeSigner{
signature: FabricEnvelopeSignature{KeyID: "node-key-1", Alg: "ed25519", Signature: "sig-1", SignedAt: "2026-05-17T00:00:02Z"},
}
sender := &recordingEnvelopeSender{
response: FabricResponse{StatusCode: http.StatusAccepted, Body: []byte(`{"accepted":true}`)},
}
binder := DefaultFabricBinder{Signer: signer, Sender: sender, Now: fixedEnvelopeNow}
response, err := binder.Forward(context.Background(), FabricRequest{
SchemaVersion: "rap.web_ingress.fabric_request.v1",
Method: "post",
Path: "/platform-admin/root",
Query: "tab=nodes",
Host: "admin.example.test",
ServiceType: "admin-ingress",
Scope: "platform",
ServiceClass: "platform_admin",
Headers: http.Header{
"X-Trace-Id": []string{"trace-b", "trace-a"},
"Authorization": []string{"Bearer should-not-forward"},
"X-Empty-Header": []string{" "},
},
Body: []byte(`{"hello":"world"}`),
ObservedAt: fixedNow(),
})
if err != nil {
t.Fatalf("Forward failed: %v", err)
}
if response.StatusCode != http.StatusAccepted {
t.Fatalf("response = %+v", response)
}
if len(signer.canonical) == 0 {
t.Fatal("signer did not receive canonical envelope")
}
if !bytes.Equal(sender.envelope.Canonical, signer.canonical) {
t.Fatalf("sender canonical does not match signer canonical")
}
if sender.envelope.SchemaVersion != "rap.web_ingress.signed_fabric_service_channel_envelope.v1" {
t.Fatalf("signed schema = %q", sender.envelope.SchemaVersion)
}
if sender.envelope.Signature.KeyID != "node-key-1" || sender.envelope.Signature.Signature != "sig-1" {
t.Fatalf("signature = %+v", sender.envelope.Signature)
}
var canonical FabricServiceChannelEnvelope
if err := json.Unmarshal(signer.canonical, &canonical); err != nil {
t.Fatalf("decode canonical: %v", err)
}
if canonical.SchemaVersion != FabricServiceChannelEnvelopeSchema ||
canonical.RequestSchema != "rap.web_ingress.fabric_request.v1" ||
canonical.Method != http.MethodPost ||
canonical.Scope != "platform" ||
canonical.ServiceClass != "platform_admin" ||
canonical.BodyBase64 != "eyJoZWxsbyI6IndvcmxkIn0=" ||
canonical.ObservedAt != "2026-05-17T00:00:00Z" ||
canonical.EnvelopedAt != "2026-05-17T00:00:01Z" {
t.Fatalf("canonical envelope = %+v", canonical)
}
if got := canonical.Headers["X-Trace-Id"]; len(got) != 2 || got[0] != "trace-a" || got[1] != "trace-b" {
t.Fatalf("canonical headers = %#v", canonical.Headers)
}
if canonical.Headers["Authorization"] != nil || canonical.Headers["X-Empty-Header"] != nil {
t.Fatalf("unsafe/empty headers leaked: %#v", canonical.Headers)
}
}
func TestDefaultFabricBinderRequiresSignerAndSender(t *testing.T) {
request := FabricRequest{Scope: "platform", ServiceClass: "platform_admin"}
_, err := (DefaultFabricBinder{Sender: &recordingEnvelopeSender{}}).Forward(context.Background(), request)
if !errors.Is(err, ErrFabricEnvelopeSignerRequired) {
t.Fatalf("signer error = %v", err)
}
_, err = (DefaultFabricBinder{Signer: &recordingEnvelopeSigner{}}).Forward(context.Background(), request)
if !errors.Is(err, ErrFabricEnvelopeSenderRequired) {
t.Fatalf("sender error = %v", err)
}
}
func TestDefaultFabricBinderRequiresScopeAndServiceClass(t *testing.T) {
binder := DefaultFabricBinder{Signer: &recordingEnvelopeSigner{}, Sender: &recordingEnvelopeSender{}}
_, err := binder.Forward(context.Background(), FabricRequest{ServiceClass: "platform_admin"})
if !errors.Is(err, ErrFabricEnvelopeScopeRequired) {
t.Fatalf("scope error = %v", err)
}
_, err = binder.Forward(context.Background(), FabricRequest{Scope: "platform"})
if !errors.Is(err, ErrFabricEnvelopeClassRequired) {
t.Fatalf("class error = %v", err)
}
}
func TestDefaultFabricBinderPropagatesSignerAndSenderFailures(t *testing.T) {
signerErr := errors.New("sign failed")
senderErr := errors.New("send failed")
request := FabricRequest{Scope: "platform", ServiceClass: "platform_admin"}
_, err := (DefaultFabricBinder{
Signer: &recordingEnvelopeSigner{err: signerErr},
Sender: &recordingEnvelopeSender{},
}).Forward(context.Background(), request)
if !errors.Is(err, signerErr) {
t.Fatalf("signer error = %v", err)
}
_, err = (DefaultFabricBinder{
Signer: &recordingEnvelopeSigner{},
Sender: &recordingEnvelopeSender{err: senderErr},
}).Forward(context.Background(), request)
if !errors.Is(err, senderErr) {
t.Fatalf("sender error = %v", err)
}
}
func fixedEnvelopeNow() time.Time {
return time.Date(2026, 5, 17, 0, 0, 1, 0, time.UTC)
}
type recordingEnvelopeSigner struct {
canonical []byte
signature FabricEnvelopeSignature
err error
}
func (s *recordingEnvelopeSigner) Sign(_ context.Context, canonical []byte) (FabricEnvelopeSignature, error) {
s.canonical = append([]byte{}, canonical...)
if s.err != nil {
return FabricEnvelopeSignature{}, s.err
}
if s.signature.KeyID == "" {
s.signature = FabricEnvelopeSignature{KeyID: "test-key", Alg: "ed25519", Signature: "test-signature"}
}
return s.signature, nil
}
type recordingEnvelopeSender struct {
envelope SignedFabricServiceChannelEnvelope
response FabricResponse
err error
}
func (s *recordingEnvelopeSender) Send(_ context.Context, envelope SignedFabricServiceChannelEnvelope) (FabricResponse, error) {
s.envelope = envelope
if s.err != nil {
return FabricResponse{}, s.err
}
return s.response, nil
}
@@ -0,0 +1,64 @@
package webingress
import (
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
)
type TrustedKeyConfig struct {
KeyID string `json:"key_id"`
PublicKey string `json:"public_key"`
}
func ParseTrustedKeysJSON(value string) (StaticEnvelopeKeyResolver, error) {
value = strings.TrimSpace(value)
if value == "" {
return nil, nil
}
resolver := StaticEnvelopeKeyResolver{}
var byID map[string]string
if err := json.Unmarshal([]byte(value), &byID); err == nil && len(byID) > 0 {
for keyID, publicKeyB64 := range byID {
if err := resolver.addBase64(keyID, publicKeyB64); err != nil {
return nil, err
}
}
return resolver, nil
}
var list []TrustedKeyConfig
if err := json.Unmarshal([]byte(value), &list); err != nil {
return nil, fmt.Errorf("%w: trusted keys json must be object or array", ErrFabricEnvelopeSignatureInvalid)
}
for _, item := range list {
if err := resolver.addBase64(item.KeyID, item.PublicKey); err != nil {
return nil, err
}
}
return resolver, nil
}
func (r StaticEnvelopeKeyResolver) addBase64(keyID string, publicKeyB64 string) error {
keyID = strings.TrimSpace(keyID)
if keyID == "" {
return fmt.Errorf("%w: trusted key id required", ErrFabricEnvelopeSignatureInvalid)
}
decoded, err := decodeEnvelopeBase64(strings.TrimSpace(publicKeyB64))
if err != nil {
return fmt.Errorf("%w: trusted public key must be base64 encoded", ErrFabricEnvelopeSignatureInvalid)
}
if len(decoded) != ed25519.PublicKeySize {
return fmt.Errorf("%w: trusted public key must decode to %d bytes", ErrFabricEnvelopeSignatureInvalid, ed25519.PublicKeySize)
}
r[keyID] = append(ed25519.PublicKey(nil), decoded...)
return nil
}
func TrustedKeysJSONForPublicKey(keyID string, publicKey ed25519.PublicKey) string {
payload, _ := json.Marshal(map[string]string{
strings.TrimSpace(keyID): base64.StdEncoding.EncodeToString(publicKey),
})
return string(payload)
}
@@ -0,0 +1,64 @@
package webingress
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"errors"
"testing"
)
func TestParseTrustedKeysJSONAcceptsMapAndArray(t *testing.T) {
publicKey, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
keyB64 := base64.StdEncoding.EncodeToString(publicKey)
resolver, err := ParseTrustedKeysJSON(`{"key-1":"` + keyB64 + `"}`)
if err != nil {
t.Fatalf("parse map: %v", err)
}
if got, ok, err := resolver.PublicKey(nil, "key-1"); err != nil || !ok || string(got) != string(publicKey) {
t.Fatalf("map resolver got=%x ok=%t err=%v", got, ok, err)
}
resolver, err = ParseTrustedKeysJSON(`[{"key_id":"key-2","public_key":"` + keyB64 + `"}]`)
if err != nil {
t.Fatalf("parse array: %v", err)
}
if _, ok, err := resolver.PublicKey(nil, "key-2"); err != nil || !ok {
t.Fatalf("array resolver ok=%t err=%v", ok, err)
}
}
func TestParseTrustedKeysJSONRejectsInvalidKeys(t *testing.T) {
_, err := ParseTrustedKeysJSON(`{"":"abc"}`)
if !errors.Is(err, ErrFabricEnvelopeSignatureInvalid) {
t.Fatalf("empty key err = %v", err)
}
_, err = ParseTrustedKeysJSON(`{"key-1":"abc"}`)
if !errors.Is(err, ErrFabricEnvelopeSignatureInvalid) {
t.Fatalf("bad public key err = %v", err)
}
_, err = ParseTrustedKeysJSON(`not-json`)
if !errors.Is(err, ErrFabricEnvelopeSignatureInvalid) {
t.Fatalf("bad json err = %v", err)
}
}
func TestTrustedKeysJSONForPublicKey(t *testing.T) {
publicKey, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
resolver, err := ParseTrustedKeysJSON(TrustedKeysJSONForPublicKey("key-1", publicKey))
if err != nil {
t.Fatalf("parse generated json: %v", err)
}
if _, ok, err := resolver.PublicKey(nil, "key-1"); err != nil || !ok {
t.Fatalf("generated resolver ok=%t err=%v", ok, err)
}
}
@@ -0,0 +1,182 @@
package webingress
import (
"context"
"crypto/tls"
"errors"
"net"
"net/http"
"strings"
"sync"
"time"
)
type ListenerConfig struct {
RuntimeConfig
HTTPAddr string
HTTPSAddr string
TLSCertFile string
TLSKeyFile string
Binder FabricBinder
}
type ListenerStatus struct {
SchemaVersion string `json:"schema_version"`
Running bool `json:"running"`
HTTPRunning bool `json:"http_running"`
HTTPSRunning bool `json:"https_running"`
HTTPAddr string `json:"http_addr,omitempty"`
HTTPSAddr string `json:"https_addr,omitempty"`
Reason string `json:"reason,omitempty"`
Errors []string `json:"errors,omitempty"`
ObservedAt string `json:"observed_at"`
}
type Manager struct {
mu sync.Mutex
http *http.Server
https *http.Server
status ListenerStatus
now func() time.Time
}
func NewManager() *Manager {
return &Manager{now: time.Now}
}
func (m *Manager) Apply(ctx context.Context, cfg ListenerConfig) ListenerStatus {
m.mu.Lock()
defer m.mu.Unlock()
_ = m.stopLocked(ctx)
runtime := Runtime{Config: cfg.RuntimeConfig, Binder: cfg.Binder, Now: m.now}
status := ListenerStatus{
SchemaVersion: "rap.web_ingress.listener_status.v1",
Reason: "started",
ObservedAt: m.observedAt(),
}
errorsOut := []string{}
if strings.TrimSpace(cfg.HTTPAddr) == "" {
cfg.HTTPAddr = ":80"
}
if strings.TrimSpace(cfg.HTTPSAddr) == "" {
cfg.HTTPSAddr = ":443"
}
if server, addr, err := startHTTPServer(ctx, cfg.HTTPAddr, runtime.HTTPHandler()); err == nil {
m.http = server
status.HTTPRunning = true
status.HTTPAddr = addr
} else {
errorsOut = append(errorsOut, "http:"+err.Error())
}
if cfg.TLSCertFile == "" || cfg.TLSKeyFile == "" {
errorsOut = append(errorsOut, "https:tls_cert_file_and_key_file_required")
} else if server, addr, err := startHTTPSServer(ctx, cfg.HTTPSAddr, cfg.TLSCertFile, cfg.TLSKeyFile, runtime.HTTPSHandler()); err == nil {
m.https = server
status.HTTPSRunning = true
status.HTTPSAddr = addr
} else {
errorsOut = append(errorsOut, "https:"+err.Error())
}
status.Running = status.HTTPRunning || status.HTTPSRunning
if len(errorsOut) > 0 {
status.Errors = errorsOut
if status.Running {
status.Reason = "partial"
} else {
status.Reason = "blocked"
}
}
m.status = status
return status
}
func (m *Manager) Stop(ctx context.Context) ListenerStatus {
m.mu.Lock()
defer m.mu.Unlock()
_ = m.stopLocked(ctx)
m.status = ListenerStatus{
SchemaVersion: "rap.web_ingress.listener_status.v1",
Reason: "stopped",
ObservedAt: m.observedAt(),
}
return m.status
}
func (m *Manager) Status() ListenerStatus {
m.mu.Lock()
defer m.mu.Unlock()
if m.status.SchemaVersion == "" {
return ListenerStatus{
SchemaVersion: "rap.web_ingress.listener_status.v1",
Reason: "not_started",
ObservedAt: m.observedAt(),
}
}
return m.status
}
func (m *Manager) stopLocked(ctx context.Context) error {
var out error
if m.http != nil {
out = errors.Join(out, m.http.Shutdown(ctx))
m.http = nil
}
if m.https != nil {
out = errors.Join(out, m.https.Shutdown(ctx))
m.https = nil
}
return out
}
func (m *Manager) observedAt() string {
now := time.Now().UTC()
if m.now != nil {
now = m.now().UTC()
}
return now.Format(time.RFC3339Nano)
}
func startHTTPServer(ctx context.Context, addr string, handler http.Handler) (*http.Server, string, error) {
listener, err := net.Listen("tcp", addr)
if err != nil {
return nil, "", err
}
server := &http.Server{Handler: handler, ReadHeaderTimeout: 5 * time.Second}
go func() {
<-ctx.Done()
_ = server.Shutdown(context.Background())
}()
go func() {
if err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
_ = server.Close()
}
}()
return server, listener.Addr().String(), nil
}
func startHTTPSServer(ctx context.Context, addr, certFile, keyFile string, handler http.Handler) (*http.Server, string, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, "", err
}
listener, err := net.Listen("tcp", addr)
if err != nil {
return nil, "", err
}
server := &http.Server{
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{cert}},
}
go func() {
<-ctx.Done()
_ = server.Shutdown(context.Background())
}()
go func() {
if err := server.ServeTLS(listener, "", ""); err != nil && !errors.Is(err, http.ErrServerClosed) {
_ = server.Close()
}
}()
return server, listener.Addr().String(), nil
}
@@ -0,0 +1,105 @@
package webingress
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestManagerStartsHTTPRedirectAndStops(t *testing.T) {
manager := NewManager()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
status := manager.Apply(ctx, ListenerConfig{
RuntimeConfig: RuntimeConfig{ServiceType: "admin-ingress", Scope: "platform", ServiceClasses: []string{"platform_admin"}},
HTTPAddr: "127.0.0.1:0",
HTTPSAddr: "127.0.0.1:0",
})
if !status.HTTPRunning || status.HTTPSRunning || !status.Running || status.HTTPAddr == "" {
t.Fatalf("status = %+v", status)
}
if status.Reason != "partial" || !containsError(status.Errors, "https:tls_cert_file_and_key_file_required") {
t.Fatalf("status = %+v", status)
}
client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
resp, err := client.Get("http://" + status.HTTPAddr + "/cluster-admin")
if err != nil {
t.Fatalf("http get: %v", err)
}
_ = resp.Body.Close()
if resp.StatusCode != http.StatusPermanentRedirect {
t.Fatalf("status = %d", resp.StatusCode)
}
stopped := manager.Stop(context.Background())
if stopped.Running || stopped.Reason != "stopped" {
t.Fatalf("stopped = %+v", stopped)
}
}
func TestManagerStartsHTTPSWhenCertificateProvided(t *testing.T) {
dir := t.TempDir()
certFile, keyFile := writeSelfSignedCert(t, dir)
manager := NewManager()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
status := manager.Apply(ctx, ListenerConfig{
RuntimeConfig: RuntimeConfig{ServiceType: "admin-ingress", Scope: "platform", ServiceClasses: []string{"platform_admin"}},
HTTPAddr: "127.0.0.1:0",
HTTPSAddr: "127.0.0.1:0",
TLSCertFile: certFile,
TLSKeyFile: keyFile,
})
if !status.HTTPRunning || !status.HTTPSRunning || status.HTTPAddr == "" || status.HTTPSAddr == "" || len(status.Errors) != 0 {
t.Fatalf("status = %+v", status)
}
}
func writeSelfSignedCert(t *testing.T, dir string) (string, string) {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("generate key: %v", err)
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "localhost"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
DNSNames: []string{"localhost"},
}
der, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
t.Fatalf("create cert: %v", err)
}
certFile := filepath.Join(dir, "cert.pem")
keyFile := filepath.Join(dir, "key.pem")
if err := os.WriteFile(certFile, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o600); err != nil {
t.Fatalf("write cert: %v", err)
}
if err := os.WriteFile(keyFile, pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}), 0o600); err != nil {
t.Fatalf("write key: %v", err)
}
return certFile, keyFile
}
func containsError(values []string, needle string) bool {
for _, value := range values {
if value == needle || strings.Contains(value, needle) {
return true
}
}
return false
}
@@ -0,0 +1,217 @@
package webingress
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/example/remote-access-platform/agents/rap-node-agent/internal/mesh"
)
var (
ErrMeshEnvelopeRuntimeRequired = errors.New("web ingress mesh envelope runtime required")
ErrMeshEnvelopeRouteRequired = errors.New("web ingress mesh envelope route set required")
ErrMeshEnvelopeIdentityInvalid = errors.New("web ingress mesh envelope identity invalid")
)
type FabricChannelReliableRuntime interface {
SendReliable(ctx context.Context, spec mesh.FabricChannelSpec, routeSet mesh.FabricRouteSet, payloads [][]byte) (mesh.FabricChannelRuntimeResult, error)
}
type FabricChannelRequestResponseRuntime interface {
SendRequestResponse(ctx context.Context, spec mesh.FabricChannelSpec, routeSet mesh.FabricRouteSet, payload []byte) (mesh.FabricChannelRequestResponseResult, error)
}
type MeshEnvelopeSender struct {
Runtime FabricChannelReliableRuntime
ResponseRuntime FabricChannelRequestResponseRuntime
RouteSet mesh.FabricRouteSet
ClusterID string
SourceNodeID string
TargetKind mesh.FabricChannelTargetKind
TargetID string
ChannelID string
Now func() time.Time
}
type MeshEnvelopeDeliveryResponse struct {
SchemaVersion string `json:"schema_version"`
Status string `json:"status"`
ChannelID string `json:"channel_id"`
RouteID string `json:"route_id,omitempty"`
TargetNode string `json:"target_node,omitempty"`
BytesSent uint64 `json:"bytes_sent"`
FramesSent uint64 `json:"frames_sent"`
AcksReceived uint64 `json:"acks_received"`
MigrationEvents int `json:"migration_events"`
}
func (s MeshEnvelopeSender) Send(ctx context.Context, envelope SignedFabricServiceChannelEnvelope) (FabricResponse, error) {
if s.Runtime == nil && s.ResponseRuntime == nil {
return FabricResponse{}, ErrMeshEnvelopeRuntimeRequired
}
if strings.TrimSpace(s.RouteSet.Primary.RouteID) == "" && len(s.RouteSet.WarmStandby) == 0 && len(s.RouteSet.ColdFallbacks) == 0 {
return FabricResponse{}, ErrMeshEnvelopeRouteRequired
}
spec, err := s.channelSpec(envelope)
if err != nil {
return FabricResponse{}, err
}
payload, err := json.Marshal(envelope)
if err != nil {
return FabricResponse{}, err
}
if s.ResponseRuntime != nil {
result, err := s.ResponseRuntime.SendRequestResponse(ctx, spec, s.routeSet(spec), payload)
if err != nil {
return FabricResponse{}, err
}
responsePayload, err := unwrapWebIngressForwardResponse(result.ResponsePayload)
if err != nil {
return FabricResponse{}, err
}
if response, ok := decodeRuntimeHTTPResponse(responsePayload); ok {
return response, nil
}
return acceptedDeliveryResponse(spec.ChannelID, result.FabricChannelRuntimeResult)
}
result, err := s.Runtime.SendReliable(ctx, spec, s.routeSet(spec), [][]byte{payload})
if err != nil {
return FabricResponse{}, err
}
return acceptedDeliveryResponse(spec.ChannelID, result)
}
func unwrapWebIngressForwardResponse(payload []byte) ([]byte, error) {
var response struct {
Payload json.RawMessage `json:"payload,omitempty"`
Error string `json:"error,omitempty"`
}
if len(payload) == 0 || json.Unmarshal(payload, &response) != nil {
return payload, nil
}
if strings.TrimSpace(response.Error) != "" {
return nil, fmt.Errorf("%w: %s", ErrMeshEnvelopeRuntimeRequired, response.Error)
}
if len(response.Payload) == 0 {
return payload, nil
}
return append([]byte(nil), response.Payload...), nil
}
func acceptedDeliveryResponse(channelID string, result mesh.FabricChannelRuntimeResult) (FabricResponse, error) {
response, err := json.Marshal(MeshEnvelopeDeliveryResponse{
SchemaVersion: "rap.web_ingress.mesh_envelope_delivery_response.v1",
Status: "accepted",
ChannelID: channelID,
RouteID: result.Channel.RouteID,
TargetNode: result.Channel.TargetNode,
BytesSent: result.BytesSent,
FramesSent: result.FramesSent,
AcksReceived: result.AcksReceived,
MigrationEvents: result.MigrationEvents,
})
if err != nil {
return FabricResponse{}, err
}
return FabricResponse{
StatusCode: http.StatusAccepted,
Headers: http.Header{"Content-Type": []string{"application/json"}},
Body: response,
}, nil
}
func decodeRuntimeHTTPResponse(payload []byte) (FabricResponse, bool) {
var response struct {
SchemaVersion string `json:"schema_version"`
StatusCode int `json:"status_code"`
Headers map[string][]string `json:"headers,omitempty"`
BodyBase64 string `json:"body_b64,omitempty"`
Body string `json:"body,omitempty"`
}
if len(payload) == 0 || json.Unmarshal(payload, &response) != nil {
return FabricResponse{}, false
}
if response.SchemaVersion != FabricRuntimeResponseSchema {
return FabricResponse{}, false
}
body := []byte(response.Body)
if response.BodyBase64 != "" {
decoded, err := decodeEnvelopeBase64(response.BodyBase64)
if err != nil {
return FabricResponse{}, false
}
body = decoded
}
headers := http.Header{}
for key, values := range response.Headers {
if !safeResponseHeader(key) {
continue
}
for _, value := range values {
headers.Add(key, value)
}
}
return FabricResponse{StatusCode: response.StatusCode, Headers: headers, Body: body}, true
}
func (s MeshEnvelopeSender) channelSpec(envelope SignedFabricServiceChannelEnvelope) (mesh.FabricChannelSpec, error) {
clusterID := strings.TrimSpace(s.ClusterID)
sourceNodeID := strings.TrimSpace(s.SourceNodeID)
targetID := strings.TrimSpace(s.TargetID)
if clusterID == "" || sourceNodeID == "" || targetID == "" {
return mesh.FabricChannelSpec{}, ErrMeshEnvelopeIdentityInvalid
}
targetKind := s.TargetKind
if targetKind == "" {
targetKind = mesh.FabricChannelTargetPool
}
channelID := strings.TrimSpace(s.ChannelID)
if channelID == "" {
channelID = defaultMeshEnvelopeChannelID(envelope, s.now())
}
spec := mesh.FabricChannelSpec{
ChannelID: channelID,
ClusterID: clusterID,
SourceNodeID: sourceNodeID,
TargetKind: targetKind,
TargetID: targetID,
TrafficClass: "control",
StickyKey: envelope.Envelope.Scope + ":" + envelope.Envelope.ServiceClass,
CreatedAt: s.now(),
}
if err := mesh.ValidateFabricChannelSpec(spec); err != nil {
return mesh.FabricChannelSpec{}, err
}
return spec, nil
}
func (s MeshEnvelopeSender) routeSet(spec mesh.FabricChannelSpec) mesh.FabricRouteSet {
routeSet := s.RouteSet
if routeSet.TargetKind == "" {
routeSet.TargetKind = spec.TargetKind
}
if strings.TrimSpace(routeSet.TargetID) == "" {
routeSet.TargetID = spec.TargetID
}
return routeSet
}
func (s MeshEnvelopeSender) now() time.Time {
if s.Now != nil {
return s.Now().UTC()
}
return time.Now().UTC()
}
func defaultMeshEnvelopeChannelID(envelope SignedFabricServiceChannelEnvelope, now time.Time) string {
serviceClass := strings.ReplaceAll(strings.TrimSpace(envelope.Envelope.ServiceClass), "_", "-")
if serviceClass == "" {
serviceClass = "web-ingress"
}
return fmt.Sprintf("web-ingress-%s-%d", serviceClass, now.UnixNano())
}
@@ -0,0 +1,267 @@
package webingress
import (
"context"
"encoding/json"
"errors"
"net/http"
"testing"
"github.com/example/remote-access-platform/agents/rap-node-agent/internal/mesh"
)
func TestMeshEnvelopeSenderSendsSignedEnvelopeOverReliableFabricRuntime(t *testing.T) {
runtime := &recordingReliableRuntime{
result: mesh.FabricChannelRuntimeResult{
Channel: mesh.FabricChannel{RouteID: "route-fast", TargetNode: "node-runtime"},
BytesSent: 123,
FramesSent: 1,
AcksReceived: 1,
},
}
sender := MeshEnvelopeSender{
Runtime: runtime,
RouteSet: testWebIngressRouteSet(),
ClusterID: "cluster-1",
SourceNodeID: "node-ingress",
TargetKind: mesh.FabricChannelTargetPool,
TargetID: "pool-admin-runtime",
ChannelID: "channel-web-1",
Now: fixedEnvelopeNow,
}
envelope := SignedFabricServiceChannelEnvelope{
SchemaVersion: "rap.web_ingress.signed_fabric_service_channel_envelope.v1",
Envelope: FabricServiceChannelEnvelope{
SchemaVersion: FabricServiceChannelEnvelopeSchema,
Scope: "platform",
ServiceClass: "platform_admin",
},
Signature: FabricEnvelopeSignature{KeyID: "node-key", Alg: "ed25519", Signature: "sig"},
}
response, err := sender.Send(context.Background(), envelope)
if err != nil {
t.Fatalf("send: %v", err)
}
if response.StatusCode != http.StatusAccepted || response.Headers.Get("Content-Type") != "application/json" {
t.Fatalf("response = %+v", response)
}
if runtime.spec.ChannelID != "channel-web-1" ||
runtime.spec.ClusterID != "cluster-1" ||
runtime.spec.SourceNodeID != "node-ingress" ||
runtime.spec.TargetID != "pool-admin-runtime" ||
runtime.spec.TargetKind != mesh.FabricChannelTargetPool ||
runtime.spec.TrafficClass != "control" ||
runtime.spec.StickyKey != "platform:platform_admin" {
t.Fatalf("spec = %+v", runtime.spec)
}
if runtime.routeSet.TargetID != "pool-admin-runtime" || len(runtime.payloads) != 1 {
t.Fatalf("route/payload = %+v payloads=%d", runtime.routeSet, len(runtime.payloads))
}
var delivered SignedFabricServiceChannelEnvelope
if err := json.Unmarshal(runtime.payloads[0], &delivered); err != nil {
t.Fatalf("decode delivered envelope: %v", err)
}
if delivered.Signature.Signature != "sig" || delivered.Envelope.ServiceClass != "platform_admin" {
t.Fatalf("delivered = %+v", delivered)
}
var body MeshEnvelopeDeliveryResponse
if err := json.Unmarshal(response.Body, &body); err != nil {
t.Fatalf("decode response: %v", err)
}
if body.SchemaVersion != "rap.web_ingress.mesh_envelope_delivery_response.v1" ||
body.Status != "accepted" ||
body.RouteID != "route-fast" ||
body.AcksReceived != 1 {
t.Fatalf("body = %+v", body)
}
}
func TestMeshEnvelopeSenderReturnsRuntimeHTTPResponse(t *testing.T) {
runtime := &recordingRequestResponseRuntime{
result: mesh.FabricChannelRequestResponseResult{
FabricChannelRuntimeResult: mesh.FabricChannelRuntimeResult{
Channel: mesh.FabricChannel{RouteID: "route-runtime", TargetNode: "node-runtime"},
BytesSent: 123,
BytesRecv: 16,
FramesSent: 1,
FramesRecv: 1,
AcksReceived: 1,
},
ResponsePayload: []byte(`{"payload":{"schema_version":"rap.web_ingress.fabric_runtime_response.v1","status_code":201,"headers":{"X-RAP-Runtime":["ok"],"Set-Cookie":["blocked"]},"body_b64":"eyJvayI6dHJ1ZX0="}}`),
},
}
sender := MeshEnvelopeSender{
ResponseRuntime: runtime,
RouteSet: testWebIngressRouteSet(),
ClusterID: "cluster-1",
SourceNodeID: "node-ingress",
TargetKind: mesh.FabricChannelTargetPool,
TargetID: "pool-admin-runtime",
ChannelID: "channel-web-1",
Now: fixedEnvelopeNow,
}
response, err := sender.Send(context.Background(), SignedFabricServiceChannelEnvelope{
SchemaVersion: "rap.web_ingress.signed_fabric_service_channel_envelope.v1",
Envelope: FabricServiceChannelEnvelope{SchemaVersion: FabricServiceChannelEnvelopeSchema, Scope: "platform", ServiceClass: "platform_admin"},
Signature: FabricEnvelopeSignature{KeyID: "node-key", Alg: "ed25519", Signature: "sig"},
})
if err != nil {
t.Fatalf("send: %v", err)
}
if response.StatusCode != http.StatusCreated || response.Headers.Get("X-RAP-Runtime") != "ok" || response.Headers.Get("Set-Cookie") != "" || string(response.Body) != `{"ok":true}` {
t.Fatalf("response = %+v body=%s", response, string(response.Body))
}
if runtime.spec.ChannelID != "channel-web-1" || len(runtime.payload) == 0 {
t.Fatalf("runtime spec=%+v payload=%s", runtime.spec, string(runtime.payload))
}
}
func TestMeshEnvelopeSenderReportsWrappedRuntimeError(t *testing.T) {
sender := MeshEnvelopeSender{
ResponseRuntime: &recordingRequestResponseRuntime{
result: mesh.FabricChannelRequestResponseResult{ResponsePayload: []byte(`{"error":"runtime unavailable"}`)},
},
RouteSet: testWebIngressRouteSet(),
ClusterID: "cluster-1",
SourceNodeID: "node-ingress",
TargetID: "pool-admin-runtime",
ChannelID: "channel-web-1",
}
_, err := sender.Send(context.Background(), SignedFabricServiceChannelEnvelope{
Envelope: FabricServiceChannelEnvelope{Scope: "platform", ServiceClass: "platform_admin"},
})
if !errors.Is(err, ErrMeshEnvelopeRuntimeRequired) {
t.Fatalf("err = %v", err)
}
}
func TestMeshEnvelopeSenderFallsBackToDeliveryAckForNonHTTPRuntimePayload(t *testing.T) {
runtime := &recordingRequestResponseRuntime{
result: mesh.FabricChannelRequestResponseResult{
FabricChannelRuntimeResult: mesh.FabricChannelRuntimeResult{
Channel: mesh.FabricChannel{RouteID: "route-runtime", TargetNode: "node-runtime"},
BytesSent: 123,
FramesSent: 1,
AcksReceived: 1,
},
ResponsePayload: []byte(`{"not":"http"}`),
},
}
sender := MeshEnvelopeSender{
ResponseRuntime: runtime,
RouteSet: testWebIngressRouteSet(),
ClusterID: "cluster-1",
SourceNodeID: "node-ingress",
TargetID: "pool-admin-runtime",
ChannelID: "channel-web-1",
}
response, err := sender.Send(context.Background(), SignedFabricServiceChannelEnvelope{
Envelope: FabricServiceChannelEnvelope{Scope: "platform", ServiceClass: "platform_admin"},
})
if err != nil {
t.Fatalf("send: %v", err)
}
if response.StatusCode != http.StatusAccepted {
t.Fatalf("response = %+v", response)
}
var body MeshEnvelopeDeliveryResponse
if err := json.Unmarshal(response.Body, &body); err != nil {
t.Fatalf("decode response: %v", err)
}
if body.Status != "accepted" || body.RouteID != "route-runtime" {
t.Fatalf("body = %+v", body)
}
}
func TestMeshEnvelopeSenderReportsRuntimeRouteAndIdentityErrors(t *testing.T) {
_, err := (MeshEnvelopeSender{}).Send(context.Background(), SignedFabricServiceChannelEnvelope{})
if !errors.Is(err, ErrMeshEnvelopeRuntimeRequired) {
t.Fatalf("runtime error = %v", err)
}
_, err = (MeshEnvelopeSender{
Runtime: &recordingReliableRuntime{},
ClusterID: "cluster-1",
SourceNodeID: "node-ingress",
TargetID: "pool-admin-runtime",
}).Send(context.Background(), SignedFabricServiceChannelEnvelope{})
if !errors.Is(err, ErrMeshEnvelopeRouteRequired) {
t.Fatalf("route error = %v", err)
}
_, err = (MeshEnvelopeSender{
Runtime: &recordingReliableRuntime{},
RouteSet: testWebIngressRouteSet(),
}).Send(context.Background(), SignedFabricServiceChannelEnvelope{})
if !errors.Is(err, ErrMeshEnvelopeIdentityInvalid) {
t.Fatalf("identity error = %v", err)
}
}
func TestMeshEnvelopeSenderPropagatesReliableRuntimeFailure(t *testing.T) {
sendErr := errors.New("send failed")
_, err := (MeshEnvelopeSender{
Runtime: &recordingReliableRuntime{err: sendErr},
RouteSet: testWebIngressRouteSet(),
ClusterID: "cluster-1",
SourceNodeID: "node-ingress",
TargetID: "pool-admin-runtime",
}).Send(context.Background(), SignedFabricServiceChannelEnvelope{})
if !errors.Is(err, sendErr) {
t.Fatalf("send error = %v", err)
}
}
type recordingReliableRuntime struct {
spec mesh.FabricChannelSpec
routeSet mesh.FabricRouteSet
payloads [][]byte
result mesh.FabricChannelRuntimeResult
err error
}
type recordingRequestResponseRuntime struct {
spec mesh.FabricChannelSpec
routeSet mesh.FabricRouteSet
payload []byte
result mesh.FabricChannelRequestResponseResult
err error
}
func (r *recordingRequestResponseRuntime) SendRequestResponse(_ context.Context, spec mesh.FabricChannelSpec, routeSet mesh.FabricRouteSet, payload []byte) (mesh.FabricChannelRequestResponseResult, error) {
r.spec = spec
r.routeSet = routeSet
r.payload = payload
if r.err != nil {
return mesh.FabricChannelRequestResponseResult{}, r.err
}
return r.result, nil
}
func (r *recordingReliableRuntime) SendReliable(_ context.Context, spec mesh.FabricChannelSpec, routeSet mesh.FabricRouteSet, payloads [][]byte) (mesh.FabricChannelRuntimeResult, error) {
r.spec = spec
r.routeSet = routeSet
r.payloads = payloads
if r.err != nil {
return mesh.FabricChannelRuntimeResult{}, r.err
}
return r.result, nil
}
func testWebIngressRouteSet() mesh.FabricRouteSet {
return mesh.FabricRouteSet{
Primary: mesh.FabricRoute{
RouteID: "route-fast",
ClusterID: "cluster-1",
SourceNodeID: "node-ingress",
DestinationNodeID: "node-runtime",
PoolID: "pool-admin-runtime",
Healthy: true,
Capacity: 100,
},
}
}
@@ -0,0 +1,219 @@
package webingress
import (
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
)
const (
SignedFabricServiceChannelEnvelopeSchema = "rap.web_ingress.signed_fabric_service_channel_envelope.v1"
FabricRuntimeResponseSchema = "rap.web_ingress.fabric_runtime_response.v1"
)
var (
ErrFabricEnvelopeSignatureInvalid = errors.New("web ingress fabric envelope signature invalid")
ErrFabricEnvelopeUnauthorized = errors.New("web ingress fabric envelope unauthorized")
ErrFabricEnvelopeRuntimeRequired = errors.New("web ingress fabric runtime handler required")
)
type EnvelopeKeyResolver interface {
PublicKey(ctx context.Context, keyID string) (ed25519.PublicKey, bool, error)
}
type EnvelopeRuntimeHandler interface {
HandleFabricRequest(ctx context.Context, request FabricRequest) (FabricResponse, error)
}
type RuntimeHandlerFunc func(ctx context.Context, request FabricRequest) (FabricResponse, error)
func (f RuntimeHandlerFunc) HandleFabricRequest(ctx context.Context, request FabricRequest) (FabricResponse, error) {
return f(ctx, request)
}
type ReceiverConfig struct {
ServiceType string
Scope string
ServiceClasses []string
MaxClockSkew time.Duration
}
type FabricRuntimeReceiver struct {
Config ReceiverConfig
Keys EnvelopeKeyResolver
Handler EnvelopeRuntimeHandler
Now func() time.Time
}
type StaticEnvelopeKeyResolver map[string]ed25519.PublicKey
func (r StaticEnvelopeKeyResolver) PublicKey(_ context.Context, keyID string) (ed25519.PublicKey, bool, error) {
key, ok := r[strings.TrimSpace(keyID)]
if !ok {
return nil, false, nil
}
return append(ed25519.PublicKey(nil), key...), true, nil
}
func (r FabricRuntimeReceiver) Receive(ctx context.Context, payload []byte) ([]byte, error) {
response, err := r.ReceiveResponse(ctx, payload)
if err != nil {
return nil, err
}
return encodeFabricRuntimeResponse(response)
}
func (r FabricRuntimeReceiver) ReceiveResponse(ctx context.Context, payload []byte) (FabricResponse, error) {
if r.Handler == nil {
return FabricResponse{}, ErrFabricEnvelopeRuntimeRequired
}
var signed SignedFabricServiceChannelEnvelope
if err := json.Unmarshal(payload, &signed); err != nil {
return FabricResponse{}, fmt.Errorf("%w: invalid signed envelope json", ErrFabricEnvelopeSignatureInvalid)
}
if err := r.verify(ctx, signed); err != nil {
return FabricResponse{}, err
}
request, err := requestFromEnvelope(signed.Envelope)
if err != nil {
return FabricResponse{}, err
}
return r.Handler.HandleFabricRequest(ctx, request)
}
func (r FabricRuntimeReceiver) verify(ctx context.Context, signed SignedFabricServiceChannelEnvelope) error {
if signed.SchemaVersion != SignedFabricServiceChannelEnvelopeSchema {
return fmt.Errorf("%w: signed schema mismatch", ErrFabricEnvelopeSignatureInvalid)
}
if signed.Envelope.SchemaVersion != FabricServiceChannelEnvelopeSchema ||
strings.TrimSpace(signed.Envelope.Scope) == "" ||
strings.TrimSpace(signed.Envelope.ServiceClass) == "" {
return fmt.Errorf("%w: envelope contract invalid", ErrFabricEnvelopeSignatureInvalid)
}
if scope := strings.TrimSpace(r.Config.Scope); scope != "" && signed.Envelope.Scope != scope {
return fmt.Errorf("%w: scope mismatch", ErrFabricEnvelopeUnauthorized)
}
if len(r.Config.ServiceClasses) > 0 && !contains(r.Config.ServiceClasses, signed.Envelope.ServiceClass) {
return fmt.Errorf("%w: service class not allowed", ErrFabricEnvelopeUnauthorized)
}
if err := r.verifyClock(signed.Envelope); err != nil {
return err
}
if r.Keys == nil {
return fmt.Errorf("%w: key resolver required", ErrFabricEnvelopeSignatureInvalid)
}
keyID := strings.TrimSpace(signed.Signature.KeyID)
publicKey, ok, err := r.Keys.PublicKey(ctx, keyID)
if err != nil {
return err
}
if !ok || len(publicKey) != ed25519.PublicKeySize {
return fmt.Errorf("%w: signing key not trusted", ErrFabricEnvelopeUnauthorized)
}
if signed.Signature.Alg != "ed25519" {
return fmt.Errorf("%w: algorithm mismatch", ErrFabricEnvelopeSignatureInvalid)
}
signature, err := decodeEnvelopeBase64(signed.Signature.Signature)
if err != nil || len(signature) != ed25519.SignatureSize {
return fmt.Errorf("%w: signature must be base64 ed25519", ErrFabricEnvelopeSignatureInvalid)
}
canonical, err := json.Marshal(signed.Envelope)
if err != nil {
return err
}
if !ed25519.Verify(publicKey, canonical, signature) {
return ErrFabricEnvelopeSignatureInvalid
}
return nil
}
func (r FabricRuntimeReceiver) verifyClock(envelope FabricServiceChannelEnvelope) error {
maxSkew := r.Config.MaxClockSkew
if maxSkew <= 0 {
maxSkew = 5 * time.Minute
}
now := time.Now().UTC()
if r.Now != nil {
now = r.Now().UTC()
}
for _, value := range []string{envelope.ObservedAt, envelope.EnvelopedAt} {
if strings.TrimSpace(value) == "" {
continue
}
parsed, err := time.Parse(time.RFC3339Nano, value)
if err != nil {
return fmt.Errorf("%w: invalid envelope timestamp", ErrFabricEnvelopeSignatureInvalid)
}
if parsed.After(now.Add(maxSkew)) || parsed.Before(now.Add(-maxSkew)) {
return fmt.Errorf("%w: envelope timestamp outside skew", ErrFabricEnvelopeUnauthorized)
}
}
return nil
}
func requestFromEnvelope(envelope FabricServiceChannelEnvelope) (FabricRequest, error) {
body, err := base64.StdEncoding.DecodeString(envelope.BodyBase64)
if err != nil && envelope.BodyBase64 != "" {
return FabricRequest{}, fmt.Errorf("%w: invalid body_b64", ErrFabricEnvelopeSignatureInvalid)
}
observedAt, _ := time.Parse(time.RFC3339Nano, envelope.ObservedAt)
headers := http.Header{}
for key, values := range envelope.Headers {
if !safeRequestHeader(key) {
continue
}
for _, value := range values {
headers.Add(key, value)
}
}
return FabricRequest{
SchemaVersion: envelope.RequestSchema,
Method: envelope.Method,
Path: envelope.Path,
Query: envelope.Query,
Host: envelope.Host,
ServiceType: envelope.ServiceType,
Scope: envelope.Scope,
ServiceClass: envelope.ServiceClass,
Headers: headers,
Body: body,
ObservedAt: observedAt,
}, nil
}
func encodeFabricRuntimeResponse(response FabricResponse) ([]byte, error) {
headers := map[string][]string{}
for key, values := range response.Headers {
if !safeResponseHeader(key) {
continue
}
copied := append([]string(nil), values...)
if len(copied) > 0 {
headers[http.CanonicalHeaderKey(key)] = copied
}
}
payload := struct {
SchemaVersion string `json:"schema_version"`
StatusCode int `json:"status_code"`
Headers map[string][]string `json:"headers,omitempty"`
BodyBase64 string `json:"body_b64,omitempty"`
}{
SchemaVersion: FabricRuntimeResponseSchema,
StatusCode: response.StatusCode,
Headers: headers,
BodyBase64: base64.StdEncoding.EncodeToString(response.Body),
}
if payload.StatusCode < 100 || payload.StatusCode > 599 {
payload.StatusCode = http.StatusOK
}
if len(payload.Headers) == 0 {
payload.Headers = nil
}
return json.Marshal(payload)
}
@@ -0,0 +1,194 @@
package webingress
import (
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"net/http"
"testing"
)
func TestFabricRuntimeReceiverVerifiesEnvelopeAndReturnsRuntimeResponse(t *testing.T) {
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
keyID := ed25519EnvelopeKeyID(publicKey)
receiver := FabricRuntimeReceiver{
Config: ReceiverConfig{ServiceType: "global-admin-runtime", Scope: "platform", ServiceClasses: []string{"platform_admin"}},
Keys: StaticEnvelopeKeyResolver{keyID: publicKey},
Handler: recordingRuntimeHandler{response: FabricResponse{
StatusCode: http.StatusCreated,
Headers: http.Header{"X-RAP-Runtime": []string{"ok"}, "Set-Cookie": []string{"blocked"}},
Body: []byte(`{"ok":true}`),
}},
Now: fixedEnvelopeNow,
}
payload := signedReceiverEnvelope(t, privateKey, keyID, FabricServiceChannelEnvelope{
SchemaVersion: FabricServiceChannelEnvelopeSchema,
RequestSchema: "rap.web_ingress.fabric_request.v1",
Method: http.MethodPost,
Path: "/platform-admin/root",
Query: "tab=nodes",
Host: "admin.example.test",
ServiceType: "admin-ingress",
Scope: "platform",
ServiceClass: "platform_admin",
Headers: map[string][]string{"X-Trace-Id": {"trace-1"}},
BodyBase64: base64.StdEncoding.EncodeToString([]byte(`{"hello":"world"}`)),
ObservedAt: "2026-05-17T00:00:00Z",
EnvelopedAt: "2026-05-17T00:00:01Z",
})
responsePayload, err := receiver.Receive(context.Background(), payload)
if err != nil {
t.Fatalf("receive: %v", err)
}
var response struct {
SchemaVersion string `json:"schema_version"`
StatusCode int `json:"status_code"`
Headers map[string][]string `json:"headers"`
BodyBase64 string `json:"body_b64"`
}
if err := json.Unmarshal(responsePayload, &response); err != nil {
t.Fatalf("decode response: %v", err)
}
if response.SchemaVersion != FabricRuntimeResponseSchema ||
response.StatusCode != http.StatusCreated ||
response.Headers["X-Rap-Runtime"][0] != "ok" ||
response.Headers["Set-Cookie"] != nil ||
response.BodyBase64 != "eyJvayI6dHJ1ZX0=" {
t.Fatalf("response = %+v", response)
}
}
func TestFabricRuntimeReceiverRejectsBadSignatureScopeClassAndStaleEnvelope(t *testing.T) {
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
keyID := ed25519EnvelopeKeyID(publicKey)
receiver := FabricRuntimeReceiver{
Config: ReceiverConfig{Scope: "platform", ServiceClasses: []string{"platform_admin"}},
Keys: StaticEnvelopeKeyResolver{keyID: publicKey},
Handler: recordingRuntimeHandler{},
Now: fixedEnvelopeNow,
}
base := FabricServiceChannelEnvelope{
SchemaVersion: FabricServiceChannelEnvelopeSchema,
RequestSchema: "rap.web_ingress.fabric_request.v1",
Method: http.MethodGet,
Path: "/platform-admin/root",
Scope: "platform",
ServiceClass: "platform_admin",
ObservedAt: "2026-05-17T00:00:00Z",
EnvelopedAt: "2026-05-17T00:00:01Z",
}
badSignature := signedReceiverEnvelope(t, privateKey, keyID, base)
badSignature[len(badSignature)-2] = 'x'
if _, err := receiver.Receive(context.Background(), badSignature); !errors.Is(err, ErrFabricEnvelopeSignatureInvalid) {
t.Fatalf("bad signature err = %v", err)
}
wrongScope := base
wrongScope.Scope = "organization"
if _, err := receiver.Receive(context.Background(), signedReceiverEnvelope(t, privateKey, keyID, wrongScope)); !errors.Is(err, ErrFabricEnvelopeUnauthorized) {
t.Fatalf("wrong scope err = %v", err)
}
wrongClass := base
wrongClass.ServiceClass = "cluster_admin"
if _, err := receiver.Receive(context.Background(), signedReceiverEnvelope(t, privateKey, keyID, wrongClass)); !errors.Is(err, ErrFabricEnvelopeUnauthorized) {
t.Fatalf("wrong class err = %v", err)
}
stale := base
stale.EnvelopedAt = "2026-05-16T00:00:00Z"
if _, err := receiver.Receive(context.Background(), signedReceiverEnvelope(t, privateKey, keyID, stale)); !errors.Is(err, ErrFabricEnvelopeUnauthorized) {
t.Fatalf("stale err = %v", err)
}
}
func TestFabricRuntimeReceiverRequiresTrustedKeyAndHandler(t *testing.T) {
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
keyID := ed25519EnvelopeKeyID(publicKey)
payload := signedReceiverEnvelope(t, privateKey, keyID, FabricServiceChannelEnvelope{
SchemaVersion: FabricServiceChannelEnvelopeSchema,
Scope: "platform",
ServiceClass: "platform_admin",
ObservedAt: "2026-05-17T00:00:00Z",
EnvelopedAt: "2026-05-17T00:00:01Z",
})
_, err = (FabricRuntimeReceiver{Keys: StaticEnvelopeKeyResolver{keyID: publicKey}, Now: fixedEnvelopeNow}).Receive(context.Background(), payload)
if !errors.Is(err, ErrFabricEnvelopeRuntimeRequired) {
t.Fatalf("handler err = %v", err)
}
_, err = (FabricRuntimeReceiver{Handler: recordingRuntimeHandler{}, Now: fixedEnvelopeNow}).Receive(context.Background(), payload)
if !errors.Is(err, ErrFabricEnvelopeSignatureInvalid) {
t.Fatalf("key resolver err = %v", err)
}
_, otherPrivateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate other key: %v", err)
}
untrusted := signedReceiverEnvelope(t, otherPrivateKey, "other-key", FabricServiceChannelEnvelope{
SchemaVersion: FabricServiceChannelEnvelopeSchema,
Scope: "platform",
ServiceClass: "platform_admin",
ObservedAt: "2026-05-17T00:00:00Z",
EnvelopedAt: "2026-05-17T00:00:01Z",
})
_, err = (FabricRuntimeReceiver{Keys: StaticEnvelopeKeyResolver{keyID: publicKey}, Handler: recordingRuntimeHandler{}, Now: fixedEnvelopeNow}).Receive(context.Background(), untrusted)
if !errors.Is(err, ErrFabricEnvelopeUnauthorized) {
t.Fatalf("untrusted key err = %v", err)
}
}
func signedReceiverEnvelope(t *testing.T, privateKey ed25519.PrivateKey, keyID string, envelope FabricServiceChannelEnvelope) []byte {
t.Helper()
canonical, err := json.Marshal(envelope)
if err != nil {
t.Fatalf("marshal envelope: %v", err)
}
payload, err := json.Marshal(SignedFabricServiceChannelEnvelope{
SchemaVersion: SignedFabricServiceChannelEnvelopeSchema,
Envelope: envelope,
Signature: FabricEnvelopeSignature{
KeyID: keyID,
Alg: "ed25519",
Signature: base64.StdEncoding.EncodeToString(ed25519.Sign(privateKey, canonical)),
SignedAt: "2026-05-17T00:00:01Z",
},
})
if err != nil {
t.Fatalf("marshal signed envelope: %v", err)
}
return payload
}
type recordingRuntimeHandler struct {
request FabricRequest
response FabricResponse
err error
}
func (h recordingRuntimeHandler) HandleFabricRequest(_ context.Context, request FabricRequest) (FabricResponse, error) {
h.request = request
if h.err != nil {
return FabricResponse{}, h.err
}
if h.response.StatusCode == 0 {
h.response = FabricResponse{StatusCode: http.StatusOK, Body: []byte(`{"ready":true}`)}
}
return h.response, nil
}
@@ -0,0 +1,243 @@
package webingress
import (
"context"
"encoding/json"
"io"
"net/http"
"strings"
"time"
)
type RuntimeConfig struct {
ServiceType string
Scope string
ServiceClasses []string
TLSMode string
HTTPPort int
HTTPSPort int
}
type Runtime struct {
Config RuntimeConfig
Binder FabricBinder
Now func() time.Time
}
type FabricBinder interface {
Forward(ctx context.Context, request FabricRequest) (FabricResponse, error)
}
type FabricRequest struct {
SchemaVersion string
Method string
Path string
Query string
Host string
ServiceType string
Scope string
ServiceClass string
Headers http.Header
Body []byte
ObservedAt time.Time
}
type FabricResponse struct {
StatusCode int
Headers http.Header
Body []byte
}
type Response struct {
SchemaVersion string `json:"schema_version"`
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
ServiceType string `json:"service_type,omitempty"`
Scope string `json:"scope,omitempty"`
ServiceClass string `json:"service_class,omitempty"`
Allowed []string `json:"allowed_service_classes,omitempty"`
ObservedAt string `json:"observed_at"`
}
func (r Runtime) HTTPHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if strings.HasPrefix(req.URL.Path, "/.well-known/acme-challenge/") {
writeJSON(w, http.StatusNotFound, r.response("not_found", "acme_challenge_backend_not_configured", ""))
return
}
if req.URL.Path == "/healthz" || req.URL.Path == "/readyz" {
writeJSON(w, http.StatusOK, r.response("ready", "http_redirect_runtime_ready", ""))
return
}
target := "https://" + req.Host + req.URL.RequestURI()
w.Header().Set("Location", target)
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusPermanentRedirect)
})
}
func (r Runtime) HTTPSHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/healthz" || req.URL.Path == "/readyz" {
writeJSON(w, http.StatusOK, r.response("ready", "https_runtime_ready", ""))
return
}
serviceClass := strings.TrimSpace(req.Header.Get("X-RAP-Service-Class"))
if serviceClass == "" {
serviceClass = serviceClassFromPath(req.URL.Path)
}
if serviceClass == "" {
writeJSON(w, http.StatusBadRequest, r.response("blocked", "service_class_required", ""))
return
}
if !contains(r.Config.ServiceClasses, serviceClass) {
writeJSON(w, http.StatusForbidden, r.response("blocked", "service_class_not_allowed", serviceClass))
return
}
if r.Binder == nil {
writeJSON(w, http.StatusNotImplemented, r.response("blocked", "fabric_service_channel_binding_not_implemented", serviceClass))
return
}
scope := scopeForServiceClass(serviceClass, r.Config.Scope)
body, err := io.ReadAll(http.MaxBytesReader(w, req.Body, 1<<20))
if err != nil {
writeJSON(w, http.StatusRequestEntityTooLarge, r.response("blocked", "request_body_too_large", serviceClass))
return
}
now := time.Now().UTC()
if r.Now != nil {
now = r.Now().UTC()
}
fabricResponse, err := r.Binder.Forward(req.Context(), FabricRequest{
SchemaVersion: "rap.web_ingress.fabric_request.v1",
Method: req.Method,
Path: req.URL.Path,
Query: req.URL.RawQuery,
Host: req.Host,
ServiceType: strings.TrimSpace(r.Config.ServiceType),
Scope: scope,
ServiceClass: serviceClass,
Headers: cloneSafeHeaders(req.Header),
Body: body,
ObservedAt: now,
})
if err != nil {
writeJSON(w, http.StatusBadGateway, r.response("blocked", "fabric_service_channel_forward_failed", serviceClass))
return
}
writeFabricResponse(w, fabricResponse)
})
}
func (r Runtime) response(status, reason, serviceClass string) Response {
now := time.Now().UTC()
if r.Now != nil {
now = r.Now().UTC()
}
return Response{
SchemaVersion: "rap.web_ingress.runtime_response.v1",
Status: status,
Reason: reason,
ServiceType: strings.TrimSpace(r.Config.ServiceType),
Scope: strings.TrimSpace(r.Config.Scope),
ServiceClass: serviceClass,
Allowed: append([]string{}, r.Config.ServiceClasses...),
ObservedAt: now.Format(time.RFC3339Nano),
}
}
func scopeForServiceClass(serviceClass string, fallback string) string {
switch strings.TrimSpace(serviceClass) {
case "platform_admin":
return "platform"
case "cluster_admin":
return "cluster"
case "organization_portal":
return "organization"
case "user_portal":
return "user"
default:
return strings.TrimSpace(fallback)
}
}
func serviceClassFromPath(path string) string {
path = strings.Trim(strings.ToLower(path), "/")
switch {
case strings.HasPrefix(path, "platform-admin"):
return "platform_admin"
case strings.HasPrefix(path, "cluster-admin"):
return "cluster_admin"
case strings.HasPrefix(path, "organizations/"):
return "organization_portal"
case strings.HasPrefix(path, "users/"):
return "user_portal"
default:
return ""
}
}
func writeJSON(w http.ResponseWriter, status int, payload Response) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}
func writeFabricResponse(w http.ResponseWriter, payload FabricResponse) {
for key, values := range payload.Headers {
if !safeResponseHeader(key) {
continue
}
for _, value := range values {
w.Header().Add(key, value)
}
}
w.Header().Set("Cache-Control", "no-store")
status := payload.StatusCode
if status < 100 || status > 599 {
status = http.StatusOK
}
w.WriteHeader(status)
_, _ = w.Write(payload.Body)
}
func cloneSafeHeaders(headers http.Header) http.Header {
out := http.Header{}
for key, values := range headers {
if !safeRequestHeader(key) {
continue
}
for _, value := range values {
out.Add(key, value)
}
}
return out
}
func safeRequestHeader(key string) bool {
switch strings.ToLower(strings.TrimSpace(key)) {
case "authorization", "cookie", "set-cookie", "x-rap-service-channel-token":
return false
default:
return true
}
}
func safeResponseHeader(key string) bool {
switch strings.ToLower(strings.TrimSpace(key)) {
case "set-cookie", "transfer-encoding", "connection":
return false
default:
return true
}
}
func contains(values []string, needle string) bool {
for _, value := range values {
if value == needle {
return true
}
}
return false
}
@@ -0,0 +1,206 @@
package webingress
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestHTTPHandlerRedirectsToHTTPS(t *testing.T) {
runtime := Runtime{Config: RuntimeConfig{ServiceType: "admin-ingress", Scope: "platform"}}
req := httptest.NewRequest(http.MethodGet, "http://admin.example.test/cluster-admin/dashboard?x=1", nil)
rec := httptest.NewRecorder()
runtime.HTTPHandler().ServeHTTP(rec, req)
if rec.Code != http.StatusPermanentRedirect {
t.Fatalf("status = %d", rec.Code)
}
if rec.Header().Get("Location") != "https://admin.example.test/cluster-admin/dashboard?x=1" {
t.Fatalf("Location = %q", rec.Header().Get("Location"))
}
}
func TestHTTPSHandlerBlocksUnknownServiceClass(t *testing.T) {
runtime := Runtime{
Config: RuntimeConfig{
ServiceType: "public-ingress",
Scope: "organization",
ServiceClasses: []string{"organization_portal", "user_portal"},
},
Now: fixedNow,
}
req := httptest.NewRequest(http.MethodGet, "https://org.example.test/platform-admin/root", nil)
rec := httptest.NewRecorder()
runtime.HTTPSHandler().ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
var payload Response
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode response: %v", err)
}
if payload.Reason != "service_class_not_allowed" || payload.ServiceClass != "platform_admin" || payload.Scope != "organization" {
t.Fatalf("payload = %+v", payload)
}
}
func TestHTTPSHandlerRequiresFabricServiceChannelBinding(t *testing.T) {
runtime := Runtime{
Config: RuntimeConfig{
ServiceType: "admin-ingress",
Scope: "platform",
ServiceClasses: []string{"platform_admin", "cluster_admin"},
},
Now: fixedNow,
}
req := httptest.NewRequest(http.MethodPost, "https://admin.example.test/platform-admin/root", nil)
rec := httptest.NewRecorder()
runtime.HTTPSHandler().ServeHTTP(rec, req)
if rec.Code != http.StatusNotImplemented {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
var payload Response
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode response: %v", err)
}
if payload.Reason != "fabric_service_channel_binding_not_implemented" ||
payload.ServiceClass != "platform_admin" ||
payload.ObservedAt != "2026-05-17T00:00:00Z" {
t.Fatalf("payload = %+v", payload)
}
}
func TestHTTPSHandlerForwardsAllowedRequestToBinder(t *testing.T) {
binder := &recordingBinder{
response: FabricResponse{
StatusCode: http.StatusAccepted,
Headers: http.Header{"X-RAP-Result": []string{"accepted"}},
Body: []byte(`{"ok":true}`),
},
}
runtime := Runtime{
Config: RuntimeConfig{
ServiceType: "admin-ingress",
Scope: "platform",
ServiceClasses: []string{"platform_admin", "cluster_admin"},
},
Binder: binder,
Now: fixedNow,
}
req := httptest.NewRequest(http.MethodPost, "https://admin.example.test/platform-admin/root?tab=nodes", strings.NewReader(`{"hello":"world"}`))
req.Header.Set("X-RAP-Service-Class", "platform_admin")
req.Header.Set("Authorization", "Bearer secret")
req.Header.Set("X-Trace-ID", "trace-1")
rec := httptest.NewRecorder()
runtime.HTTPSHandler().ServeHTTP(rec, req)
if rec.Code != http.StatusAccepted {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
if rec.Header().Get("X-RAP-Result") != "accepted" || rec.Body.String() != `{"ok":true}` {
t.Fatalf("unexpected response headers=%v body=%s", rec.Header(), rec.Body.String())
}
if binder.request.ServiceClass != "platform_admin" ||
binder.request.Scope != "platform" ||
binder.request.Path != "/platform-admin/root" ||
binder.request.Query != "tab=nodes" ||
string(binder.request.Body) != `{"hello":"world"}` {
t.Fatalf("request = %+v", binder.request)
}
if binder.request.Headers.Get("Authorization") != "" || binder.request.Headers.Get("X-Trace-ID") != "trace-1" {
t.Fatalf("headers = %#v", binder.request.Headers)
}
}
func TestHTTPSHandlerDerivesFabricScopeFromServiceClass(t *testing.T) {
binder := &recordingBinder{response: FabricResponse{StatusCode: http.StatusOK}}
runtime := Runtime{
Config: RuntimeConfig{
ServiceType: "admin-ingress",
Scope: "platform",
ServiceClasses: []string{"platform_admin", "cluster_admin"},
},
Binder: binder,
Now: fixedNow,
}
req := httptest.NewRequest(http.MethodGet, "https://admin.example.test/cluster-admin/ui-manifest", nil)
rec := httptest.NewRecorder()
runtime.HTTPSHandler().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
if binder.request.ServiceClass != "cluster_admin" || binder.request.Scope != "cluster" {
t.Fatalf("request = %+v", binder.request)
}
}
func TestHTTPSHandlerReportsBinderFailure(t *testing.T) {
runtime := Runtime{
Config: RuntimeConfig{ServiceType: "admin-ingress", Scope: "platform", ServiceClasses: []string{"platform_admin"}},
Binder: failingBinder{},
Now: fixedNow,
}
req := httptest.NewRequest(http.MethodPost, "https://admin.example.test/platform-admin/root", nil)
rec := httptest.NewRecorder()
runtime.HTTPSHandler().ServeHTTP(rec, req)
if rec.Code != http.StatusBadGateway {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
var payload Response
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode response: %v", err)
}
if payload.Reason != "fabric_service_channel_forward_failed" {
t.Fatalf("payload = %+v", payload)
}
}
func TestHTTPSHandlerHealth(t *testing.T) {
runtime := Runtime{Config: RuntimeConfig{ServiceType: "admin-ingress", Scope: "platform"}, Now: fixedNow}
req := httptest.NewRequest(http.MethodGet, "https://admin.example.test/healthz", nil)
rec := httptest.NewRecorder()
runtime.HTTPSHandler().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
}
func fixedNow() time.Time {
return time.Date(2026, 5, 17, 0, 0, 0, 0, time.UTC)
}
type recordingBinder struct {
request FabricRequest
response FabricResponse
}
func (b *recordingBinder) Forward(_ context.Context, request FabricRequest) (FabricResponse, error) {
b.request = request
return b.response, nil
}
type failingBinder struct{}
func (failingBinder) Forward(context.Context, FabricRequest) (FabricResponse, error) {
return FabricResponse{}, errTestBinderFailure{}
}
type errTestBinderFailure struct{}
func (errTestBinderFailure) Error() string { return "binder failed" }
@@ -0,0 +1,95 @@
package webingress
import (
"context"
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
)
var ErrFabricEnvelopeSigningKeyInvalid = errors.New("web ingress fabric envelope signing key invalid")
type Ed25519EnvelopeSigner struct {
PrivateKey ed25519.PrivateKey
KeyID string
Now func() time.Time
}
func NewEd25519EnvelopeSigner(privateKeyB64, keyID string) (Ed25519EnvelopeSigner, error) {
privateKey, err := decodeEd25519PrivateKey(privateKeyB64)
if err != nil {
return Ed25519EnvelopeSigner{}, err
}
keyID = strings.TrimSpace(keyID)
if keyID == "" {
publicKey, ok := privateKey.Public().(ed25519.PublicKey)
if !ok {
return Ed25519EnvelopeSigner{}, ErrFabricEnvelopeSigningKeyInvalid
}
keyID = ed25519EnvelopeKeyID(publicKey)
}
return Ed25519EnvelopeSigner{PrivateKey: privateKey, KeyID: keyID}, nil
}
func (s Ed25519EnvelopeSigner) Sign(_ context.Context, canonical []byte) (FabricEnvelopeSignature, error) {
if len(s.PrivateKey) != ed25519.PrivateKeySize {
return FabricEnvelopeSignature{}, ErrFabricEnvelopeSigningKeyInvalid
}
if len(canonical) == 0 {
return FabricEnvelopeSignature{}, fmt.Errorf("%w: canonical envelope empty", ErrFabricEnvelopeSigningKeyInvalid)
}
keyID := strings.TrimSpace(s.KeyID)
if keyID == "" {
publicKey, ok := s.PrivateKey.Public().(ed25519.PublicKey)
if !ok {
return FabricEnvelopeSignature{}, ErrFabricEnvelopeSigningKeyInvalid
}
keyID = ed25519EnvelopeKeyID(publicKey)
}
now := time.Now().UTC()
if s.Now != nil {
now = s.Now().UTC()
}
return FabricEnvelopeSignature{
KeyID: keyID,
Alg: "ed25519",
Signature: base64.StdEncoding.EncodeToString(ed25519.Sign(s.PrivateKey, canonical)),
SignedAt: now.Format(time.RFC3339Nano),
}, nil
}
func decodeEd25519PrivateKey(value string) (ed25519.PrivateKey, error) {
decoded, err := decodeEnvelopeBase64(strings.TrimSpace(value))
if err != nil {
return nil, fmt.Errorf("%w: private key must be base64 encoded", ErrFabricEnvelopeSigningKeyInvalid)
}
if len(decoded) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("%w: private key must decode to %d bytes", ErrFabricEnvelopeSigningKeyInvalid, ed25519.PrivateKeySize)
}
return ed25519.PrivateKey(decoded), nil
}
func decodeEnvelopeBase64(value string) ([]byte, error) {
if value == "" {
return nil, errors.New("empty base64 value")
}
decoded, err := base64.StdEncoding.DecodeString(value)
if err == nil {
return decoded, nil
}
decoded, err = base64.RawStdEncoding.DecodeString(value)
if err == nil {
return decoded, nil
}
return base64.RawURLEncoding.DecodeString(value)
}
func ed25519EnvelopeKeyID(publicKey ed25519.PublicKey) string {
sum := sha256.Sum256(publicKey)
return "rap-node-ed25519-" + hex.EncodeToString(sum[:16])
}
@@ -0,0 +1,80 @@
package webingress
import (
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"errors"
"testing"
)
func TestEd25519EnvelopeSignerSignsCanonicalEnvelope(t *testing.T) {
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
signer, err := NewEd25519EnvelopeSigner(base64.StdEncoding.EncodeToString(privateKey), "")
if err != nil {
t.Fatalf("new signer: %v", err)
}
signer.Now = fixedEnvelopeNow
signature, err := signer.Sign(context.Background(), []byte(`{"schema_version":"test"}`))
if err != nil {
t.Fatalf("sign: %v", err)
}
decoded, err := base64.StdEncoding.DecodeString(signature.Signature)
if err != nil {
t.Fatalf("decode signature: %v", err)
}
if !ed25519.Verify(publicKey, []byte(`{"schema_version":"test"}`), decoded) {
t.Fatal("signature did not verify")
}
if signature.KeyID != ed25519EnvelopeKeyID(publicKey) ||
signature.Alg != "ed25519" ||
signature.SignedAt != "2026-05-17T00:00:01Z" {
t.Fatalf("signature metadata = %+v", signature)
}
}
func TestEd25519EnvelopeSignerUsesExplicitKeyID(t *testing.T) {
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
signer, err := NewEd25519EnvelopeSigner(base64.RawStdEncoding.EncodeToString(privateKey), "node-explicit")
if err != nil {
t.Fatalf("new signer: %v", err)
}
signature, err := signer.Sign(context.Background(), []byte(`{}`))
if err != nil {
t.Fatalf("sign: %v", err)
}
if signature.KeyID != "node-explicit" {
t.Fatalf("key id = %q", signature.KeyID)
}
}
func TestEd25519EnvelopeSignerRejectsInvalidKeyAndPayload(t *testing.T) {
_, err := NewEd25519EnvelopeSigner("not-base64", "")
if !errors.Is(err, ErrFabricEnvelopeSigningKeyInvalid) {
t.Fatalf("invalid key error = %v", err)
}
signer := Ed25519EnvelopeSigner{}
_, err = signer.Sign(context.Background(), []byte(`{}`))
if !errors.Is(err, ErrFabricEnvelopeSigningKeyInvalid) {
t.Fatalf("missing key error = %v", err)
}
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
signer = Ed25519EnvelopeSigner{PrivateKey: privateKey}
_, err = signer.Sign(context.Background(), nil)
if !errors.Is(err, ErrFabricEnvelopeSigningKeyInvalid) {
t.Fatalf("empty canonical error = %v", err)
}
}