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