Source file
src/net/http/cookie.go
1
2
3
4
5 package http
6
7 import (
8 "errors"
9 "fmt"
10 "log"
11 "net"
12 "net/http/internal/ascii"
13 "net/textproto"
14 "strconv"
15 "strings"
16 "time"
17 )
18
19
20
21
22
23 type Cookie struct {
24 Name string
25 Value string
26 Quoted bool
27
28 Path string
29 Domain string
30 Expires time.Time
31 RawExpires string
32
33
34
35
36 MaxAge int
37 Secure bool
38 HttpOnly bool
39 SameSite SameSite
40 Raw string
41 Unparsed []string
42 }
43
44
45
46
47
48
49
50 type SameSite int
51
52 const (
53 SameSiteDefaultMode SameSite = iota + 1
54 SameSiteLaxMode
55 SameSiteStrictMode
56 SameSiteNoneMode
57 )
58
59 var (
60 errBlankCookie = errors.New("http: blank cookie")
61 errEqualNotFoundInCookie = errors.New("http: '=' not found in cookie")
62 errInvalidCookieName = errors.New("http: invalid cookie name")
63 errInvalidCookieValue = errors.New("http: invalid cookie value")
64 )
65
66
67
68
69 func ParseCookie(line string) ([]*Cookie, error) {
70 parts := strings.Split(textproto.TrimString(line), ";")
71 if len(parts) == 1 && parts[0] == "" {
72 return nil, errBlankCookie
73 }
74 cookies := make([]*Cookie, 0, len(parts))
75 for _, s := range parts {
76 s = textproto.TrimString(s)
77 name, value, found := strings.Cut(s, "=")
78 if !found {
79 return nil, errEqualNotFoundInCookie
80 }
81 if !isCookieNameValid(name) {
82 return nil, errInvalidCookieName
83 }
84 value, quoted, found := parseCookieValue(value, true)
85 if !found {
86 return nil, errInvalidCookieValue
87 }
88 cookies = append(cookies, &Cookie{Name: name, Value: value, Quoted: quoted})
89 }
90 return cookies, nil
91 }
92
93
94
95 func ParseSetCookie(line string) (*Cookie, error) {
96 parts := strings.Split(textproto.TrimString(line), ";")
97 if len(parts) == 1 && parts[0] == "" {
98 return nil, errBlankCookie
99 }
100 parts[0] = textproto.TrimString(parts[0])
101 name, value, ok := strings.Cut(parts[0], "=")
102 if !ok {
103 return nil, errEqualNotFoundInCookie
104 }
105 name = textproto.TrimString(name)
106 if !isCookieNameValid(name) {
107 return nil, errInvalidCookieName
108 }
109 value, quoted, ok := parseCookieValue(value, true)
110 if !ok {
111 return nil, errInvalidCookieValue
112 }
113 c := &Cookie{
114 Name: name,
115 Value: value,
116 Quoted: quoted,
117 Raw: line,
118 }
119 for i := 1; i < len(parts); i++ {
120 parts[i] = textproto.TrimString(parts[i])
121 if len(parts[i]) == 0 {
122 continue
123 }
124
125 attr, val, _ := strings.Cut(parts[i], "=")
126 lowerAttr, isASCII := ascii.ToLower(attr)
127 if !isASCII {
128 continue
129 }
130 val, _, ok = parseCookieValue(val, false)
131 if !ok {
132 c.Unparsed = append(c.Unparsed, parts[i])
133 continue
134 }
135
136 switch lowerAttr {
137 case "samesite":
138 lowerVal, ascii := ascii.ToLower(val)
139 if !ascii {
140 c.SameSite = SameSiteDefaultMode
141 continue
142 }
143 switch lowerVal {
144 case "lax":
145 c.SameSite = SameSiteLaxMode
146 case "strict":
147 c.SameSite = SameSiteStrictMode
148 case "none":
149 c.SameSite = SameSiteNoneMode
150 default:
151 c.SameSite = SameSiteDefaultMode
152 }
153 continue
154 case "secure":
155 c.Secure = true
156 continue
157 case "httponly":
158 c.HttpOnly = true
159 continue
160 case "domain":
161 c.Domain = val
162 continue
163 case "max-age":
164 secs, err := strconv.Atoi(val)
165 if err != nil || secs != 0 && val[0] == '0' {
166 break
167 }
168 if secs <= 0 {
169 secs = -1
170 }
171 c.MaxAge = secs
172 continue
173 case "expires":
174 c.RawExpires = val
175 exptime, err := time.Parse(time.RFC1123, val)
176 if err != nil {
177 exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", val)
178 if err != nil {
179 c.Expires = time.Time{}
180 break
181 }
182 }
183 c.Expires = exptime.UTC()
184 continue
185 case "path":
186 c.Path = val
187 continue
188 }
189 c.Unparsed = append(c.Unparsed, parts[i])
190 }
191 return c, nil
192 }
193
194
195
196 func readSetCookies(h Header) []*Cookie {
197 cookieCount := len(h["Set-Cookie"])
198 if cookieCount == 0 {
199 return []*Cookie{}
200 }
201 cookies := make([]*Cookie, 0, cookieCount)
202 for _, line := range h["Set-Cookie"] {
203 if cookie, err := ParseSetCookie(line); err == nil {
204 cookies = append(cookies, cookie)
205 }
206 }
207 return cookies
208 }
209
210
211
212
213 func SetCookie(w ResponseWriter, cookie *Cookie) {
214 if v := cookie.String(); v != "" {
215 w.Header().Add("Set-Cookie", v)
216 }
217 }
218
219
220
221
222
223 func (c *Cookie) String() string {
224 if c == nil || !isCookieNameValid(c.Name) {
225 return ""
226 }
227
228
229 const extraCookieLength = 110
230 var b strings.Builder
231 b.Grow(len(c.Name) + len(c.Value) + len(c.Domain) + len(c.Path) + extraCookieLength)
232 b.WriteString(c.Name)
233 b.WriteRune('=')
234 b.WriteString(sanitizeCookieValue(c.Value, c.Quoted))
235
236 if len(c.Path) > 0 {
237 b.WriteString("; Path=")
238 b.WriteString(sanitizeCookiePath(c.Path))
239 }
240 if len(c.Domain) > 0 {
241 if validCookieDomain(c.Domain) {
242
243
244
245
246 d := c.Domain
247 if d[0] == '.' {
248 d = d[1:]
249 }
250 b.WriteString("; Domain=")
251 b.WriteString(d)
252 } else {
253 log.Printf("net/http: invalid Cookie.Domain %q; dropping domain attribute", c.Domain)
254 }
255 }
256 var buf [len(TimeFormat)]byte
257 if validCookieExpires(c.Expires) {
258 b.WriteString("; Expires=")
259 b.Write(c.Expires.UTC().AppendFormat(buf[:0], TimeFormat))
260 }
261 if c.MaxAge > 0 {
262 b.WriteString("; Max-Age=")
263 b.Write(strconv.AppendInt(buf[:0], int64(c.MaxAge), 10))
264 } else if c.MaxAge < 0 {
265 b.WriteString("; Max-Age=0")
266 }
267 if c.HttpOnly {
268 b.WriteString("; HttpOnly")
269 }
270 if c.Secure {
271 b.WriteString("; Secure")
272 }
273 switch c.SameSite {
274 case SameSiteDefaultMode:
275
276 case SameSiteNoneMode:
277 b.WriteString("; SameSite=None")
278 case SameSiteLaxMode:
279 b.WriteString("; SameSite=Lax")
280 case SameSiteStrictMode:
281 b.WriteString("; SameSite=Strict")
282 }
283 return b.String()
284 }
285
286
287 func (c *Cookie) Valid() error {
288 if c == nil {
289 return errors.New("http: nil Cookie")
290 }
291 if !isCookieNameValid(c.Name) {
292 return errors.New("http: invalid Cookie.Name")
293 }
294 if !c.Expires.IsZero() && !validCookieExpires(c.Expires) {
295 return errors.New("http: invalid Cookie.Expires")
296 }
297 for i := 0; i < len(c.Value); i++ {
298 if !validCookieValueByte(c.Value[i]) {
299 return fmt.Errorf("http: invalid byte %q in Cookie.Value", c.Value[i])
300 }
301 }
302 if len(c.Path) > 0 {
303 for i := 0; i < len(c.Path); i++ {
304 if !validCookiePathByte(c.Path[i]) {
305 return fmt.Errorf("http: invalid byte %q in Cookie.Path", c.Path[i])
306 }
307 }
308 }
309 if len(c.Domain) > 0 {
310 if !validCookieDomain(c.Domain) {
311 return errors.New("http: invalid Cookie.Domain")
312 }
313 }
314 return nil
315 }
316
317
318
319
320
321 func readCookies(h Header, filter string) []*Cookie {
322 lines := h["Cookie"]
323 if len(lines) == 0 {
324 return []*Cookie{}
325 }
326
327 cookies := make([]*Cookie, 0, len(lines)+strings.Count(lines[0], ";"))
328 for _, line := range lines {
329 line = textproto.TrimString(line)
330
331 var part string
332 for len(line) > 0 {
333 part, line, _ = strings.Cut(line, ";")
334 part = textproto.TrimString(part)
335 if part == "" {
336 continue
337 }
338 name, val, _ := strings.Cut(part, "=")
339 name = textproto.TrimString(name)
340 if !isCookieNameValid(name) {
341 continue
342 }
343 if filter != "" && filter != name {
344 continue
345 }
346 val, quoted, ok := parseCookieValue(val, true)
347 if !ok {
348 continue
349 }
350 cookies = append(cookies, &Cookie{Name: name, Value: val, Quoted: quoted})
351 }
352 }
353 return cookies
354 }
355
356
357 func validCookieDomain(v string) bool {
358 if isCookieDomainName(v) {
359 return true
360 }
361 if net.ParseIP(v) != nil && !strings.Contains(v, ":") {
362 return true
363 }
364 return false
365 }
366
367
368 func validCookieExpires(t time.Time) bool {
369
370 return t.Year() >= 1601
371 }
372
373
374
375
376 func isCookieDomainName(s string) bool {
377 if len(s) == 0 {
378 return false
379 }
380 if len(s) > 255 {
381 return false
382 }
383
384 if s[0] == '.' {
385
386 s = s[1:]
387 }
388 last := byte('.')
389 ok := false
390 partlen := 0
391 for i := 0; i < len(s); i++ {
392 c := s[i]
393 switch {
394 default:
395 return false
396 case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z':
397
398 ok = true
399 partlen++
400 case '0' <= c && c <= '9':
401
402 partlen++
403 case c == '-':
404
405 if last == '.' {
406 return false
407 }
408 partlen++
409 case c == '.':
410
411 if last == '.' || last == '-' {
412 return false
413 }
414 if partlen > 63 || partlen == 0 {
415 return false
416 }
417 partlen = 0
418 }
419 last = c
420 }
421 if last == '-' || partlen > 63 {
422 return false
423 }
424
425 return ok
426 }
427
428 var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-")
429
430 func sanitizeCookieName(n string) string {
431 return cookieNameSanitizer.Replace(n)
432 }
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448 func sanitizeCookieValue(v string, quoted bool) string {
449 v = sanitizeOrWarn("Cookie.Value", validCookieValueByte, v)
450 if len(v) == 0 {
451 return v
452 }
453 if strings.ContainsAny(v, " ,") || quoted {
454 return `"` + v + `"`
455 }
456 return v
457 }
458
459 func validCookieValueByte(b byte) bool {
460 return 0x20 <= b && b < 0x7f && b != '"' && b != ';' && b != '\\'
461 }
462
463
464
465 func sanitizeCookiePath(v string) string {
466 return sanitizeOrWarn("Cookie.Path", validCookiePathByte, v)
467 }
468
469 func validCookiePathByte(b byte) bool {
470 return 0x20 <= b && b < 0x7f && b != ';'
471 }
472
473 func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string {
474 ok := true
475 for i := 0; i < len(v); i++ {
476 if valid(v[i]) {
477 continue
478 }
479 log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName)
480 ok = false
481 break
482 }
483 if ok {
484 return v
485 }
486 buf := make([]byte, 0, len(v))
487 for i := 0; i < len(v); i++ {
488 if b := v[i]; valid(b) {
489 buf = append(buf, b)
490 }
491 }
492 return string(buf)
493 }
494
495
496
497
498
499
500
501
502
503
504 func parseCookieValue(raw string, allowDoubleQuote bool) (value string, quoted, ok bool) {
505
506 if allowDoubleQuote && len(raw) > 1 && raw[0] == '"' && raw[len(raw)-1] == '"' {
507 raw = raw[1 : len(raw)-1]
508 quoted = true
509 }
510 for i := 0; i < len(raw); i++ {
511 if !validCookieValueByte(raw[i]) {
512 return "", quoted, false
513 }
514 }
515 return raw, quoted, true
516 }
517
518 func isCookieNameValid(raw string) bool {
519 if raw == "" {
520 return false
521 }
522 return strings.IndexFunc(raw, isNotToken) < 0
523 }
524
View as plain text