152 lines
4.7 KiB
Go
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
|
|
}
|