nidus-sync/auth/content_negotiation.go

156 lines
3.9 KiB
Go

package auth
import (
"sort"
"strconv"
"strings"
)
// acceptValue represents a parsed accept header value
type acceptValue struct {
value string // the media type or value
quality float64 // quality factor (0.0 to 1.0)
specificity int // specificity level for sorting
order int // original order in the accept header
}
// NegotiateContent returns the best content to offer from a set of possible
// values, based on the preferences represented by the accept values.
func NegotiateContent(accepts []string, offers []string) string {
if len(offers) == 0 {
return ""
}
// If no accept values, return first offer
if len(accepts) == 0 {
return offers[0]
}
// Parse accept values (limit to first 32 to avoid DOS)
acceptValues := parseAcceptValues(accepts)
if len(acceptValues) > 32 {
acceptValues = acceptValues[:32]
}
// Find best match
bestMatch := ""
bestScore := -1.0
bestSpecificity := -1
bestOrder := len(offers) // use offer order as tiebreaker
for offerIdx, offer := range offers {
score, specificity := calculateScore(offer, acceptValues)
// Check if this is a better match
if score > bestScore ||
(score == bestScore && specificity > bestSpecificity) ||
(score == bestScore && specificity == bestSpecificity && offerIdx < bestOrder) {
bestMatch = offer
bestScore = score
bestSpecificity = specificity
bestOrder = offerIdx
}
}
if bestScore <= 0 {
return ""
}
return bestMatch
}
// parseAcceptValues parses accept header values into structured format
func parseAcceptValues(accepts []string) []acceptValue {
var values []acceptValue
order := 0
for _, accept := range accepts {
// Split by comma to handle multiple values in one string
parts := strings.Split(accept, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
value := acceptValue{
quality: 1.0, // default quality
order: order,
}
// Split by semicolon to separate value from parameters
sections := strings.Split(part, ";")
value.value = strings.TrimSpace(sections[0])
// Parse quality parameter
for i := 1; i < len(sections); i++ {
param := strings.TrimSpace(sections[i])
if strings.HasPrefix(param, "q=") {
if q, err := strconv.ParseFloat(param[2:], 64); err == nil {
if q >= 0.0 && q <= 1.0 {
value.quality = q
}
}
break
}
}
// Calculate specificity for media types
value.specificity = calculateSpecificity(value.value)
values = append(values, value)
order++
}
}
// Sort by quality (desc), then specificity (desc), then order (asc)
sort.Slice(values, func(i, j int) bool {
if values[i].quality != values[j].quality {
return values[i].quality > values[j].quality
}
if values[i].specificity != values[j].specificity {
return values[i].specificity > values[j].specificity
}
return values[i].order < values[j].order
})
return values
}
// calculateSpecificity returns specificity level for media type matching
func calculateSpecificity(mediaType string) int {
if mediaType == "*/*" {
return 0 // least specific
}
if strings.HasSuffix(mediaType, "/*") {
return 1 // type wildcard
}
return 2 // exact match (most specific)
}
// calculateScore returns the quality score and specificity for an offer against accept values
func calculateScore(offer string, acceptValues []acceptValue) (float64, int) {
for _, accept := range acceptValues {
if matches(offer, accept.value) {
return accept.quality, accept.specificity
}
}
return 0.0, 0
}
// matches checks if an offer matches an accept value (including wildcards)
func matches(offer, accept string) bool {
if accept == "*/*" {
return true
}
if strings.HasSuffix(accept, "/*") {
// Type wildcard (e.g., "text/*")
offerType := strings.Split(offer, "/")[0]
acceptType := strings.Split(accept, "/")[0]
return offerType == acceptType
}
// Exact match
return offer == accept
}