github.com/hxx258456/ccgo@v0.0.5-0.20230213014102-48b35f46f66f/gmhttp/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 gmhttp 6 7 import ( 8 "log" 9 "net" 10 "net/textproto" 11 "strconv" 12 "strings" 13 "time" 14 15 "github.com/hxx258456/ccgo/gmhttp/internal/ascii" 16 ) 17 18 // A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an 19 // HTTP response or the Cookie header of an HTTP request. 20 // 21 // See https://tools.ietf.org/html/rfc6265 for details. 22 type Cookie struct { 23 Name string 24 Value string 25 26 Path string // optional 27 Domain string // optional 28 Expires time.Time // optional 29 RawExpires string // for reading cookies only 30 31 // MaxAge=0 means no 'Max-Age' attribute specified. 32 // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' 33 // MaxAge>0 means Max-Age attribute present and given in seconds 34 MaxAge int 35 Secure bool 36 HttpOnly bool 37 SameSite SameSite 38 Raw string 39 Unparsed []string // Raw text of unparsed attribute-value pairs 40 } 41 42 // SameSite allows a server to define a cookie attribute making it impossible for 43 // the browser to send this cookie along with cross-site requests. The main 44 // goal is to mitigate the risk of cross-origin information leakage, and provide 45 // some protection against cross-site request forgery attacks. 46 // 47 // See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details. 48 type SameSite int 49 50 const ( 51 SameSiteDefaultMode SameSite = iota + 1 52 SameSiteLaxMode 53 SameSiteStrictMode 54 SameSiteNoneMode 55 ) 56 57 // readSetCookies parses all "Set-Cookie" values from 58 // the header h and returns the successfully parsed Cookies. 59 func readSetCookies(h Header) []*Cookie { 60 cookieCount := len(h["Set-Cookie"]) 61 if cookieCount == 0 { 62 return []*Cookie{} 63 } 64 cookies := make([]*Cookie, 0, cookieCount) 65 for _, line := range h["Set-Cookie"] { 66 parts := strings.Split(textproto.TrimString(line), ";") 67 if len(parts) == 1 && parts[0] == "" { 68 continue 69 } 70 parts[0] = textproto.TrimString(parts[0]) 71 j := strings.Index(parts[0], "=") 72 if j < 0 { 73 continue 74 } 75 name, value := parts[0][:j], parts[0][j+1:] 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 := parts[i], "" 95 if j := strings.Index(attr, "="); j >= 0 { 96 attr, val = attr[:j], attr[j+1:] 97 } 98 lowerAttr, isASCII := ascii.ToLower(attr) 99 if !isASCII { 100 continue 101 } 102 val, ok = parseCookieValue(val, false) 103 if !ok { 104 c.Unparsed = append(c.Unparsed, parts[i]) 105 continue 106 } 107 108 switch lowerAttr { 109 case "samesite": 110 lowerVal, ascii := ascii.ToLower(val) 111 if !ascii { 112 c.SameSite = SameSiteDefaultMode 113 continue 114 } 115 switch lowerVal { 116 case "lax": 117 c.SameSite = SameSiteLaxMode 118 case "strict": 119 c.SameSite = SameSiteStrictMode 120 case "none": 121 c.SameSite = SameSiteNoneMode 122 default: 123 c.SameSite = SameSiteDefaultMode 124 } 125 continue 126 case "secure": 127 c.Secure = true 128 continue 129 case "httponly": 130 c.HttpOnly = true 131 continue 132 case "domain": 133 c.Domain = val 134 continue 135 case "max-age": 136 secs, err := strconv.Atoi(val) 137 if err != nil || secs != 0 && val[0] == '0' { 138 break 139 } 140 if secs <= 0 { 141 secs = -1 142 } 143 c.MaxAge = secs 144 continue 145 case "expires": 146 c.RawExpires = val 147 exptime, err := time.Parse(time.RFC1123, val) 148 if err != nil { 149 exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", val) 150 if err != nil { 151 c.Expires = time.Time{} 152 break 153 } 154 } 155 c.Expires = exptime.UTC() 156 continue 157 case "path": 158 c.Path = val 159 continue 160 } 161 c.Unparsed = append(c.Unparsed, parts[i]) 162 } 163 cookies = append(cookies, c) 164 } 165 return cookies 166 } 167 168 // SetCookie adds a Set-Cookie header to the provided ResponseWriter's headers. 169 // The provided cookie must have a valid Name. Invalid cookies may be 170 // silently dropped. 171 func SetCookie(w ResponseWriter, cookie *Cookie) { 172 if v := cookie.String(); v != "" { 173 w.Header().Add("Set-Cookie", v) 174 } 175 } 176 177 // String returns the serialization of the cookie for use in a Cookie 178 // header (if only Name and Value are set) or a Set-Cookie response 179 // header (if other fields are set). 180 // If c is nil or c.Name is invalid, the empty string is returned. 181 func (c *Cookie) String() string { 182 if c == nil || !isCookieNameValid(c.Name) { 183 return "" 184 } 185 // extraCookieLength derived from typical length of cookie attributes 186 // see RFC 6265 Sec 4.1. 187 const extraCookieLength = 110 188 var b strings.Builder 189 b.Grow(len(c.Name) + len(c.Value) + len(c.Domain) + len(c.Path) + extraCookieLength) 190 b.WriteString(c.Name) 191 b.WriteRune('=') 192 b.WriteString(sanitizeCookieValue(c.Value)) 193 194 if len(c.Path) > 0 { 195 b.WriteString("; Path=") 196 b.WriteString(sanitizeCookiePath(c.Path)) 197 } 198 if len(c.Domain) > 0 { 199 if validCookieDomain(c.Domain) { 200 // A c.Domain containing illegal characters is not 201 // sanitized but simply dropped which turns the cookie 202 // into a host-only cookie. A leading dot is okay 203 // but won't be sent. 204 d := c.Domain 205 if d[0] == '.' { 206 d = d[1:] 207 } 208 b.WriteString("; Domain=") 209 b.WriteString(d) 210 } else { 211 log.Printf("github.com/hxx258456/ccgo/gmhttp: invalid Cookie.Domain %q; dropping domain attribute", c.Domain) 212 } 213 } 214 var buf [len(TimeFormat)]byte 215 if validCookieExpires(c.Expires) { 216 b.WriteString("; Expires=") 217 b.Write(c.Expires.UTC().AppendFormat(buf[:0], TimeFormat)) 218 } 219 if c.MaxAge > 0 { 220 b.WriteString("; Max-Age=") 221 b.Write(strconv.AppendInt(buf[:0], int64(c.MaxAge), 10)) 222 } else if c.MaxAge < 0 { 223 b.WriteString("; Max-Age=0") 224 } 225 if c.HttpOnly { 226 b.WriteString("; HttpOnly") 227 } 228 if c.Secure { 229 b.WriteString("; Secure") 230 } 231 switch c.SameSite { 232 case SameSiteDefaultMode: 233 // Skip, default mode is obtained by not emitting the attribute. 234 case SameSiteNoneMode: 235 b.WriteString("; SameSite=None") 236 case SameSiteLaxMode: 237 b.WriteString("; SameSite=Lax") 238 case SameSiteStrictMode: 239 b.WriteString("; SameSite=Strict") 240 } 241 return b.String() 242 } 243 244 // readCookies parses all "Cookie" values from the header h and 245 // returns the successfully parsed Cookies. 246 // 247 // if filter isn't empty, only cookies of that name are returned 248 func readCookies(h Header, filter string) []*Cookie { 249 lines := h["Cookie"] 250 if len(lines) == 0 { 251 return []*Cookie{} 252 } 253 254 cookies := make([]*Cookie, 0, len(lines)+strings.Count(lines[0], ";")) 255 for _, line := range lines { 256 line = textproto.TrimString(line) 257 258 var part string 259 for len(line) > 0 { // continue since we have rest 260 if splitIndex := strings.Index(line, ";"); splitIndex > 0 { 261 part, line = line[:splitIndex], line[splitIndex+1:] 262 } else { 263 part, line = line, "" 264 } 265 part = textproto.TrimString(part) 266 if len(part) == 0 { 267 continue 268 } 269 name, val := part, "" 270 if j := strings.Index(part, "="); j >= 0 { 271 name, val = name[:j], name[j+1:] 272 } 273 if !isCookieNameValid(name) { 274 continue 275 } 276 if filter != "" && filter != name { 277 continue 278 } 279 val, ok := parseCookieValue(val, true) 280 if !ok { 281 continue 282 } 283 cookies = append(cookies, &Cookie{Name: name, Value: val}) 284 } 285 } 286 return cookies 287 } 288 289 // validCookieDomain reports whether v is a valid cookie domain-value. 290 func validCookieDomain(v string) bool { 291 if isCookieDomainName(v) { 292 return true 293 } 294 if net.ParseIP(v) != nil && !strings.Contains(v, ":") { 295 return true 296 } 297 return false 298 } 299 300 // validCookieExpires reports whether v is a valid cookie expires-value. 301 func validCookieExpires(t time.Time) bool { 302 // IETF RFC 6265 Section 5.1.1.5, the year must not be less than 1601 303 return t.Year() >= 1601 304 } 305 306 // isCookieDomainName reports whether s is a valid domain name or a valid 307 // domain name with a leading dot '.'. It is almost a direct copy of 308 // package net's isDomainName. 309 func isCookieDomainName(s string) bool { 310 if len(s) == 0 { 311 return false 312 } 313 if len(s) > 255 { 314 return false 315 } 316 317 if s[0] == '.' { 318 // A cookie a domain attribute may start with a leading dot. 319 s = s[1:] 320 } 321 last := byte('.') 322 ok := false // Ok once we've seen a letter. 323 partlen := 0 324 for i := 0; i < len(s); i++ { 325 c := s[i] 326 switch { 327 default: 328 return false 329 case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z': 330 // No '_' allowed here (in contrast to package net). 331 ok = true 332 partlen++ 333 case '0' <= c && c <= '9': 334 // fine 335 partlen++ 336 case c == '-': 337 // Byte before dash cannot be dot. 338 if last == '.' { 339 return false 340 } 341 partlen++ 342 case c == '.': 343 // Byte before dot cannot be dot, dash. 344 if last == '.' || last == '-' { 345 return false 346 } 347 if partlen > 63 || partlen == 0 { 348 return false 349 } 350 partlen = 0 351 } 352 last = c 353 } 354 if last == '-' || partlen > 63 { 355 return false 356 } 357 358 return ok 359 } 360 361 var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-") 362 363 func sanitizeCookieName(n string) string { 364 return cookieNameSanitizer.Replace(n) 365 } 366 367 // sanitizeCookieValue produces a suitable cookie-value from v. 368 // https://tools.ietf.org/html/rfc6265#section-4.1.1 369 // cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) 370 // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E 371 // ; US-ASCII characters excluding CTLs, 372 // ; whitespace DQUOTE, comma, semicolon, 373 // ; and backslash 374 // We loosen this as spaces and commas are common in cookie values 375 // but we produce a quoted cookie-value if and only if v contains 376 // commas or spaces. 377 // See https://golang.org/issue/7243 for the discussion. 378 func sanitizeCookieValue(v string) string { 379 v = sanitizeOrWarn("Cookie.Value", validCookieValueByte, v) 380 if len(v) == 0 { 381 return v 382 } 383 if strings.IndexByte(v, ' ') >= 0 || strings.IndexByte(v, ',') >= 0 { 384 return `"` + v + `"` 385 } 386 return v 387 } 388 389 func validCookieValueByte(b byte) bool { 390 return 0x20 <= b && b < 0x7f && b != '"' && b != ';' && b != '\\' 391 } 392 393 // path-av = "Path=" path-value 394 // path-value = <any CHAR except CTLs or ";"> 395 func sanitizeCookiePath(v string) string { 396 return sanitizeOrWarn("Cookie.Path", validCookiePathByte, v) 397 } 398 399 func validCookiePathByte(b byte) bool { 400 return 0x20 <= b && b < 0x7f && b != ';' 401 } 402 403 func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string { 404 ok := true 405 for i := 0; i < len(v); i++ { 406 if valid(v[i]) { 407 continue 408 } 409 log.Printf("github.com/hxx258456/ccgo/gmhttp: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName) 410 ok = false 411 break 412 } 413 if ok { 414 return v 415 } 416 buf := make([]byte, 0, len(v)) 417 for i := 0; i < len(v); i++ { 418 if b := v[i]; valid(b) { 419 buf = append(buf, b) 420 } 421 } 422 return string(buf) 423 } 424 425 func parseCookieValue(raw string, allowDoubleQuote bool) (string, bool) { 426 // Strip the quotes, if present. 427 if allowDoubleQuote && len(raw) > 1 && raw[0] == '"' && raw[len(raw)-1] == '"' { 428 raw = raw[1 : len(raw)-1] 429 } 430 for i := 0; i < len(raw); i++ { 431 if !validCookieValueByte(raw[i]) { 432 return "", false 433 } 434 } 435 return raw, true 436 } 437 438 func isCookieNameValid(raw string) bool { 439 if raw == "" { 440 return false 441 } 442 return strings.IndexFunc(raw, isNotToken) < 0 443 }