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