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 nparts := strings.Count(line, ";") + 1
93 if !cookieNumWithinMax(nparts) {
94 return nil, errCookieNumLimitExceeded
95 } else if nparts == 1 && textproto.TrimString(line) == "" {
96 return nil, errBlankCookie
97 }
98 cookies := make([]*Cookie, 0, nparts)
99 for s := range strings.SplitSeq(line, ";") {
100 s = textproto.TrimString(s)
101 name, value, found := strings.Cut(s, "=")
102 if !found {
103 return nil, errEqualNotFoundInCookie
104 }
105 if !isToken(name) {
106 return nil, errInvalidCookieName
107 }
108 value, quoted, found := parseCookieValue(value, true)
109 if !found {
110 return nil, errInvalidCookieValue
111 }
112 cookies = append(cookies, &Cookie{Name: name, Value: value, Quoted: quoted})
113 }
114 return cookies, nil
115 }
116
117
118
119 func ParseSetCookie(line string) (*Cookie, error) {
120 parts := strings.Split(textproto.TrimString(line), ";")
121 if len(parts) == 1 && parts[0] == "" {
122 return nil, errBlankCookie
123 }
124 parts[0] = textproto.TrimString(parts[0])
125 name, value, ok := strings.Cut(parts[0], "=")
126 if !ok {
127 return nil, errEqualNotFoundInCookie
128 }
129 name = textproto.TrimString(name)
130 if !isToken(name) {
131 return nil, errInvalidCookieName
132 }
133 value, quoted, ok := parseCookieValue(value, true)
134 if !ok {
135 return nil, errInvalidCookieValue
136 }
137 c := &Cookie{
138 Name: name,
139 Value: value,
140 Quoted: quoted,
141 Raw: line,
142 }
143 for i := 1; i < len(parts); i++ {
144 parts[i] = textproto.TrimString(parts[i])
145 if len(parts[i]) == 0 {
146 continue
147 }
148
149 attr, val, _ := strings.Cut(parts[i], "=")
150 lowerAttr, isASCII := ascii.ToLower(attr)
151 if !isASCII {
152 continue
153 }
154 val, _, ok = parseCookieValue(val, false)
155 if !ok {
156 c.Unparsed = append(c.Unparsed, parts[i])
157 continue
158 }
159
160 switch lowerAttr {
161 case "samesite":
162 lowerVal, ascii := ascii.ToLower(val)
163 if !ascii {
164 c.SameSite = SameSiteDefaultMode
165 continue
166 }
167 switch lowerVal {
168 case "lax":
169 c.SameSite = SameSiteLaxMode
170 case "strict":
171 c.SameSite = SameSiteStrictMode
172 case "none":
173 c.SameSite = SameSiteNoneMode
174 default:
175 c.SameSite = SameSiteDefaultMode
176 }
177 continue
178 case "secure":
179 c.Secure = true
180 continue
181 case "httponly":
182 c.HttpOnly = true
183 continue
184 case "domain":
185 c.Domain = val
186 continue
187 case "max-age":
188 secs, err := strconv.Atoi(val)
189 if err != nil || secs != 0 && val[0] == '0' {
190 break
191 }
192 if secs <= 0 {
193 secs = -1
194 }
195 c.MaxAge = secs
196 continue
197 case "expires":
198 c.RawExpires = val
199 exptime, err := time.Parse(time.RFC1123, val)
200 if err != nil {
201 exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", val)
202 if err != nil {
203 c.Expires = time.Time{}
204 break
205 }
206 }
207 c.Expires = exptime.UTC()
208 continue
209 case "path":
210 c.Path = val
211 continue
212 case "partitioned":
213 c.Partitioned = true
214 continue
215 }
216 c.Unparsed = append(c.Unparsed, parts[i])
217 }
218 return c, nil
219 }
220
221
222
223
224
225
226
227 func readSetCookies(h Header) []*Cookie {
228 cookieCount := len(h["Set-Cookie"])
229 if cookieCount == 0 {
230 return []*Cookie{}
231 }
232
233
234
235 if !cookieNumWithinMax(cookieCount) {
236 return []*Cookie{}
237 }
238 cookies := make([]*Cookie, 0, cookieCount)
239 for _, line := range h["Set-Cookie"] {
240 if cookie, err := ParseSetCookie(line); err == nil {
241 cookies = append(cookies, cookie)
242 }
243 }
244 return cookies
245 }
246
247
248
249
250 func SetCookie(w ResponseWriter, cookie *Cookie) {
251 if v := cookie.String(); v != "" {
252 w.Header().Add("Set-Cookie", v)
253 }
254 }
255
256
257
258
259
260 func (c *Cookie) String() string {
261 if c == nil || !isToken(c.Name) {
262 return ""
263 }
264
265
266 const extraCookieLength = 110
267 var b strings.Builder
268 b.Grow(len(c.Name) + len(c.Value) + len(c.Domain) + len(c.Path) + extraCookieLength)
269 b.WriteString(c.Name)
270 b.WriteRune('=')
271 b.WriteString(sanitizeCookieValue(c.Value, c.Quoted))
272
273 if len(c.Path) > 0 {
274 b.WriteString("; Path=")
275 b.WriteString(sanitizeCookiePath(c.Path))
276 }
277 if len(c.Domain) > 0 {
278 if validCookieDomain(c.Domain) {
279
280
281
282
283 d := c.Domain
284 if d[0] == '.' {
285 d = d[1:]
286 }
287 b.WriteString("; Domain=")
288 b.WriteString(d)
289 } else {
290 log.Printf("net/http: invalid Cookie.Domain %q; dropping domain attribute", c.Domain)
291 }
292 }
293 var buf [len(TimeFormat)]byte
294 if validCookieExpires(c.Expires) {
295 b.WriteString("; Expires=")
296 b.Write(c.Expires.UTC().AppendFormat(buf[:0], TimeFormat))
297 }
298 if c.MaxAge > 0 {
299 b.WriteString("; Max-Age=")
300 b.Write(strconv.AppendInt(buf[:0], int64(c.MaxAge), 10))
301 } else if c.MaxAge < 0 {
302 b.WriteString("; Max-Age=0")
303 }
304 if c.HttpOnly {
305 b.WriteString("; HttpOnly")
306 }
307 if c.Secure {
308 b.WriteString("; Secure")
309 }
310 switch c.SameSite {
311 case SameSiteDefaultMode:
312
313 case SameSiteNoneMode:
314 b.WriteString("; SameSite=None")
315 case SameSiteLaxMode:
316 b.WriteString("; SameSite=Lax")
317 case SameSiteStrictMode:
318 b.WriteString("; SameSite=Strict")
319 }
320 if c.Partitioned {
321 b.WriteString("; Partitioned")
322 }
323 return b.String()
324 }
325
326
327 func (c *Cookie) Valid() error {
328 if c == nil {
329 return errors.New("http: nil Cookie")
330 }
331 if !isToken(c.Name) {
332 return errors.New("http: invalid Cookie.Name")
333 }
334 if !c.Expires.IsZero() && !validCookieExpires(c.Expires) {
335 return errors.New("http: invalid Cookie.Expires")
336 }
337 for i := 0; i < len(c.Value); i++ {
338 if !validCookieValueByte(c.Value[i]) {
339 return fmt.Errorf("http: invalid byte %q in Cookie.Value", c.Value[i])
340 }
341 }
342 if len(c.Path) > 0 {
343 for i := 0; i < len(c.Path); i++ {
344 if !validCookiePathByte(c.Path[i]) {
345 return fmt.Errorf("http: invalid byte %q in Cookie.Path", c.Path[i])
346 }
347 }
348 }
349 if len(c.Domain) > 0 {
350 if !validCookieDomain(c.Domain) {
351 return errors.New("http: invalid Cookie.Domain")
352 }
353 }
354 if c.Partitioned {
355 if !c.Secure {
356 return errors.New("http: partitioned cookies must be set with Secure")
357 }
358 }
359 return nil
360 }
361
362
363
364
365
366
367
368
369
370 func readCookies(h Header, filter string) []*Cookie {
371 lines := h["Cookie"]
372 if len(lines) == 0 {
373 return []*Cookie{}
374 }
375
376
377
378
379 cookieCount := 0
380 for _, line := range lines {
381 cookieCount += strings.Count(line, ";") + 1
382 }
383 if !cookieNumWithinMax(cookieCount) {
384 return []*Cookie{}
385 }
386
387 cookies := make([]*Cookie, 0, len(lines)+strings.Count(lines[0], ";"))
388 for _, line := range lines {
389 line = textproto.TrimString(line)
390
391 var part string
392 for len(line) > 0 {
393 part, line, _ = strings.Cut(line, ";")
394 part = textproto.TrimString(part)
395 if part == "" {
396 continue
397 }
398 name, val, _ := strings.Cut(part, "=")
399 name = textproto.TrimString(name)
400 if !isToken(name) {
401 continue
402 }
403 if filter != "" && filter != name {
404 continue
405 }
406 val, quoted, ok := parseCookieValue(val, true)
407 if !ok {
408 continue
409 }
410 cookies = append(cookies, &Cookie{Name: name, Value: val, Quoted: quoted})
411 }
412 }
413 return cookies
414 }
415
416
417 func validCookieDomain(v string) bool {
418 if isCookieDomainName(v) {
419 return true
420 }
421 if net.ParseIP(v) != nil && !strings.Contains(v, ":") {
422 return true
423 }
424 return false
425 }
426
427
428 func validCookieExpires(t time.Time) bool {
429
430 return t.Year() >= 1601
431 }
432
433
434
435
436 func isCookieDomainName(s string) bool {
437 if len(s) == 0 {
438 return false
439 }
440 if len(s) > 255 {
441 return false
442 }
443
444 if s[0] == '.' {
445
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 strings.ContainsAny(v, " ,") || quoted {
512 return `"` + v + `"`
513 }
514 return v
515 }
516
517 func validCookieValueByte(b byte) bool {
518 return 0x20 <= b && b < 0x7f && b != '"' && b != ';' && b != '\\'
519 }
520
521
522
523 func sanitizeCookiePath(v string) string {
524 return sanitizeOrWarn("Cookie.Path", validCookiePathByte, v)
525 }
526
527 func validCookiePathByte(b byte) bool {
528 return 0x20 <= b && b < 0x7f && b != ';'
529 }
530
531 func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string {
532 ok := true
533 for i := 0; i < len(v); i++ {
534 if valid(v[i]) {
535 continue
536 }
537 log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName)
538 ok = false
539 break
540 }
541 if ok {
542 return v
543 }
544 buf := make([]byte, 0, len(v))
545 for i := 0; i < len(v); i++ {
546 if b := v[i]; valid(b) {
547 buf = append(buf, b)
548 }
549 }
550 return string(buf)
551 }
552
553
554
555
556
557
558
559
560
561
562 func parseCookieValue(raw string, allowDoubleQuote bool) (value string, quoted, ok bool) {
563
564 if allowDoubleQuote && len(raw) > 1 && raw[0] == '"' && raw[len(raw)-1] == '"' {
565 raw = raw[1 : len(raw)-1]
566 quoted = true
567 }
568 for i := 0; i < len(raw); i++ {
569 if !validCookieValueByte(raw[i]) {
570 return "", quoted, false
571 }
572 }
573 return raw, quoted, true
574 }
575
View as plain text