github.com/Kolosok86/http@v0.1.2/cookie.go (about) 1 // Copyright 2009 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package http 6 7 import ( 8 "errors" 9 "fmt" 10 "log" 11 "net" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/Kolosok86/http/internal/ascii" 17 "github.com/Kolosok86/http/textproto" 18 ) 19 20 // A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an 21 // HTTP response or the Cookie header of an HTTP request. 22 // 23 // See https://tools.ietf.org/html/rfc6265 for details. 24 type Cookie struct { 25 Name string 26 Value string 27 28 Path string // optional 29 Domain string // optional 30 Expires time.Time // optional 31 RawExpires string // for reading cookies only 32 33 // MaxAge=0 means no 'Max-Age' attribute specified. 34 // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' 35 // MaxAge>0 means Max-Age attribute present and given in seconds 36 MaxAge int 37 Secure bool 38 HttpOnly bool 39 SameSite SameSite 40 Raw string 41 Unparsed []string // Raw text of unparsed attribute-value pairs 42 } 43 44 // SameSite allows a server to define a cookie attribute making it impossible for 45 // the browser to send this cookie along with cross-site requests. The main 46 // goal is to mitigate the risk of cross-origin information leakage, and provide 47 // some protection against cross-site request forgery attacks. 48 // 49 // See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details. 50 type SameSite int 51 52 const ( 53 SameSiteDefaultMode SameSite = iota + 1 54 SameSiteLaxMode 55 SameSiteStrictMode 56 SameSiteNoneMode 57 ) 58 59 // readSetCookies parses all "Set-Cookie" Values from 60 // the header h and returns the successfully parsed Cookies. 61 func readSetCookies(h Header) []*Cookie { 62 cookieCount := len(h["Set-Cookie"]) 63 if cookieCount == 0 { 64 return []*Cookie{} 65 } 66 cookies := make([]*Cookie, 0, cookieCount) 67 for _, line := range h["Set-Cookie"] { 68 parts := strings.Split(textproto.TrimString(line), ";") 69 if len(parts) == 1 && parts[0] == "" { 70 continue 71 } 72 parts[0] = textproto.TrimString(parts[0]) 73 name, value, ok := strings.Cut(parts[0], "=") 74 if !ok { 75 continue 76 } 77 name = textproto.TrimString(name) 78 if !isCookieNameValid(name) { 79 continue 80 } 81 value, ok = parseCookieValue(value, true) 82 if !ok { 83 continue 84 } 85 c := &Cookie{ 86 Name: name, 87 Value: value, 88 Raw: line, 89 } 90 for i := 1; i < len(parts); i++ { 91 parts[i] = textproto.TrimString(parts[i]) 92 if len(parts[i]) == 0 { 93 continue 94 } 95 96 attr, val, _ := strings.Cut(parts[i], "=") 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 // SetCookie adds a Set-Cookie header to the provided ResponseWriter's headers. 168 // The provided cookie must have a valid Name. Invalid cookies may be 169 // silently dropped. 170 func SetCookie(w ResponseWriter, cookie *Cookie) { 171 if v := cookie.String(); v != "" { 172 w.Header().Add("Set-Cookie", v) 173 } 174 } 175 176 // String returns the serialization of the cookie for use in a Cookie 177 // header (if only Name and Value are set) or a Set-Cookie response 178 // header (if other fields are set). 179 // If c is nil or c.Name is invalid, the empty string is returned. 180 func (c *Cookie) String() string { 181 if c == nil || !isCookieNameValid(c.Name) { 182 return "" 183 } 184 // extraCookieLength derived from typical length of cookie attributes 185 // see RFC 6265 Sec 4.1. 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 // A c.Domain containing illegal characters is not 200 // sanitized but simply dropped which turns the cookie 201 // into a host-only cookie. A leading dot is okay 202 // but won't be sent. 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 // Skip, default mode is obtained by not emitting the attribute. 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 // Valid reports whether the cookie is valid. 244 func (c *Cookie) Valid() error { 245 if c == nil { 246 return errors.New("http: nil Cookie") 247 } 248 if !isCookieNameValid(c.Name) { 249 return errors.New("http: invalid Cookie.Name") 250 } 251 if !c.Expires.IsZero() && !validCookieExpires(c.Expires) { 252 return errors.New("http: invalid Cookie.Expires") 253 } 254 for i := 0; i < len(c.Value); i++ { 255 if !validCookieValueByte(c.Value[i]) { 256 return fmt.Errorf("http: invalid byte %q in Cookie.Value", c.Value[i]) 257 } 258 } 259 if len(c.Path) > 0 { 260 for i := 0; i < len(c.Path); i++ { 261 if !validCookiePathByte(c.Path[i]) { 262 return fmt.Errorf("http: invalid byte %q in Cookie.Path", c.Path[i]) 263 } 264 } 265 } 266 if len(c.Domain) > 0 { 267 if !validCookieDomain(c.Domain) { 268 return errors.New("http: invalid Cookie.Domain") 269 } 270 } 271 return nil 272 } 273 274 // readCookies parses all "Cookie" Values from the header h and 275 // returns the successfully parsed Cookies. 276 // 277 // if filter isn't empty, only cookies of that name are returned. 278 func readCookies(h Header, filter string) []*Cookie { 279 lines := h["Cookie"] 280 if len(lines) == 0 { 281 return []*Cookie{} 282 } 283 284 cookies := make([]*Cookie, 0, len(lines)+strings.Count(lines[0], ";")) 285 for _, line := range lines { 286 line = textproto.TrimString(line) 287 288 var part string 289 for len(line) > 0 { // continue since we have rest 290 part, line, _ = strings.Cut(line, ";") 291 part = textproto.TrimString(part) 292 if part == "" { 293 continue 294 } 295 name, val, _ := strings.Cut(part, "=") 296 name = textproto.TrimString(name) 297 if !isCookieNameValid(name) { 298 continue 299 } 300 if filter != "" && filter != name { 301 continue 302 } 303 val, ok := parseCookieValue(val, true) 304 if !ok { 305 continue 306 } 307 cookies = append(cookies, &Cookie{Name: name, Value: val}) 308 } 309 } 310 return cookies 311 } 312 313 // validCookieDomain reports whether v is a valid cookie domain-value. 314 func validCookieDomain(v string) bool { 315 if isCookieDomainName(v) { 316 return true 317 } 318 if net.ParseIP(v) != nil && !strings.Contains(v, ":") { 319 return true 320 } 321 return false 322 } 323 324 // validCookieExpires reports whether v is a valid cookie expires-value. 325 func validCookieExpires(t time.Time) bool { 326 // IETF RFC 6265 Section 5.1.1.5, the year must not be less than 1601 327 return t.Year() >= 1601 328 } 329 330 // isCookieDomainName reports whether s is a valid domain name or a valid 331 // domain name with a leading dot '.'. It is almost a direct copy of 332 // package net's isDomainName. 333 func isCookieDomainName(s string) bool { 334 if len(s) == 0 { 335 return false 336 } 337 if len(s) > 255 { 338 return false 339 } 340 341 if s[0] == '.' { 342 // A cookie a domain attribute may start with a leading dot. 343 s = s[1:] 344 } 345 last := byte('.') 346 ok := false // Ok once we've seen a letter. 347 partlen := 0 348 for i := 0; i < len(s); i++ { 349 c := s[i] 350 switch { 351 default: 352 return false 353 case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z': 354 // No '_' allowed here (in contrast to package net). 355 ok = true 356 partlen++ 357 case '0' <= c && c <= '9': 358 // fine 359 partlen++ 360 case c == '-': 361 // Byte before dash cannot be dot. 362 if last == '.' { 363 return false 364 } 365 partlen++ 366 case c == '.': 367 // Byte before dot cannot be dot, dash. 368 if last == '.' || last == '-' { 369 return false 370 } 371 if partlen > 63 || partlen == 0 { 372 return false 373 } 374 partlen = 0 375 } 376 last = c 377 } 378 if last == '-' || partlen > 63 { 379 return false 380 } 381 382 return ok 383 } 384 385 var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-") 386 387 func sanitizeCookieName(n string) string { 388 return cookieNameSanitizer.Replace(n) 389 } 390 391 // sanitizeCookieValue produces a suitable cookie-value from v. 392 // https://tools.ietf.org/html/rfc6265#section-4.1.1 393 // 394 // cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) 395 // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E 396 // ; US-ASCII characters excluding CTLs, 397 // ; whitespace DQUOTE, comma, semicolon, 398 // ; and backslash 399 // 400 // We loosen this as spaces and commas are common in cookie Values 401 // but we produce a quoted cookie-value if and only if v contains 402 // commas or spaces. 403 // See https://golang.org/issue/7243 for the discussion. 404 func sanitizeCookieValue(v string) string { 405 v = sanitizeOrWarn("Cookie.Value", validCookieValueByte, v) 406 if len(v) == 0 { 407 return v 408 } 409 if strings.ContainsAny(v, " ,") { 410 return `"` + v + `"` 411 } 412 return v 413 } 414 415 func validCookieValueByte(b byte) bool { 416 return 0x20 <= b && b < 0x7f && b != '"' && b != ';' && b != '\\' 417 } 418 419 // path-av = "Path=" path-value 420 // path-value = <any CHAR except CTLs or ";"> 421 func sanitizeCookiePath(v string) string { 422 return sanitizeOrWarn("Cookie.Path", validCookiePathByte, v) 423 } 424 425 func validCookiePathByte(b byte) bool { 426 return 0x20 <= b && b < 0x7f && b != ';' 427 } 428 429 func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string { 430 ok := true 431 for i := 0; i < len(v); i++ { 432 if valid(v[i]) { 433 continue 434 } 435 log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName) 436 ok = false 437 break 438 } 439 if ok { 440 return v 441 } 442 buf := make([]byte, 0, len(v)) 443 for i := 0; i < len(v); i++ { 444 if b := v[i]; valid(b) { 445 buf = append(buf, b) 446 } 447 } 448 return string(buf) 449 } 450 451 func parseCookieValue(raw string, allowDoubleQuote bool) (string, bool) { 452 // Strip the quotes, if present. 453 if allowDoubleQuote && len(raw) > 1 && raw[0] == '"' && raw[len(raw)-1] == '"' { 454 raw = raw[1 : len(raw)-1] 455 } 456 for i := 0; i < len(raw); i++ { 457 if !validCookieValueByte(raw[i]) { 458 return "", false 459 } 460 } 461 return raw, true 462 } 463 464 func isCookieNameValid(raw string) bool { 465 if raw == "" { 466 return false 467 } 468 return strings.IndexFunc(raw, isNotToken) < 0 469 }