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 }