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