1// Copyright 2010 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 5package http 6 7import ( 8 "encoding/json" 9 "errors" 10 "fmt" 11 "log" 12 "os" 13 "reflect" 14 "strings" 15 "testing" 16 "time" 17) 18 19var writeSetCookiesTests = []struct { 20 Cookie *Cookie 21 Raw string 22}{ 23 { 24 &Cookie{Name: "cookie-1", Value: "v$1"}, 25 "cookie-1=v$1", 26 }, 27 { 28 &Cookie{Name: "cookie-2", Value: "two", MaxAge: 3600}, 29 "cookie-2=two; Max-Age=3600", 30 }, 31 { 32 &Cookie{Name: "cookie-3", Value: "three", Domain: ".example.com"}, 33 "cookie-3=three; Domain=example.com", 34 }, 35 { 36 &Cookie{Name: "cookie-4", Value: "four", Path: "/restricted/"}, 37 "cookie-4=four; Path=/restricted/", 38 }, 39 { 40 &Cookie{Name: "cookie-5", Value: "five", Domain: "wrong;bad.abc"}, 41 "cookie-5=five", 42 }, 43 { 44 &Cookie{Name: "cookie-6", Value: "six", Domain: "bad-.abc"}, 45 "cookie-6=six", 46 }, 47 { 48 &Cookie{Name: "cookie-7", Value: "seven", Domain: "127.0.0.1"}, 49 "cookie-7=seven; Domain=127.0.0.1", 50 }, 51 { 52 &Cookie{Name: "cookie-8", Value: "eight", Domain: "::1"}, 53 "cookie-8=eight", 54 }, 55 { 56 &Cookie{Name: "cookie-9", Value: "expiring", Expires: time.Unix(1257894000, 0)}, 57 "cookie-9=expiring; Expires=Tue, 10 Nov 2009 23:00:00 GMT", 58 }, 59 // According to IETF 6265 Section 5.1.1.5, the year cannot be less than 1601 60 { 61 &Cookie{Name: "cookie-10", Value: "expiring-1601", Expires: time.Date(1601, 1, 1, 1, 1, 1, 1, time.UTC)}, 62 "cookie-10=expiring-1601; Expires=Mon, 01 Jan 1601 01:01:01 GMT", 63 }, 64 { 65 &Cookie{Name: "cookie-11", Value: "invalid-expiry", Expires: time.Date(1600, 1, 1, 1, 1, 1, 1, time.UTC)}, 66 "cookie-11=invalid-expiry", 67 }, 68 { 69 &Cookie{Name: "cookie-12", Value: "samesite-default", SameSite: SameSiteDefaultMode}, 70 "cookie-12=samesite-default", 71 }, 72 { 73 &Cookie{Name: "cookie-13", Value: "samesite-lax", SameSite: SameSiteLaxMode}, 74 "cookie-13=samesite-lax; SameSite=Lax", 75 }, 76 { 77 &Cookie{Name: "cookie-14", Value: "samesite-strict", SameSite: SameSiteStrictMode}, 78 "cookie-14=samesite-strict; SameSite=Strict", 79 }, 80 { 81 &Cookie{Name: "cookie-15", Value: "samesite-none", SameSite: SameSiteNoneMode}, 82 "cookie-15=samesite-none; SameSite=None", 83 }, 84 { 85 &Cookie{Name: "cookie-16", Value: "partitioned", SameSite: SameSiteNoneMode, Secure: true, Path: "/", Partitioned: true}, 86 "cookie-16=partitioned; Path=/; Secure; SameSite=None; Partitioned", 87 }, 88 // The "special" cookies have values containing commas or spaces which 89 // are disallowed by RFC 6265 but are common in the wild. 90 { 91 &Cookie{Name: "special-1", Value: "a z"}, 92 `special-1="a z"`, 93 }, 94 { 95 &Cookie{Name: "special-2", Value: " z"}, 96 `special-2=" z"`, 97 }, 98 { 99 &Cookie{Name: "special-3", Value: "a "}, 100 `special-3="a "`, 101 }, 102 { 103 &Cookie{Name: "special-4", Value: " "}, 104 `special-4=" "`, 105 }, 106 { 107 &Cookie{Name: "special-5", Value: "a,z"}, 108 `special-5="a,z"`, 109 }, 110 { 111 &Cookie{Name: "special-6", Value: ",z"}, 112 `special-6=",z"`, 113 }, 114 { 115 &Cookie{Name: "special-7", Value: "a,"}, 116 `special-7="a,"`, 117 }, 118 { 119 &Cookie{Name: "special-8", Value: ","}, 120 `special-8=","`, 121 }, 122 { 123 &Cookie{Name: "empty-value", Value: ""}, 124 `empty-value=`, 125 }, 126 { 127 nil, 128 ``, 129 }, 130 { 131 &Cookie{Name: ""}, 132 ``, 133 }, 134 { 135 &Cookie{Name: "\t"}, 136 ``, 137 }, 138 { 139 &Cookie{Name: "\r"}, 140 ``, 141 }, 142 { 143 &Cookie{Name: "a\nb", Value: "v"}, 144 ``, 145 }, 146 { 147 &Cookie{Name: "a\nb", Value: "v"}, 148 ``, 149 }, 150 { 151 &Cookie{Name: "a\rb", Value: "v"}, 152 ``, 153 }, 154 // Quoted values (issue #46443) 155 { 156 &Cookie{Name: "cookie", Value: "quoted", Quoted: true}, 157 `cookie="quoted"`, 158 }, 159 { 160 &Cookie{Name: "cookie", Value: "quoted with spaces", Quoted: true}, 161 `cookie="quoted with spaces"`, 162 }, 163 { 164 &Cookie{Name: "cookie", Value: "quoted,with,commas", Quoted: true}, 165 `cookie="quoted,with,commas"`, 166 }, 167} 168 169func TestWriteSetCookies(t *testing.T) { 170 defer log.SetOutput(os.Stderr) 171 var logbuf strings.Builder 172 log.SetOutput(&logbuf) 173 174 for i, tt := range writeSetCookiesTests { 175 if g, e := tt.Cookie.String(), tt.Raw; g != e { 176 t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, e, g) 177 } 178 } 179 180 if got, sub := logbuf.String(), "dropping domain attribute"; !strings.Contains(got, sub) { 181 t.Errorf("Expected substring %q in log output. Got:\n%s", sub, got) 182 } 183} 184 185type headerOnlyResponseWriter Header 186 187func (ho headerOnlyResponseWriter) Header() Header { 188 return Header(ho) 189} 190 191func (ho headerOnlyResponseWriter) Write([]byte) (int, error) { 192 panic("NOIMPL") 193} 194 195func (ho headerOnlyResponseWriter) WriteHeader(int) { 196 panic("NOIMPL") 197} 198 199func TestSetCookie(t *testing.T) { 200 m := make(Header) 201 SetCookie(headerOnlyResponseWriter(m), &Cookie{Name: "cookie-1", Value: "one", Path: "/restricted/"}) 202 SetCookie(headerOnlyResponseWriter(m), &Cookie{Name: "cookie-2", Value: "two", MaxAge: 3600}) 203 if l := len(m["Set-Cookie"]); l != 2 { 204 t.Fatalf("expected %d cookies, got %d", 2, l) 205 } 206 if g, e := m["Set-Cookie"][0], "cookie-1=one; Path=/restricted/"; g != e { 207 t.Errorf("cookie #1: want %q, got %q", e, g) 208 } 209 if g, e := m["Set-Cookie"][1], "cookie-2=two; Max-Age=3600"; g != e { 210 t.Errorf("cookie #2: want %q, got %q", e, g) 211 } 212} 213 214var addCookieTests = []struct { 215 Cookies []*Cookie 216 Raw string 217}{ 218 { 219 []*Cookie{}, 220 "", 221 }, 222 { 223 []*Cookie{{Name: "cookie-1", Value: "v$1"}}, 224 "cookie-1=v$1", 225 }, 226 { 227 []*Cookie{ 228 {Name: "cookie-1", Value: "v$1"}, 229 {Name: "cookie-2", Value: "v$2"}, 230 {Name: "cookie-3", Value: "v$3"}, 231 }, 232 "cookie-1=v$1; cookie-2=v$2; cookie-3=v$3", 233 }, 234 // Quoted values (issue #46443) 235 { 236 []*Cookie{ 237 {Name: "cookie-1", Value: "quoted", Quoted: true}, 238 {Name: "cookie-2", Value: "quoted with spaces", Quoted: true}, 239 {Name: "cookie-3", Value: "quoted,with,commas", Quoted: true}, 240 }, 241 `cookie-1="quoted"; cookie-2="quoted with spaces"; cookie-3="quoted,with,commas"`, 242 }, 243} 244 245func TestAddCookie(t *testing.T) { 246 for i, tt := range addCookieTests { 247 req, _ := NewRequest("GET", "http://example.com/", nil) 248 for _, c := range tt.Cookies { 249 req.AddCookie(c) 250 } 251 if g := req.Header.Get("Cookie"); g != tt.Raw { 252 t.Errorf("Test %d:\nwant: %s\n got: %s\n", i, tt.Raw, g) 253 } 254 } 255} 256 257var readSetCookiesTests = []struct { 258 Header Header 259 Cookies []*Cookie 260}{ 261 { 262 Header{"Set-Cookie": {"Cookie-1=v$1"}}, 263 []*Cookie{{Name: "Cookie-1", Value: "v$1", Raw: "Cookie-1=v$1"}}, 264 }, 265 { 266 Header{"Set-Cookie": {"NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly"}}, 267 []*Cookie{{ 268 Name: "NID", 269 Value: "99=YsDT5i3E-CXax-", 270 Path: "/", 271 Domain: ".google.ch", 272 HttpOnly: true, 273 Expires: time.Date(2011, 11, 23, 1, 5, 3, 0, time.UTC), 274 RawExpires: "Wed, 23-Nov-2011 01:05:03 GMT", 275 Raw: "NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly", 276 }}, 277 }, 278 { 279 Header{"Set-Cookie": {".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly"}}, 280 []*Cookie{{ 281 Name: ".ASPXAUTH", 282 Value: "7E3AA", 283 Path: "/", 284 Expires: time.Date(2012, 3, 7, 14, 25, 6, 0, time.UTC), 285 RawExpires: "Wed, 07-Mar-2012 14:25:06 GMT", 286 HttpOnly: true, 287 Raw: ".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly", 288 }}, 289 }, 290 { 291 Header{"Set-Cookie": {"ASP.NET_SessionId=foo; path=/; HttpOnly"}}, 292 []*Cookie{{ 293 Name: "ASP.NET_SessionId", 294 Value: "foo", 295 Path: "/", 296 HttpOnly: true, 297 Raw: "ASP.NET_SessionId=foo; path=/; HttpOnly", 298 }}, 299 }, 300 { 301 Header{"Set-Cookie": {"samesitedefault=foo; SameSite"}}, 302 []*Cookie{{ 303 Name: "samesitedefault", 304 Value: "foo", 305 SameSite: SameSiteDefaultMode, 306 Raw: "samesitedefault=foo; SameSite", 307 }}, 308 }, 309 { 310 Header{"Set-Cookie": {"samesiteinvalidisdefault=foo; SameSite=invalid"}}, 311 []*Cookie{{ 312 Name: "samesiteinvalidisdefault", 313 Value: "foo", 314 SameSite: SameSiteDefaultMode, 315 Raw: "samesiteinvalidisdefault=foo; SameSite=invalid", 316 }}, 317 }, 318 { 319 Header{"Set-Cookie": {"samesitelax=foo; SameSite=Lax"}}, 320 []*Cookie{{ 321 Name: "samesitelax", 322 Value: "foo", 323 SameSite: SameSiteLaxMode, 324 Raw: "samesitelax=foo; SameSite=Lax", 325 }}, 326 }, 327 { 328 Header{"Set-Cookie": {"samesitestrict=foo; SameSite=Strict"}}, 329 []*Cookie{{ 330 Name: "samesitestrict", 331 Value: "foo", 332 SameSite: SameSiteStrictMode, 333 Raw: "samesitestrict=foo; SameSite=Strict", 334 }}, 335 }, 336 { 337 Header{"Set-Cookie": {"samesitenone=foo; SameSite=None"}}, 338 []*Cookie{{ 339 Name: "samesitenone", 340 Value: "foo", 341 SameSite: SameSiteNoneMode, 342 Raw: "samesitenone=foo; SameSite=None", 343 }}, 344 }, 345 // Make sure we can properly read back the Set-Cookie headers we create 346 // for values containing spaces or commas: 347 { 348 Header{"Set-Cookie": {`special-1=a z`}}, 349 []*Cookie{{Name: "special-1", Value: "a z", Raw: `special-1=a z`}}, 350 }, 351 { 352 Header{"Set-Cookie": {`special-2=" z"`}}, 353 []*Cookie{{Name: "special-2", Value: " z", Quoted: true, Raw: `special-2=" z"`}}, 354 }, 355 { 356 Header{"Set-Cookie": {`special-3="a "`}}, 357 []*Cookie{{Name: "special-3", Value: "a ", Quoted: true, Raw: `special-3="a "`}}, 358 }, 359 { 360 Header{"Set-Cookie": {`special-4=" "`}}, 361 []*Cookie{{Name: "special-4", Value: " ", Quoted: true, Raw: `special-4=" "`}}, 362 }, 363 { 364 Header{"Set-Cookie": {`special-5=a,z`}}, 365 []*Cookie{{Name: "special-5", Value: "a,z", Raw: `special-5=a,z`}}, 366 }, 367 { 368 Header{"Set-Cookie": {`special-6=",z"`}}, 369 []*Cookie{{Name: "special-6", Value: ",z", Quoted: true, Raw: `special-6=",z"`}}, 370 }, 371 { 372 Header{"Set-Cookie": {`special-7=a,`}}, 373 []*Cookie{{Name: "special-7", Value: "a,", Raw: `special-7=a,`}}, 374 }, 375 { 376 Header{"Set-Cookie": {`special-8=","`}}, 377 []*Cookie{{Name: "special-8", Value: ",", Quoted: true, Raw: `special-8=","`}}, 378 }, 379 // Make sure we can properly read back the Set-Cookie headers 380 // for names containing spaces: 381 { 382 Header{"Set-Cookie": {`special-9 =","`}}, 383 []*Cookie{{Name: "special-9", Value: ",", Quoted: true, Raw: `special-9 =","`}}, 384 }, 385 // Quoted values (issue #46443) 386 { 387 Header{"Set-Cookie": {`cookie="quoted"`}}, 388 []*Cookie{{Name: "cookie", Value: "quoted", Quoted: true, Raw: `cookie="quoted"`}}, 389 }, 390 391 // TODO(bradfitz): users have reported seeing this in the 392 // wild, but do browsers handle it? RFC 6265 just says "don't 393 // do that" (section 3) and then never mentions header folding 394 // again. 395 // Header{"Set-Cookie": {"ASP.NET_SessionId=foo; path=/; HttpOnly, .ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly"}}, 396} 397 398func toJSON(v any) string { 399 b, err := json.Marshal(v) 400 if err != nil { 401 return fmt.Sprintf("%#v", v) 402 } 403 return string(b) 404} 405 406func TestReadSetCookies(t *testing.T) { 407 for i, tt := range readSetCookiesTests { 408 for n := 0; n < 2; n++ { // to verify readSetCookies doesn't mutate its input 409 c := readSetCookies(tt.Header) 410 if !reflect.DeepEqual(c, tt.Cookies) { 411 t.Errorf("#%d readSetCookies: have\n%s\nwant\n%s\n", i, toJSON(c), toJSON(tt.Cookies)) 412 } 413 } 414 } 415} 416 417var readCookiesTests = []struct { 418 Header Header 419 Filter string 420 Cookies []*Cookie 421}{ 422 { 423 Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}}, 424 "", 425 []*Cookie{ 426 {Name: "Cookie-1", Value: "v$1"}, 427 {Name: "c2", Value: "v2"}, 428 }, 429 }, 430 { 431 Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}}, 432 "c2", 433 []*Cookie{ 434 {Name: "c2", Value: "v2"}, 435 }, 436 }, 437 { 438 Header{"Cookie": {"Cookie-1=v$1; c2=v2"}}, 439 "", 440 []*Cookie{ 441 {Name: "Cookie-1", Value: "v$1"}, 442 {Name: "c2", Value: "v2"}, 443 }, 444 }, 445 { 446 Header{"Cookie": {"Cookie-1=v$1; c2=v2"}}, 447 "c2", 448 []*Cookie{ 449 {Name: "c2", Value: "v2"}, 450 }, 451 }, 452 { 453 Header{"Cookie": {`Cookie-1="v$1"; c2="v2"`}}, 454 "", 455 []*Cookie{ 456 {Name: "Cookie-1", Value: "v$1", Quoted: true}, 457 {Name: "c2", Value: "v2", Quoted: true}, 458 }, 459 }, 460 { 461 Header{"Cookie": {`Cookie-1="v$1"; c2=v2;`}}, 462 "", 463 []*Cookie{ 464 {Name: "Cookie-1", Value: "v$1", Quoted: true}, 465 {Name: "c2", Value: "v2"}, 466 }, 467 }, 468 { 469 Header{"Cookie": {``}}, 470 "", 471 []*Cookie{}, 472 }, 473} 474 475func TestReadCookies(t *testing.T) { 476 for i, tt := range readCookiesTests { 477 for n := 0; n < 2; n++ { // to verify readCookies doesn't mutate its input 478 c := readCookies(tt.Header, tt.Filter) 479 if !reflect.DeepEqual(c, tt.Cookies) { 480 t.Errorf("#%d readCookies:\nhave: %s\nwant: %s\n", i, toJSON(c), toJSON(tt.Cookies)) 481 } 482 } 483 } 484} 485 486func TestSetCookieDoubleQuotes(t *testing.T) { 487 res := &Response{Header: Header{}} 488 res.Header.Add("Set-Cookie", `quoted0=none; max-age=30`) 489 res.Header.Add("Set-Cookie", `quoted1="cookieValue"; max-age=31`) 490 res.Header.Add("Set-Cookie", `quoted2=cookieAV; max-age="32"`) 491 res.Header.Add("Set-Cookie", `quoted3="both"; max-age="33"`) 492 got := res.Cookies() 493 want := []*Cookie{ 494 {Name: "quoted0", Value: "none", MaxAge: 30}, 495 {Name: "quoted1", Value: "cookieValue", MaxAge: 31}, 496 {Name: "quoted2", Value: "cookieAV"}, 497 {Name: "quoted3", Value: "both"}, 498 } 499 if len(got) != len(want) { 500 t.Fatalf("got %d cookies, want %d", len(got), len(want)) 501 } 502 for i, w := range want { 503 g := got[i] 504 if g.Name != w.Name || g.Value != w.Value || g.MaxAge != w.MaxAge { 505 t.Errorf("cookie #%d:\ngot %v\nwant %v", i, g, w) 506 } 507 } 508} 509 510func TestCookieSanitizeValue(t *testing.T) { 511 defer log.SetOutput(os.Stderr) 512 var logbuf strings.Builder 513 log.SetOutput(&logbuf) 514 515 tests := []struct { 516 in string 517 quoted bool 518 want string 519 }{ 520 {"foo", false, "foo"}, 521 {"foo;bar", false, "foobar"}, 522 {"foo\\bar", false, "foobar"}, 523 {"foo\"bar", false, "foobar"}, 524 {"\x00\x7e\x7f\x80", false, "\x7e"}, 525 {`withquotes`, true, `"withquotes"`}, 526 {`"withquotes"`, true, `"withquotes"`}, // double quotes are not valid octets 527 {"a z", false, `"a z"`}, 528 {" z", false, `" z"`}, 529 {"a ", false, `"a "`}, 530 {"a,z", false, `"a,z"`}, 531 {",z", false, `",z"`}, 532 {"a,", false, `"a,"`}, 533 } 534 for _, tt := range tests { 535 if got := sanitizeCookieValue(tt.in, tt.quoted); got != tt.want { 536 t.Errorf("sanitizeCookieValue(%q) = %q; want %q", tt.in, got, tt.want) 537 } 538 } 539 540 if got, sub := logbuf.String(), "dropping invalid bytes"; !strings.Contains(got, sub) { 541 t.Errorf("Expected substring %q in log output. Got:\n%s", sub, got) 542 } 543} 544 545func TestCookieSanitizePath(t *testing.T) { 546 defer log.SetOutput(os.Stderr) 547 var logbuf strings.Builder 548 log.SetOutput(&logbuf) 549 550 tests := []struct { 551 in, want string 552 }{ 553 {"/path", "/path"}, 554 {"/path with space/", "/path with space/"}, 555 {"/just;no;semicolon\x00orstuff/", "/justnosemicolonorstuff/"}, 556 } 557 for _, tt := range tests { 558 if got := sanitizeCookiePath(tt.in); got != tt.want { 559 t.Errorf("sanitizeCookiePath(%q) = %q; want %q", tt.in, got, tt.want) 560 } 561 } 562 563 if got, sub := logbuf.String(), "dropping invalid bytes"; !strings.Contains(got, sub) { 564 t.Errorf("Expected substring %q in log output. Got:\n%s", sub, got) 565 } 566} 567 568func TestCookieValid(t *testing.T) { 569 tests := []struct { 570 cookie *Cookie 571 valid bool 572 }{ 573 {nil, false}, 574 {&Cookie{Name: ""}, false}, 575 {&Cookie{Name: "invalid-value", Value: "foo\"bar"}, false}, 576 {&Cookie{Name: "invalid-path", Path: "/foo;bar/"}, false}, 577 {&Cookie{Name: "invalid-secure-for-partitioned", Value: "foo", Path: "/", Secure: false, Partitioned: true}, false}, 578 {&Cookie{Name: "invalid-domain", Domain: "example.com:80"}, false}, 579 {&Cookie{Name: "invalid-expiry", Value: "", Expires: time.Date(1600, 1, 1, 1, 1, 1, 1, time.UTC)}, false}, 580 {&Cookie{Name: "valid-empty"}, true}, 581 {&Cookie{Name: "valid-expires", Value: "foo", Path: "/bar", Domain: "example.com", Expires: time.Unix(0, 0)}, true}, 582 {&Cookie{Name: "valid-max-age", Value: "foo", Path: "/bar", Domain: "example.com", MaxAge: 60}, true}, 583 {&Cookie{Name: "valid-all-fields", Value: "foo", Path: "/bar", Domain: "example.com", Expires: time.Unix(0, 0), MaxAge: 0}, true}, 584 {&Cookie{Name: "valid-partitioned", Value: "foo", Path: "/", Secure: true, Partitioned: true}, true}, 585 } 586 587 for _, tt := range tests { 588 err := tt.cookie.Valid() 589 if err != nil && tt.valid { 590 t.Errorf("%#v.Valid() returned error %v; want nil", tt.cookie, err) 591 } 592 if err == nil && !tt.valid { 593 t.Errorf("%#v.Valid() returned nil; want error", tt.cookie) 594 } 595 } 596} 597 598func BenchmarkCookieString(b *testing.B) { 599 const wantCookieString = `cookie-9=i3e01nf61b6t23bvfmplnanol3; Path=/restricted/; Domain=example.com; Expires=Tue, 10 Nov 2009 23:00:00 GMT; Max-Age=3600` 600 c := &Cookie{ 601 Name: "cookie-9", 602 Value: "i3e01nf61b6t23bvfmplnanol3", 603 Expires: time.Unix(1257894000, 0), 604 Path: "/restricted/", 605 Domain: ".example.com", 606 MaxAge: 3600, 607 } 608 var benchmarkCookieString string 609 b.ReportAllocs() 610 b.ResetTimer() 611 for i := 0; i < b.N; i++ { 612 benchmarkCookieString = c.String() 613 } 614 if have, want := benchmarkCookieString, wantCookieString; have != want { 615 b.Fatalf("Have: %v Want: %v", have, want) 616 } 617} 618 619func BenchmarkReadSetCookies(b *testing.B) { 620 header := Header{ 621 "Set-Cookie": { 622 "NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly", 623 ".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly", 624 }, 625 } 626 wantCookies := []*Cookie{ 627 { 628 Name: "NID", 629 Value: "99=YsDT5i3E-CXax-", 630 Path: "/", 631 Domain: ".google.ch", 632 HttpOnly: true, 633 Expires: time.Date(2011, 11, 23, 1, 5, 3, 0, time.UTC), 634 RawExpires: "Wed, 23-Nov-2011 01:05:03 GMT", 635 Raw: "NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly", 636 }, 637 { 638 Name: ".ASPXAUTH", 639 Value: "7E3AA", 640 Path: "/", 641 Expires: time.Date(2012, 3, 7, 14, 25, 6, 0, time.UTC), 642 RawExpires: "Wed, 07-Mar-2012 14:25:06 GMT", 643 HttpOnly: true, 644 Raw: ".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly", 645 }, 646 } 647 var c []*Cookie 648 b.ReportAllocs() 649 b.ResetTimer() 650 for i := 0; i < b.N; i++ { 651 c = readSetCookies(header) 652 } 653 if !reflect.DeepEqual(c, wantCookies) { 654 b.Fatalf("readSetCookies:\nhave: %s\nwant: %s\n", toJSON(c), toJSON(wantCookies)) 655 } 656} 657 658func BenchmarkReadCookies(b *testing.B) { 659 header := Header{ 660 "Cookie": { 661 `de=; client_region=0; rpld1=0:hispeed.ch|20:che|21:zh|22:zurich|23:47.36|24:8.53|; rpld0=1:08|; backplane-channel=newspaper.com:1471; devicetype=0; osfam=0; rplmct=2; s_pers=%20s_vmonthnum%3D1472680800496%2526vn%253D1%7C1472680800496%3B%20s_nr%3D1471686767664-New%7C1474278767664%3B%20s_lv%3D1471686767669%7C1566294767669%3B%20s_lv_s%3DFirst%2520Visit%7C1471688567669%3B%20s_monthinvisit%3Dtrue%7C1471688567677%3B%20gvp_p5%3Dsports%253Ablog%253Aearly-lead%2520-%2520184693%2520-%252020160820%2520-%2520u-s%7C1471688567681%3B%20gvp_p51%3Dwp%2520-%2520sports%7C1471688567684%3B; s_sess=%20s_wp_ep%3Dhomepage%3B%20s._ref%3Dhttps%253A%252F%252Fwww.google.ch%252F%3B%20s_cc%3Dtrue%3B%20s_ppvl%3Dsports%25253Ablog%25253Aearly-lead%252520-%252520184693%252520-%25252020160820%252520-%252520u-lawyer%252C12%252C12%252C502%252C1231%252C502%252C1680%252C1050%252C2%252CP%3B%20s_ppv%3Dsports%25253Ablog%25253Aearly-lead%252520-%252520184693%252520-%25252020160820%252520-%252520u-s-lawyer%252C12%252C12%252C502%252C1231%252C502%252C1680%252C1050%252C2%252CP%3B%20s_dslv%3DFirst%2520Visit%3B%20s_sq%3Dwpninewspapercom%253D%252526pid%25253Dsports%2525253Ablog%2525253Aearly-lead%25252520-%25252520184693%25252520-%2525252020160820%25252520-%25252520u-s%252526pidt%25253D1%252526oid%25253Dhttps%2525253A%2525252F%2525252Fwww.newspaper.com%2525252F%2525253Fnid%2525253Dmenu_nav_homepage%252526ot%25253DA%3B`, 662 }, 663 } 664 wantCookies := []*Cookie{ 665 {Name: "de", Value: ""}, 666 {Name: "client_region", Value: "0"}, 667 {Name: "rpld1", Value: "0:hispeed.ch|20:che|21:zh|22:zurich|23:47.36|24:8.53|"}, 668 {Name: "rpld0", Value: "1:08|"}, 669 {Name: "backplane-channel", Value: "newspaper.com:1471"}, 670 {Name: "devicetype", Value: "0"}, 671 {Name: "osfam", Value: "0"}, 672 {Name: "rplmct", Value: "2"}, 673 {Name: "s_pers", Value: "%20s_vmonthnum%3D1472680800496%2526vn%253D1%7C1472680800496%3B%20s_nr%3D1471686767664-New%7C1474278767664%3B%20s_lv%3D1471686767669%7C1566294767669%3B%20s_lv_s%3DFirst%2520Visit%7C1471688567669%3B%20s_monthinvisit%3Dtrue%7C1471688567677%3B%20gvp_p5%3Dsports%253Ablog%253Aearly-lead%2520-%2520184693%2520-%252020160820%2520-%2520u-s%7C1471688567681%3B%20gvp_p51%3Dwp%2520-%2520sports%7C1471688567684%3B"}, 674 {Name: "s_sess", Value: "%20s_wp_ep%3Dhomepage%3B%20s._ref%3Dhttps%253A%252F%252Fwww.google.ch%252F%3B%20s_cc%3Dtrue%3B%20s_ppvl%3Dsports%25253Ablog%25253Aearly-lead%252520-%252520184693%252520-%25252020160820%252520-%252520u-lawyer%252C12%252C12%252C502%252C1231%252C502%252C1680%252C1050%252C2%252CP%3B%20s_ppv%3Dsports%25253Ablog%25253Aearly-lead%252520-%252520184693%252520-%25252020160820%252520-%252520u-s-lawyer%252C12%252C12%252C502%252C1231%252C502%252C1680%252C1050%252C2%252CP%3B%20s_dslv%3DFirst%2520Visit%3B%20s_sq%3Dwpninewspapercom%253D%252526pid%25253Dsports%2525253Ablog%2525253Aearly-lead%25252520-%25252520184693%25252520-%2525252020160820%25252520-%25252520u-s%252526pidt%25253D1%252526oid%25253Dhttps%2525253A%2525252F%2525252Fwww.newspaper.com%2525252F%2525253Fnid%2525253Dmenu_nav_homepage%252526ot%25253DA%3B"}, 675 } 676 var c []*Cookie 677 b.ReportAllocs() 678 b.ResetTimer() 679 for i := 0; i < b.N; i++ { 680 c = readCookies(header, "") 681 } 682 if !reflect.DeepEqual(c, wantCookies) { 683 b.Fatalf("readCookies:\nhave: %s\nwant: %s\n", toJSON(c), toJSON(wantCookies)) 684 } 685} 686 687func TestParseCookie(t *testing.T) { 688 tests := []struct { 689 line string 690 cookies []*Cookie 691 err error 692 }{ 693 { 694 line: "Cookie-1=v$1", 695 cookies: []*Cookie{{Name: "Cookie-1", Value: "v$1"}}, 696 }, 697 { 698 line: "Cookie-1=v$1;c2=v2", 699 cookies: []*Cookie{{Name: "Cookie-1", Value: "v$1"}, {Name: "c2", Value: "v2"}}, 700 }, 701 { 702 line: `Cookie-1="v$1";c2="v2"`, 703 cookies: []*Cookie{{Name: "Cookie-1", Value: "v$1", Quoted: true}, {Name: "c2", Value: "v2", Quoted: true}}, 704 }, 705 { 706 line: "k1=", 707 cookies: []*Cookie{{Name: "k1", Value: ""}}, 708 }, 709 { 710 line: "", 711 err: errBlankCookie, 712 }, 713 { 714 line: "equal-not-found", 715 err: errEqualNotFoundInCookie, 716 }, 717 { 718 line: "=v1", 719 err: errInvalidCookieName, 720 }, 721 { 722 line: "k1=\\", 723 err: errInvalidCookieValue, 724 }, 725 } 726 for i, tt := range tests { 727 gotCookies, gotErr := ParseCookie(tt.line) 728 if !errors.Is(gotErr, tt.err) { 729 t.Errorf("#%d ParseCookie got error %v, want error %v", i, gotErr, tt.err) 730 } 731 if !reflect.DeepEqual(gotCookies, tt.cookies) { 732 t.Errorf("#%d ParseCookie:\ngot cookies: %s\nwant cookies: %s\n", i, toJSON(gotCookies), toJSON(tt.cookies)) 733 } 734 } 735} 736 737func TestParseSetCookie(t *testing.T) { 738 tests := []struct { 739 line string 740 cookie *Cookie 741 err error 742 }{ 743 { 744 line: "Cookie-1=v$1", 745 cookie: &Cookie{Name: "Cookie-1", Value: "v$1", Raw: "Cookie-1=v$1"}, 746 }, 747 { 748 line: "NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly", 749 cookie: &Cookie{ 750 Name: "NID", 751 Value: "99=YsDT5i3E-CXax-", 752 Path: "/", 753 Domain: ".google.ch", 754 HttpOnly: true, 755 Expires: time.Date(2011, 11, 23, 1, 5, 3, 0, time.UTC), 756 RawExpires: "Wed, 23-Nov-2011 01:05:03 GMT", 757 Raw: "NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly", 758 }, 759 }, 760 { 761 line: ".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly", 762 cookie: &Cookie{ 763 Name: ".ASPXAUTH", 764 Value: "7E3AA", 765 Path: "/", 766 Expires: time.Date(2012, 3, 7, 14, 25, 6, 0, time.UTC), 767 RawExpires: "Wed, 07-Mar-2012 14:25:06 GMT", 768 HttpOnly: true, 769 Raw: ".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly", 770 }, 771 }, 772 { 773 line: "ASP.NET_SessionId=foo; path=/; HttpOnly", 774 cookie: &Cookie{ 775 Name: "ASP.NET_SessionId", 776 Value: "foo", 777 Path: "/", 778 HttpOnly: true, 779 Raw: "ASP.NET_SessionId=foo; path=/; HttpOnly", 780 }, 781 }, 782 { 783 line: "samesitedefault=foo; SameSite", 784 cookie: &Cookie{ 785 Name: "samesitedefault", 786 Value: "foo", 787 SameSite: SameSiteDefaultMode, 788 Raw: "samesitedefault=foo; SameSite", 789 }, 790 }, 791 { 792 line: "samesiteinvalidisdefault=foo; SameSite=invalid", 793 cookie: &Cookie{ 794 Name: "samesiteinvalidisdefault", 795 Value: "foo", 796 SameSite: SameSiteDefaultMode, 797 Raw: "samesiteinvalidisdefault=foo; SameSite=invalid", 798 }, 799 }, 800 { 801 line: "samesitelax=foo; SameSite=Lax", 802 cookie: &Cookie{ 803 Name: "samesitelax", 804 Value: "foo", 805 SameSite: SameSiteLaxMode, 806 Raw: "samesitelax=foo; SameSite=Lax", 807 }, 808 }, 809 { 810 line: "samesitestrict=foo; SameSite=Strict", 811 cookie: &Cookie{ 812 Name: "samesitestrict", 813 Value: "foo", 814 SameSite: SameSiteStrictMode, 815 Raw: "samesitestrict=foo; SameSite=Strict", 816 }, 817 }, 818 { 819 line: "samesitenone=foo; SameSite=None", 820 cookie: &Cookie{ 821 Name: "samesitenone", 822 Value: "foo", 823 SameSite: SameSiteNoneMode, 824 Raw: "samesitenone=foo; SameSite=None", 825 }, 826 }, 827 // Make sure we can properly read back the Set-Cookie headers we create 828 // for values containing spaces or commas: 829 { 830 line: `special-1=a z`, 831 cookie: &Cookie{Name: "special-1", Value: "a z", Raw: `special-1=a z`}, 832 }, 833 { 834 line: `special-2=" z"`, 835 cookie: &Cookie{Name: "special-2", Value: " z", Quoted: true, Raw: `special-2=" z"`}, 836 }, 837 { 838 line: `special-3="a "`, 839 cookie: &Cookie{Name: "special-3", Value: "a ", Quoted: true, Raw: `special-3="a "`}, 840 }, 841 { 842 line: `special-4=" "`, 843 cookie: &Cookie{Name: "special-4", Value: " ", Quoted: true, Raw: `special-4=" "`}, 844 }, 845 { 846 line: `special-5=a,z`, 847 cookie: &Cookie{Name: "special-5", Value: "a,z", Raw: `special-5=a,z`}, 848 }, 849 { 850 line: `special-6=",z"`, 851 cookie: &Cookie{Name: "special-6", Value: ",z", Quoted: true, Raw: `special-6=",z"`}, 852 }, 853 { 854 line: `special-7=a,`, 855 cookie: &Cookie{Name: "special-7", Value: "a,", Raw: `special-7=a,`}, 856 }, 857 { 858 line: `special-8=","`, 859 cookie: &Cookie{Name: "special-8", Value: ",", Quoted: true, Raw: `special-8=","`}, 860 }, 861 // Make sure we can properly read back the Set-Cookie headers 862 // for names containing spaces: 863 { 864 line: `special-9 =","`, 865 cookie: &Cookie{Name: "special-9", Value: ",", Quoted: true, Raw: `special-9 =","`}, 866 }, 867 { 868 line: "", 869 err: errBlankCookie, 870 }, 871 { 872 line: "equal-not-found", 873 err: errEqualNotFoundInCookie, 874 }, 875 { 876 line: "=v1", 877 err: errInvalidCookieName, 878 }, 879 { 880 line: "k1=\\", 881 err: errInvalidCookieValue, 882 }, 883 } 884 for i, tt := range tests { 885 gotCookie, gotErr := ParseSetCookie(tt.line) 886 if !errors.Is(gotErr, tt.err) { 887 t.Errorf("#%d ParseSetCookie got error %v, want error %v", i, gotErr, tt.err) 888 continue 889 } 890 if !reflect.DeepEqual(gotCookie, tt.cookie) { 891 t.Errorf("#%d ParseSetCookie:\ngot cookie: %s\nwant cookie: %s\n", i, toJSON(gotCookie), toJSON(tt.cookie)) 892 } 893 } 894} 895