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