blob: 3d1de2f2b19de8a6938f78e93a10967662f4d8ac [file] [log] [blame]
// Copyright The Monogon Project Authors.
// SPDX-License-Identifier: Apache-2.0
package registry
import "strings"
type authenticateChallenge struct {
scheme string
info string
params map[string]string
}
// parseAuthenticateHeader parses the values of a WWW-Authenticate HTTP header.
// parameter names are converted to lower case.
// If any value fails to parse, it returns nil.
func parseAuthenticateHeader(authenticate []string) []authenticateChallenge {
// From RFC 9110:
// WWW-Authenticate = #challenge
// challenge = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
// auth-scheme = token
// token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="
// auth-param = token BWS "=" BWS ( token / quoted-string )
// #element => [ element ] *( OWS "," OWS [ element ] )
// token = 1*tchar
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
// / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
// / DIGIT / ALPHA
// quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
// qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
// quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
// obs-text = %x80-FF
// OWS = *( SP / HTAB )
// BWS = OWS
// VCHAR = %x21-7E
var challenges []authenticateChallenge
for _, a := range authenticate {
for {
a = strings.TrimLeft(a, " \t,") // Consume commas and OWS
if a == "" {
break
}
var scheme string
scheme, a = scanToken(a) // Consume auth-scheme
if scheme == "" {
return nil
}
challenge := authenticateChallenge{
scheme: scheme,
}
if !strings.HasPrefix(a, " ") { // Check for 1*SP
a = strings.TrimLeft(a, " \t") // Consume OWS
if a != "" && a[0] != ',' { // Check for mandatory comma
return nil
}
challenges = append(challenges, challenge)
continue
}
a = strings.TrimLeft(a, " ") // Consume 1*SP
// Check for token68
i := 0
for i < len(a) && charType[a[i]]&charTypeToken68 != 0 {
i++
}
if i != 0 {
for i < len(a) && a[i] == '=' { // Consume *"="
i++
}
remain := strings.TrimLeft(a[i:], " \t") // Consume OWS
if remain == "" || remain[0] == ',' { // Check for mandatory comma
// Confirmed token68
challenge.info = a[:i]
challenges = append(challenges, challenge)
a = remain
continue
}
}
challenge.params = make(map[string]string)
for {
// Check for auth-param
remain := strings.TrimLeft(a, " \t,") // Consume commas and OWS
var name string
name, remain = scanToken(remain) // Consume token
if name == "" {
break
}
remain = strings.TrimLeft(remain, " \t") // Consume BWS
var ok bool
if remain, ok = strings.CutPrefix(remain, "="); !ok { // Consume "="
break
}
remain = strings.TrimLeft(remain, " \t") // Consume BWS
var value string
if remain, ok = strings.CutPrefix(remain, `"`); ok { // Check for quoted-string
i := 0
for i < len(remain) {
if charType[remain[i]]&charTypeQdtext != 0 {
i++
} else if remain[i] == '\\' && i+1 < len(remain) && charType[remain[i+1]]&charTypeQuotedPair != 0 {
value += remain[:i]
remain = remain[i+1:] // Drop the backslash to unescape the string
i = 1
} else {
break
}
}
value += remain[:i]
remain = remain[i:]
if remain, ok = strings.CutPrefix(remain, `"`); !ok { // Consume quote
break
}
} else {
value, remain = scanToken(remain) // Consume token
if value == "" {
break
}
}
// Confirmed auth-param
name = strings.ToLower(name) // name is case-insensitive
if _, ok := challenge.params[name]; ok {
return nil // each parameter name MUST only occur once
}
challenge.params[name] = value
a = remain
a = strings.TrimLeft(a, " \t") // Consume OWS
if a != "" && a[0] != ',' { // Check for mandatory comma
return nil
}
}
challenges = append(challenges, challenge)
a = strings.TrimLeft(a, " \t") // Consume OWS
if a != "" && a[0] != ',' { // Check for mandatory comma
return nil
}
}
}
return challenges
}
var charType [256]uint8
const (
charTypeToken = 1 << iota
charTypeToken68
charTypeQdtext
charTypeQuotedPair
)
func init() {
for _, c := range "!#$%&'*+-.^_`|~" {
charType[c] |= charTypeToken
}
for _, c := range "-._~+/" {
charType[c] |= charTypeToken68
}
for c := range 256 {
if '0' <= c && c <= '9' || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' {
charType[c] |= charTypeToken | charTypeToken68
}
if c == '\t' || c == ' ' || 0x21 <= c && c != 0x7f {
if c != '"' && c != '\\' {
charType[c] |= charTypeQdtext
}
charType[c] |= charTypeQuotedPair
}
}
}
func scanToken(s string) (token string, remain string) {
for i := range len(s) {
if charType[s[i]]&charTypeToken == 0 {
return s[:i], s[i:]
}
}
return s, ""
}