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