blob: 3d1de2f2b19de8a6938f78e93a10967662f4d8ac [file] [log] [blame]
Jan Schärcc9e4d12025-04-14 10:28:40 +00001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
4package registry
5
6import "strings"
7
8type authenticateChallenge struct {
9 scheme string
10 info string
11 params map[string]string
12}
13
14// parseAuthenticateHeader parses the values of a WWW-Authenticate HTTP header.
15// parameter names are converted to lower case.
16// If any value fails to parse, it returns nil.
17func parseAuthenticateHeader(authenticate []string) []authenticateChallenge {
18 // From RFC 9110:
19 // WWW-Authenticate = #challenge
20 // challenge = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
21 // auth-scheme = token
22 // token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="
23 // auth-param = token BWS "=" BWS ( token / quoted-string )
24 // #element => [ element ] *( OWS "," OWS [ element ] )
25 // token = 1*tchar
26 // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
27 // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
28 // / DIGIT / ALPHA
29 // quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
30 // qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
31 // quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
32 // obs-text = %x80-FF
33 // OWS = *( SP / HTAB )
34 // BWS = OWS
35 // VCHAR = %x21-7E
36
37 var challenges []authenticateChallenge
38 for _, a := range authenticate {
39 for {
40 a = strings.TrimLeft(a, " \t,") // Consume commas and OWS
41 if a == "" {
42 break
43 }
44 var scheme string
45 scheme, a = scanToken(a) // Consume auth-scheme
46 if scheme == "" {
47 return nil
48 }
49 challenge := authenticateChallenge{
50 scheme: scheme,
51 }
52 if !strings.HasPrefix(a, " ") { // Check for 1*SP
53 a = strings.TrimLeft(a, " \t") // Consume OWS
54 if a != "" && a[0] != ',' { // Check for mandatory comma
55 return nil
56 }
57 challenges = append(challenges, challenge)
58 continue
59 }
60 a = strings.TrimLeft(a, " ") // Consume 1*SP
61
62 // Check for token68
63 i := 0
64 for i < len(a) && charType[a[i]]&charTypeToken68 != 0 {
65 i++
66 }
67 if i != 0 {
68 for i < len(a) && a[i] == '=' { // Consume *"="
69 i++
70 }
71 remain := strings.TrimLeft(a[i:], " \t") // Consume OWS
72 if remain == "" || remain[0] == ',' { // Check for mandatory comma
73 // Confirmed token68
74 challenge.info = a[:i]
75 challenges = append(challenges, challenge)
76 a = remain
77 continue
78 }
79 }
80
81 challenge.params = make(map[string]string)
82 for {
83 // Check for auth-param
84 remain := strings.TrimLeft(a, " \t,") // Consume commas and OWS
85 var name string
86 name, remain = scanToken(remain) // Consume token
87 if name == "" {
88 break
89 }
90 remain = strings.TrimLeft(remain, " \t") // Consume BWS
91 var ok bool
92 if remain, ok = strings.CutPrefix(remain, "="); !ok { // Consume "="
93 break
94 }
95 remain = strings.TrimLeft(remain, " \t") // Consume BWS
96 var value string
97 if remain, ok = strings.CutPrefix(remain, `"`); ok { // Check for quoted-string
98 i := 0
99 for i < len(remain) {
100 if charType[remain[i]]&charTypeQdtext != 0 {
101 i++
102 } else if remain[i] == '\\' && i+1 < len(remain) && charType[remain[i+1]]&charTypeQuotedPair != 0 {
103 value += remain[:i]
104 remain = remain[i+1:] // Drop the backslash to unescape the string
105 i = 1
106 } else {
107 break
108 }
109 }
110 value += remain[:i]
111 remain = remain[i:]
112 if remain, ok = strings.CutPrefix(remain, `"`); !ok { // Consume quote
113 break
114 }
115 } else {
116 value, remain = scanToken(remain) // Consume token
117 if value == "" {
118 break
119 }
120 }
121 // Confirmed auth-param
122 name = strings.ToLower(name) // name is case-insensitive
123 if _, ok := challenge.params[name]; ok {
124 return nil // each parameter name MUST only occur once
125 }
126 challenge.params[name] = value
127 a = remain
128 a = strings.TrimLeft(a, " \t") // Consume OWS
129 if a != "" && a[0] != ',' { // Check for mandatory comma
130 return nil
131 }
132 }
133 challenges = append(challenges, challenge)
134 a = strings.TrimLeft(a, " \t") // Consume OWS
135 if a != "" && a[0] != ',' { // Check for mandatory comma
136 return nil
137 }
138 }
139 }
140 return challenges
141}
142
143var charType [256]uint8
144
145const (
146 charTypeToken = 1 << iota
147 charTypeToken68
148 charTypeQdtext
149 charTypeQuotedPair
150)
151
152func init() {
153 for _, c := range "!#$%&'*+-.^_`|~" {
154 charType[c] |= charTypeToken
155 }
156 for _, c := range "-._~+/" {
157 charType[c] |= charTypeToken68
158 }
159 for c := range 256 {
160 if '0' <= c && c <= '9' || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' {
161 charType[c] |= charTypeToken | charTypeToken68
162 }
163 if c == '\t' || c == ' ' || 0x21 <= c && c != 0x7f {
164 if c != '"' && c != '\\' {
165 charType[c] |= charTypeQdtext
166 }
167 charType[c] |= charTypeQuotedPair
168 }
169 }
170}
171
172func scanToken(s string) (token string, remain string) {
173 for i := range len(s) {
174 if charType[s[i]]&charTypeToken == 0 {
175 return s[:i], s[i:]
176 }
177 }
178 return s, ""
179}