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 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