Files

152 lines
4.7 KiB
Go

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
}