Home | History | Annotate | Download | only in http
      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 
      5 package http
      6 
      7 import (
      8 	"bytes"
      9 	"encoding/json"
     10 	"fmt"
     11 	"log"
     12 	"os"
     13 	"reflect"
     14 	"strings"
     15 	"testing"
     16 	"time"
     17 )
     18 
     19 var 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 	// The "special" cookies have values containing commas or spaces which
     60 	// are disallowed by RFC 6265 but are common in the wild.
     61 	{
     62 		&Cookie{Name: "special-1", Value: "a z"},
     63 		`special-1=a z`,
     64 	},
     65 	{
     66 		&Cookie{Name: "special-2", Value: " z"},
     67 		`special-2=" z"`,
     68 	},
     69 	{
     70 		&Cookie{Name: "special-3", Value: "a "},
     71 		`special-3="a "`,
     72 	},
     73 	{
     74 		&Cookie{Name: "special-4", Value: " "},
     75 		`special-4=" "`,
     76 	},
     77 	{
     78 		&Cookie{Name: "special-5", Value: "a,z"},
     79 		`special-5=a,z`,
     80 	},
     81 	{
     82 		&Cookie{Name: "special-6", Value: ",z"},
     83 		`special-6=",z"`,
     84 	},
     85 	{
     86 		&Cookie{Name: "special-7", Value: "a,"},
     87 		`special-7="a,"`,
     88 	},
     89 	{
     90 		&Cookie{Name: "special-8", Value: ","},
     91 		`special-8=","`,
     92 	},
     93 	{
     94 		&Cookie{Name: "empty-value", Value: ""},
     95 		`empty-value=`,
     96 	},
     97 	{
     98 		nil,
     99 		``,
    100 	},
    101 	{
    102 		&Cookie{Name: ""},
    103 		``,
    104 	},
    105 	{
    106 		&Cookie{Name: "\t"},
    107 		``,
    108 	},
    109 }
    110 
    111 func TestWriteSetCookies(t *testing.T) {
    112 	defer log.SetOutput(os.Stderr)
    113 	var logbuf bytes.Buffer
    114 	log.SetOutput(&logbuf)
    115 
    116 	for i, tt := range writeSetCookiesTests {
    117 		if g, e := tt.Cookie.String(), tt.Raw; g != e {
    118 			t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, e, g)
    119 			continue
    120 		}
    121 	}
    122 
    123 	if got, sub := logbuf.String(), "dropping domain attribute"; !strings.Contains(got, sub) {
    124 		t.Errorf("Expected substring %q in log output. Got:\n%s", sub, got)
    125 	}
    126 }
    127 
    128 type headerOnlyResponseWriter Header
    129 
    130 func (ho headerOnlyResponseWriter) Header() Header {
    131 	return Header(ho)
    132 }
    133 
    134 func (ho headerOnlyResponseWriter) Write([]byte) (int, error) {
    135 	panic("NOIMPL")
    136 }
    137 
    138 func (ho headerOnlyResponseWriter) WriteHeader(int) {
    139 	panic("NOIMPL")
    140 }
    141 
    142 func TestSetCookie(t *testing.T) {
    143 	m := make(Header)
    144 	SetCookie(headerOnlyResponseWriter(m), &Cookie{Name: "cookie-1", Value: "one", Path: "/restricted/"})
    145 	SetCookie(headerOnlyResponseWriter(m), &Cookie{Name: "cookie-2", Value: "two", MaxAge: 3600})
    146 	if l := len(m["Set-Cookie"]); l != 2 {
    147 		t.Fatalf("expected %d cookies, got %d", 2, l)
    148 	}
    149 	if g, e := m["Set-Cookie"][0], "cookie-1=one; Path=/restricted/"; g != e {
    150 		t.Errorf("cookie #1: want %q, got %q", e, g)
    151 	}
    152 	if g, e := m["Set-Cookie"][1], "cookie-2=two; Max-Age=3600"; g != e {
    153 		t.Errorf("cookie #2: want %q, got %q", e, g)
    154 	}
    155 }
    156 
    157 var addCookieTests = []struct {
    158 	Cookies []*Cookie
    159 	Raw     string
    160 }{
    161 	{
    162 		[]*Cookie{},
    163 		"",
    164 	},
    165 	{
    166 		[]*Cookie{{Name: "cookie-1", Value: "v$1"}},
    167 		"cookie-1=v$1",
    168 	},
    169 	{
    170 		[]*Cookie{
    171 			{Name: "cookie-1", Value: "v$1"},
    172 			{Name: "cookie-2", Value: "v$2"},
    173 			{Name: "cookie-3", Value: "v$3"},
    174 		},
    175 		"cookie-1=v$1; cookie-2=v$2; cookie-3=v$3",
    176 	},
    177 }
    178 
    179 func TestAddCookie(t *testing.T) {
    180 	for i, tt := range addCookieTests {
    181 		req, _ := NewRequest("GET", "http://example.com/", nil)
    182 		for _, c := range tt.Cookies {
    183 			req.AddCookie(c)
    184 		}
    185 		if g := req.Header.Get("Cookie"); g != tt.Raw {
    186 			t.Errorf("Test %d:\nwant: %s\n got: %s\n", i, tt.Raw, g)
    187 			continue
    188 		}
    189 	}
    190 }
    191 
    192 var readSetCookiesTests = []struct {
    193 	Header  Header
    194 	Cookies []*Cookie
    195 }{
    196 	{
    197 		Header{"Set-Cookie": {"Cookie-1=v$1"}},
    198 		[]*Cookie{{Name: "Cookie-1", Value: "v$1", Raw: "Cookie-1=v$1"}},
    199 	},
    200 	{
    201 		Header{"Set-Cookie": {"NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly"}},
    202 		[]*Cookie{{
    203 			Name:       "NID",
    204 			Value:      "99=YsDT5i3E-CXax-",
    205 			Path:       "/",
    206 			Domain:     ".google.ch",
    207 			HttpOnly:   true,
    208 			Expires:    time.Date(2011, 11, 23, 1, 5, 3, 0, time.UTC),
    209 			RawExpires: "Wed, 23-Nov-2011 01:05:03 GMT",
    210 			Raw:        "NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly",
    211 		}},
    212 	},
    213 	{
    214 		Header{"Set-Cookie": {".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly"}},
    215 		[]*Cookie{{
    216 			Name:       ".ASPXAUTH",
    217 			Value:      "7E3AA",
    218 			Path:       "/",
    219 			Expires:    time.Date(2012, 3, 7, 14, 25, 6, 0, time.UTC),
    220 			RawExpires: "Wed, 07-Mar-2012 14:25:06 GMT",
    221 			HttpOnly:   true,
    222 			Raw:        ".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly",
    223 		}},
    224 	},
    225 	{
    226 		Header{"Set-Cookie": {"ASP.NET_SessionId=foo; path=/; HttpOnly"}},
    227 		[]*Cookie{{
    228 			Name:     "ASP.NET_SessionId",
    229 			Value:    "foo",
    230 			Path:     "/",
    231 			HttpOnly: true,
    232 			Raw:      "ASP.NET_SessionId=foo; path=/; HttpOnly",
    233 		}},
    234 	},
    235 	// Make sure we can properly read back the Set-Cookie headers we create
    236 	// for values containing spaces or commas:
    237 	{
    238 		Header{"Set-Cookie": {`special-1=a z`}},
    239 		[]*Cookie{{Name: "special-1", Value: "a z", Raw: `special-1=a z`}},
    240 	},
    241 	{
    242 		Header{"Set-Cookie": {`special-2=" z"`}},
    243 		[]*Cookie{{Name: "special-2", Value: " z", Raw: `special-2=" z"`}},
    244 	},
    245 	{
    246 		Header{"Set-Cookie": {`special-3="a "`}},
    247 		[]*Cookie{{Name: "special-3", Value: "a ", Raw: `special-3="a "`}},
    248 	},
    249 	{
    250 		Header{"Set-Cookie": {`special-4=" "`}},
    251 		[]*Cookie{{Name: "special-4", Value: " ", Raw: `special-4=" "`}},
    252 	},
    253 	{
    254 		Header{"Set-Cookie": {`special-5=a,z`}},
    255 		[]*Cookie{{Name: "special-5", Value: "a,z", Raw: `special-5=a,z`}},
    256 	},
    257 	{
    258 		Header{"Set-Cookie": {`special-6=",z"`}},
    259 		[]*Cookie{{Name: "special-6", Value: ",z", Raw: `special-6=",z"`}},
    260 	},
    261 	{
    262 		Header{"Set-Cookie": {`special-7=a,`}},
    263 		[]*Cookie{{Name: "special-7", Value: "a,", Raw: `special-7=a,`}},
    264 	},
    265 	{
    266 		Header{"Set-Cookie": {`special-8=","`}},
    267 		[]*Cookie{{Name: "special-8", Value: ",", Raw: `special-8=","`}},
    268 	},
    269 
    270 	// TODO(bradfitz): users have reported seeing this in the
    271 	// wild, but do browsers handle it? RFC 6265 just says "don't
    272 	// do that" (section 3) and then never mentions header folding
    273 	// again.
    274 	// Header{"Set-Cookie": {"ASP.NET_SessionId=foo; path=/; HttpOnly, .ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly"}},
    275 }
    276 
    277 func toJSON(v interface{}) string {
    278 	b, err := json.Marshal(v)
    279 	if err != nil {
    280 		return fmt.Sprintf("%#v", v)
    281 	}
    282 	return string(b)
    283 }
    284 
    285 func TestReadSetCookies(t *testing.T) {
    286 	for i, tt := range readSetCookiesTests {
    287 		for n := 0; n < 2; n++ { // to verify readSetCookies doesn't mutate its input
    288 			c := readSetCookies(tt.Header)
    289 			if !reflect.DeepEqual(c, tt.Cookies) {
    290 				t.Errorf("#%d readSetCookies: have\n%s\nwant\n%s\n", i, toJSON(c), toJSON(tt.Cookies))
    291 				continue
    292 			}
    293 		}
    294 	}
    295 }
    296 
    297 var readCookiesTests = []struct {
    298 	Header  Header
    299 	Filter  string
    300 	Cookies []*Cookie
    301 }{
    302 	{
    303 		Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}},
    304 		"",
    305 		[]*Cookie{
    306 			{Name: "Cookie-1", Value: "v$1"},
    307 			{Name: "c2", Value: "v2"},
    308 		},
    309 	},
    310 	{
    311 		Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}},
    312 		"c2",
    313 		[]*Cookie{
    314 			{Name: "c2", Value: "v2"},
    315 		},
    316 	},
    317 	{
    318 		Header{"Cookie": {"Cookie-1=v$1; c2=v2"}},
    319 		"",
    320 		[]*Cookie{
    321 			{Name: "Cookie-1", Value: "v$1"},
    322 			{Name: "c2", Value: "v2"},
    323 		},
    324 	},
    325 	{
    326 		Header{"Cookie": {"Cookie-1=v$1; c2=v2"}},
    327 		"c2",
    328 		[]*Cookie{
    329 			{Name: "c2", Value: "v2"},
    330 		},
    331 	},
    332 	{
    333 		Header{"Cookie": {`Cookie-1="v$1"; c2="v2"`}},
    334 		"",
    335 		[]*Cookie{
    336 			{Name: "Cookie-1", Value: "v$1"},
    337 			{Name: "c2", Value: "v2"},
    338 		},
    339 	},
    340 }
    341 
    342 func TestReadCookies(t *testing.T) {
    343 	for i, tt := range readCookiesTests {
    344 		for n := 0; n < 2; n++ { // to verify readCookies doesn't mutate its input
    345 			c := readCookies(tt.Header, tt.Filter)
    346 			if !reflect.DeepEqual(c, tt.Cookies) {
    347 				t.Errorf("#%d readCookies:\nhave: %s\nwant: %s\n", i, toJSON(c), toJSON(tt.Cookies))
    348 				continue
    349 			}
    350 		}
    351 	}
    352 }
    353 
    354 func TestSetCookieDoubleQuotes(t *testing.T) {
    355 	res := &Response{Header: Header{}}
    356 	res.Header.Add("Set-Cookie", `quoted0=none; max-age=30`)
    357 	res.Header.Add("Set-Cookie", `quoted1="cookieValue"; max-age=31`)
    358 	res.Header.Add("Set-Cookie", `quoted2=cookieAV; max-age="32"`)
    359 	res.Header.Add("Set-Cookie", `quoted3="both"; max-age="33"`)
    360 	got := res.Cookies()
    361 	want := []*Cookie{
    362 		{Name: "quoted0", Value: "none", MaxAge: 30},
    363 		{Name: "quoted1", Value: "cookieValue", MaxAge: 31},
    364 		{Name: "quoted2", Value: "cookieAV"},
    365 		{Name: "quoted3", Value: "both"},
    366 	}
    367 	if len(got) != len(want) {
    368 		t.Fatalf("got %d cookies, want %d", len(got), len(want))
    369 	}
    370 	for i, w := range want {
    371 		g := got[i]
    372 		if g.Name != w.Name || g.Value != w.Value || g.MaxAge != w.MaxAge {
    373 			t.Errorf("cookie #%d:\ngot  %v\nwant %v", i, g, w)
    374 		}
    375 	}
    376 }
    377 
    378 func TestCookieSanitizeValue(t *testing.T) {
    379 	defer log.SetOutput(os.Stderr)
    380 	var logbuf bytes.Buffer
    381 	log.SetOutput(&logbuf)
    382 
    383 	tests := []struct {
    384 		in, want string
    385 	}{
    386 		{"foo", "foo"},
    387 		{"foo;bar", "foobar"},
    388 		{"foo\\bar", "foobar"},
    389 		{"foo\"bar", "foobar"},
    390 		{"\x00\x7e\x7f\x80", "\x7e"},
    391 		{`"withquotes"`, "withquotes"},
    392 		{"a z", "a z"},
    393 		{" z", `" z"`},
    394 		{"a ", `"a "`},
    395 	}
    396 	for _, tt := range tests {
    397 		if got := sanitizeCookieValue(tt.in); got != tt.want {
    398 			t.Errorf("sanitizeCookieValue(%q) = %q; want %q", tt.in, got, tt.want)
    399 		}
    400 	}
    401 
    402 	if got, sub := logbuf.String(), "dropping invalid bytes"; !strings.Contains(got, sub) {
    403 		t.Errorf("Expected substring %q in log output. Got:\n%s", sub, got)
    404 	}
    405 }
    406 
    407 func TestCookieSanitizePath(t *testing.T) {
    408 	defer log.SetOutput(os.Stderr)
    409 	var logbuf bytes.Buffer
    410 	log.SetOutput(&logbuf)
    411 
    412 	tests := []struct {
    413 		in, want string
    414 	}{
    415 		{"/path", "/path"},
    416 		{"/path with space/", "/path with space/"},
    417 		{"/just;no;semicolon\x00orstuff/", "/justnosemicolonorstuff/"},
    418 	}
    419 	for _, tt := range tests {
    420 		if got := sanitizeCookiePath(tt.in); got != tt.want {
    421 			t.Errorf("sanitizeCookiePath(%q) = %q; want %q", tt.in, got, tt.want)
    422 		}
    423 	}
    424 
    425 	if got, sub := logbuf.String(), "dropping invalid bytes"; !strings.Contains(got, sub) {
    426 		t.Errorf("Expected substring %q in log output. Got:\n%s", sub, got)
    427 	}
    428 }
    429