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