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