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