152 lines
4.3 KiB
Go
152 lines
4.3 KiB
Go
package ucp
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// UCP-Agent header parsing (RFC 8941 Dictionary Structured Field)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// ucpAgentProfileKey is the context key for the UCP-Agent profile URI.
|
|
type ucpAgentProfileKey struct{}
|
|
|
|
// UCPAgentProfileFromContext returns the UCP-Agent profile URI stored in the
|
|
// request context, or empty string if none was advertised.
|
|
func UCPAgentProfileFromContext(ctx context.Context) string {
|
|
v, _ := ctx.Value(ucpAgentProfileKey{}).(string)
|
|
return v
|
|
}
|
|
|
|
// parseUCPAgentProfile parses a single "profile" member value from an RFC 8941
|
|
// Dictionary Structured Field header. It only handles the string-value form:
|
|
//
|
|
// UCP-Agent: profile="https://agent.example/profiles/shopping-agent.json"
|
|
//
|
|
// Returns the profile URI (without surrounding quotes) or empty string.
|
|
func parseUCPAgentProfile(header string) string {
|
|
if header == "" {
|
|
return ""
|
|
}
|
|
|
|
// Walk the dictionary entries looking for "profile".
|
|
// RFC 8941 §3.1.2: Dict = ((im-key | key) value) *(SP "," ((im-key | key) value))
|
|
i := 0
|
|
bs := []byte(header)
|
|
n := len(bs)
|
|
|
|
for i < n {
|
|
// Skip whitespace.
|
|
for i < n && (bs[i] == ' ' || bs[i] == '\t') {
|
|
i++
|
|
}
|
|
if i >= n {
|
|
break
|
|
}
|
|
|
|
// Read key (im-key or key). Lowercase letters, digits, "_", "-", "*".
|
|
start := i
|
|
for i < n && isKeyChar(bs[i]) {
|
|
i++
|
|
}
|
|
if i == start {
|
|
// No key found; skip to next comma or end.
|
|
for i < n && bs[i] != ',' {
|
|
i++
|
|
}
|
|
i++ // skip comma
|
|
continue
|
|
}
|
|
key := string(bs[start:i])
|
|
|
|
// Check for "=" separator. RFC 8941 allows bare names (boolean true)
|
|
// when "=? " follows — but we only care about key=value entries.
|
|
if i < n && bs[i] == '=' {
|
|
i++ // skip '='
|
|
|
|
// Read the value. For our case it's a string (starts with '"').
|
|
if i < n && bs[i] == '"' {
|
|
i++ // skip opening quote
|
|
valStart := i
|
|
for i < n && bs[i] != '"' {
|
|
if bs[i] == '\\' {
|
|
i++ // skip escape
|
|
}
|
|
i++
|
|
}
|
|
val := string(bs[valStart:i])
|
|
if i < n {
|
|
i++ // skip closing quote
|
|
}
|
|
if strings.EqualFold(key, "profile") {
|
|
return val
|
|
}
|
|
}
|
|
}
|
|
|
|
// Skip to next comma or end.
|
|
for i < n && bs[i] != ',' {
|
|
i++
|
|
}
|
|
i++ // skip comma
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// isKeyChar reports whether b is valid in an RFC 8941 key (lc-ltr, DIGIT, "_", "-", "*").
|
|
func isKeyChar(b byte) bool {
|
|
return (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9') || b == '_' || b == '-' || b == '*'
|
|
}
|
|
|
|
// WithUCPAgent is HTTP middleware that reads the UCP-Agent header from incoming
|
|
// requests, parses it per RFC 8941, and stores the profile URI in the request
|
|
// context so downstream handlers can inspect the calling platform's identity.
|
|
//
|
|
// Usage:
|
|
//
|
|
// mux.Handle("/ucp/v1/carts", ucp.WithUCPAgent(ucp.CartHandler(pool)))
|
|
func WithUCPAgent(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if profile := parseUCPAgentProfile(r.Header.Get("UCP-Agent")); profile != "" {
|
|
ctx := context.WithValue(r.Context(), ucpAgentProfileKey{}, profile)
|
|
r = r.WithContext(ctx)
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// DefaultUCPAgentProfile is the profile URI sent on outgoing requests from
|
|
// this commerce platform. Override via SetDefaultUCPAgentProfile or the
|
|
// UCP_AGENT_PROFILE environment variable at init time.
|
|
var DefaultUCPAgentProfile = "https://cart.k6n.net/.well-known/ucp"
|
|
|
|
// SetDefaultUCPAgentProfile overrides the default profile URI used for outgoing
|
|
// UCP-Agent headers.
|
|
func SetDefaultUCPAgentProfile(profile string) {
|
|
if profile != "" {
|
|
DefaultUCPAgentProfile = profile
|
|
}
|
|
}
|
|
|
|
// UCPAgentHeaderValue returns the RFC 8941 Dictionary value for the UCP-Agent
|
|
// header, encoding the given profile URI.
|
|
func UCPAgentHeaderValue(profile string) string {
|
|
if profile == "" {
|
|
profile = DefaultUCPAgentProfile
|
|
}
|
|
return `profile="` + profile + `"`
|
|
}
|
|
|
|
// WithUCPAgentOnRequest adds the UCP-Agent header to an outgoing HTTP request.
|
|
// It sets the header to the default profile unless a custom profile is provided.
|
|
func WithUCPAgentOnRequest(req *http.Request, profile ...string) {
|
|
p := DefaultUCPAgentProfile
|
|
if len(profile) > 0 && profile[0] != "" {
|
|
p = profile[0]
|
|
}
|
|
req.Header.Set("UCP-Agent", `profile="`+p+`"`)
|
|
}
|