1 package httpcc
2
3 import (
4 "bufio"
5 "fmt"
6 "strconv"
7 "strings"
8 "unicode/utf8"
9 )
10
11 const (
12
13 MaxAge = "max-age"
14 MaxStale = "max-stale"
15 MinFresh = "min-fresh"
16 NoCache = "no-cache"
17 NoStore = "no-store"
18 NoTransform = "no-transform"
19 OnlyIfCached = "only-if-cached"
20
21
22 MustRevalidate = "must-revalidate"
23 Public = "public"
24 Private = "private"
25 ProxyRevalidate = "proxy-revalidate"
26 SMaxAge = "s-maxage"
27 )
28
29 type TokenPair struct {
30 Name string
31 Value string
32 }
33
34 type TokenValuePolicy int
35
36 const (
37 NoArgument TokenValuePolicy = iota
38 TokenOnly
39 QuotedStringOnly
40 AnyTokenValue
41 )
42
43 type directiveValidator interface {
44 Validate(string) TokenValuePolicy
45 }
46 type directiveValidatorFn func(string) TokenValuePolicy
47
48 func (fn directiveValidatorFn) Validate(ccd string) TokenValuePolicy {
49 return fn(ccd)
50 }
51
52 func responseDirectiveValidator(s string) TokenValuePolicy {
53 switch s {
54 case MustRevalidate, NoStore, NoTransform, Public, ProxyRevalidate:
55 return NoArgument
56 case NoCache, Private:
57 return QuotedStringOnly
58 case MaxAge, SMaxAge:
59 return TokenOnly
60 default:
61 return AnyTokenValue
62 }
63 }
64
65 func requestDirectiveValidator(s string) TokenValuePolicy {
66 switch s {
67 case MaxAge, MaxStale, MinFresh:
68 return TokenOnly
69 case NoCache, NoStore, NoTransform, OnlyIfCached:
70 return NoArgument
71 default:
72 return AnyTokenValue
73 }
74 }
75
76
77 func ParseRequestDirective(s string) (*TokenPair, error) {
78 return parseDirective(s, directiveValidatorFn(requestDirectiveValidator))
79 }
80
81 func ParseResponseDirective(s string) (*TokenPair, error) {
82 return parseDirective(s, directiveValidatorFn(responseDirectiveValidator))
83 }
84
85 func parseDirective(s string, ccd directiveValidator) (*TokenPair, error) {
86 s = strings.TrimSpace(s)
87
88 i := strings.IndexByte(s, '=')
89 if i == -1 {
90 return &TokenPair{Name: s}, nil
91 }
92
93 pair := &TokenPair{Name: strings.TrimSpace(s[:i])}
94
95 if len(s) <= i {
96
97
98 return pair, nil
99 }
100
101 v := strings.TrimSpace(s[i+1:])
102 switch ccd.Validate(pair.Name) {
103 case TokenOnly:
104 if v[0] == '"' {
105 return nil, fmt.Errorf(`invalid value for %s (quoted string not allowed)`, pair.Name)
106 }
107 case QuotedStringOnly:
108 if v[0] != '"' {
109 return nil, fmt.Errorf(`invalid value for %s (bare token not allowed)`, pair.Name)
110 }
111 tmp, err := strconv.Unquote(v)
112 if err != nil {
113 return nil, fmt.Errorf(`malformed quoted string in token`)
114 }
115 v = tmp
116 case AnyTokenValue:
117 if v[0] == '"' {
118 tmp, err := strconv.Unquote(v)
119 if err != nil {
120 return nil, fmt.Errorf(`malformed quoted string in token`)
121 }
122 v = tmp
123 }
124 case NoArgument:
125 if len(v) > 0 {
126 return nil, fmt.Errorf(`received argument to directive %s`, pair.Name)
127 }
128 }
129
130 pair.Value = v
131 return pair, nil
132 }
133
134 func ParseResponseDirectives(s string) ([]*TokenPair, error) {
135 return parseDirectives(s, ParseResponseDirective)
136 }
137
138 func ParseRequestDirectives(s string) ([]*TokenPair, error) {
139 return parseDirectives(s, ParseRequestDirective)
140 }
141
142 func parseDirectives(s string, p func(string) (*TokenPair, error)) ([]*TokenPair, error) {
143 scanner := bufio.NewScanner(strings.NewReader(s))
144 scanner.Split(scanCommaSeparatedWords)
145
146 var tokens []*TokenPair
147 for scanner.Scan() {
148 tok, err := p(scanner.Text())
149 if err != nil {
150 return nil, fmt.Errorf(`failed to parse token #%d: %w`, len(tokens)+1, err)
151 }
152 tokens = append(tokens, tok)
153 }
154 return tokens, nil
155 }
156
157
158
159
160 func isSpace(r rune) bool {
161 if r <= '\u00FF' {
162
163 switch r {
164 case ' ', '\t', '\n', '\v', '\f', '\r':
165 return true
166 case '\u0085', '\u00A0':
167 return true
168 }
169 return false
170 }
171
172 if '\u2000' <= r && r <= '\u200a' {
173 return true
174 }
175 switch r {
176 case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
177 return true
178 }
179 return false
180 }
181
182 func scanCommaSeparatedWords(data []byte, atEOF bool) (advance int, token []byte, err error) {
183
184 start := 0
185 for width := 0; start < len(data); start += width {
186 var r rune
187 r, width = utf8.DecodeRune(data[start:])
188 if !isSpace(r) {
189 break
190 }
191 }
192
193
194 var ws int
195 for width, i := 0, start; i < len(data); i += width {
196 var r rune
197 r, width = utf8.DecodeRune(data[i:])
198 switch {
199 case isSpace(r):
200 ws++
201 case r == ',':
202 return i + width, data[start : i-ws], nil
203 default:
204 ws = 0
205 }
206 }
207
208
209 if atEOF && len(data) > start {
210 return len(data), data[start : len(data)-ws], nil
211 }
212
213
214 return start, nil, nil
215 }
216
217
218 func ParseRequest(v string) (*RequestDirective, error) {
219 var dir RequestDirective
220 tokens, err := ParseRequestDirectives(v)
221 if err != nil {
222 return nil, fmt.Errorf(`failed to parse tokens: %w`, err)
223 }
224
225 for _, token := range tokens {
226 name := strings.ToLower(token.Name)
227 switch name {
228 case MaxAge:
229 iv, err := strconv.ParseUint(token.Value, 10, 64)
230 if err != nil {
231 return nil, fmt.Errorf(`failed to parse max-age: %w`, err)
232 }
233 dir.maxAge = &iv
234 case MaxStale:
235 iv, err := strconv.ParseUint(token.Value, 10, 64)
236 if err != nil {
237 return nil, fmt.Errorf(`failed to parse max-stale: %w`, err)
238 }
239 dir.maxStale = &iv
240 case MinFresh:
241 iv, err := strconv.ParseUint(token.Value, 10, 64)
242 if err != nil {
243 return nil, fmt.Errorf(`failed to parse min-fresh: %w`, err)
244 }
245 dir.minFresh = &iv
246 case NoCache:
247 dir.noCache = true
248 case NoStore:
249 dir.noStore = true
250 case NoTransform:
251 dir.noTransform = true
252 case OnlyIfCached:
253 dir.onlyIfCached = true
254 default:
255 dir.extensions[token.Name] = token.Value
256 }
257 }
258 return &dir, nil
259 }
260
261
262 func ParseResponse(v string) (*ResponseDirective, error) {
263 tokens, err := ParseResponseDirectives(v)
264 if err != nil {
265 return nil, fmt.Errorf(`failed to parse tokens: %w`, err)
266 }
267
268 var dir ResponseDirective
269 dir.extensions = make(map[string]string)
270 for _, token := range tokens {
271 name := strings.ToLower(token.Name)
272 switch name {
273 case MaxAge:
274 iv, err := strconv.ParseUint(token.Value, 10, 64)
275 if err != nil {
276 return nil, fmt.Errorf(`failed to parse max-age: %w`, err)
277 }
278 dir.maxAge = &iv
279 case NoCache:
280 scanner := bufio.NewScanner(strings.NewReader(token.Value))
281 scanner.Split(scanCommaSeparatedWords)
282 for scanner.Scan() {
283 dir.noCache = append(dir.noCache, scanner.Text())
284 }
285 case NoStore:
286 dir.noStore = true
287 case NoTransform:
288 dir.noTransform = true
289 case Public:
290 dir.public = true
291 case Private:
292 scanner := bufio.NewScanner(strings.NewReader(token.Value))
293 scanner.Split(scanCommaSeparatedWords)
294 for scanner.Scan() {
295 dir.private = append(dir.private, scanner.Text())
296 }
297 case ProxyRevalidate:
298 dir.proxyRevalidate = true
299 case SMaxAge:
300 iv, err := strconv.ParseUint(token.Value, 10, 64)
301 if err != nil {
302 return nil, fmt.Errorf(`failed to parse s-maxage: %w`, err)
303 }
304 dir.sMaxAge = &iv
305 default:
306 dir.extensions[token.Name] = token.Value
307 }
308 }
309 return &dir, nil
310 }
311
View as plain text