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.
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.
19 // See http://tools.ietf.org/html/rfc6265 for details.
24 Path
string // optional
25 Domain
string // optional
26 Expires time
.Time
// optional
27 RawExpires
string // for reading cookies only
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
36 Unparsed
[]string // Raw text of unparsed attribute-value pairs
39 // readSetCookies parses all "Set-Cookie" values from
40 // the header h and returns the successfully parsed Cookies.
41 func readSetCookies(h Header
) []*Cookie
{
42 cookieCount
:= len(h
["Set-Cookie"])
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] == "" {
52 parts
[0] = strings
.TrimSpace(parts
[0])
53 j
:= strings
.Index(parts
[0], "=")
57 name
, value
:= parts
[0][:j
], parts
[0][j
+1:]
58 if !isCookieNameValid(name
) {
61 value
, ok
:= parseCookieValue(value
, true)
70 for i
:= 1; i
< len(parts
); i
++ {
71 parts
[i
] = strings
.TrimSpace(parts
[i
])
72 if len(parts
[i
]) == 0 {
76 attr
, val
:= parts
[i
], ""
77 if j
:= strings
.Index(attr
, "="); j
>= 0 {
78 attr
, val
= attr
[:j
], attr
[j
+1:]
80 lowerAttr
:= strings
.ToLower(attr
)
81 val
, ok
= parseCookieValue(val
, false)
83 c
.Unparsed
= append(c
.Unparsed
, parts
[i
])
97 secs
, err
:= strconv
.Atoi(val
)
98 if err
!= nil || secs
!= 0 && val
[0] == '0' {
108 exptime
, err
:= time
.Parse(time
.RFC1123
, val
)
110 exptime
, err
= time
.Parse("Mon, 02-Jan-2006 15:04:05 MST", val
)
112 c
.Expires
= time
.Time
{}
116 c
.Expires
= exptime
.UTC()
122 c
.Unparsed
= append(c
.Unparsed
, parts
[i
])
124 cookies
= append(cookies
, c
)
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
132 func SetCookie(w ResponseWriter
, cookie
*Cookie
) {
133 if v
:= cookie
.String(); v
!= "" {
134 w
.Header().Add("Set-Cookie", v
)
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.
142 func (c
*Cookie
) String() string {
143 if c
== nil ||
!isCookieNameValid(c
.Name
) {
147 b
.WriteString(sanitizeCookieName(c
.Name
))
149 b
.WriteString(sanitizeCookieValue(c
.Value
))
152 b
.WriteString("; Path=")
153 b
.WriteString(sanitizeCookiePath(c
.Path
))
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.
165 b
.WriteString("; Domain=")
168 log
.Printf("net/http: invalid Cookie.Domain %q; dropping domain attribute", c
.Domain
)
171 if validCookieExpires(c
.Expires
) {
172 b
.WriteString("; Expires=")
175 b
.Write(c
.Expires
.UTC().AppendFormat(b2
, TimeFormat
))
178 b
.WriteString("; Max-Age=")
181 b
.Write(strconv
.AppendInt(b2
, int64(c
.MaxAge
), 10))
182 } else if c
.MaxAge
< 0 {
183 b
.WriteString("; Max-Age=0")
186 b
.WriteString("; HttpOnly")
189 b
.WriteString("; Secure")
194 // readCookies parses all "Cookie" values from the header h and
195 // returns the successfully parsed Cookies.
197 // if filter isn't empty, only cookies of that name are returned
198 func readCookies(h Header
, filter
string) []*Cookie
{
199 lines
, ok
:= h
["Cookie"]
204 cookies
:= []*Cookie
{}
205 for _
, line
:= range lines
{
206 parts
:= strings
.Split(strings
.TrimSpace(line
), ";")
207 if len(parts
) == 1 && parts
[0] == "" {
210 // Per-line attributes
212 for i
:= 0; i
< len(parts
); i
++ {
213 parts
[i
] = strings
.TrimSpace(parts
[i
])
214 if len(parts
[i
]) == 0 {
217 name
, val
:= parts
[i
], ""
218 if j
:= strings
.Index(name
, "="); j
>= 0 {
219 name
, val
= name
[:j
], name
[j
+1:]
221 if !isCookieNameValid(name
) {
224 if filter
!= "" && filter
!= name
{
227 val
, ok
:= parseCookieValue(val
, true)
231 cookies
= append(cookies
, &Cookie
{Name
: name
, Value
: val
})
238 // validCookieDomain returns whether v is a valid cookie domain-value.
239 func validCookieDomain(v
string) bool {
240 if isCookieDomainName(v
) {
243 if net
.ParseIP(v
) != nil && !strings
.Contains(v
, ":") {
249 // validCookieExpires returns whether v is a valid cookie expires-value.
250 func validCookieExpires(t time
.Time
) bool {
251 // IETF RFC 6265 Section 5.1.1.5, the year must not be less than 1601
252 return t
.Year() >= 1601
255 // isCookieDomainName returns whether s is a valid domain name or a valid
256 // domain name with a leading dot '.'. It is almost a direct copy of
257 // package net's isDomainName.
258 func isCookieDomainName(s
string) bool {
267 // A cookie a domain attribute may start with a leading dot.
271 ok
:= false // Ok once we've seen a letter.
273 for i
:= 0; i
< len(s
); i
++ {
278 case 'a' <= c
&& c
<= 'z' ||
'A' <= c
&& c
<= 'Z':
279 // No '_' allowed here (in contrast to package net).
282 case '0' <= c
&& c
<= '9':
286 // Byte before dash cannot be dot.
292 // Byte before dot cannot be dot, dash.
293 if last
== '.' || last
== '-' {
296 if partlen
> 63 || partlen
== 0 {
303 if last
== '-' || partlen
> 63 {
310 var cookieNameSanitizer
= strings
.NewReplacer("\n", "-", "\r", "-")
312 func sanitizeCookieName(n
string) string {
313 return cookieNameSanitizer
.Replace(n
)
316 // http://tools.ietf.org/html/rfc6265#section-4.1.1
317 // cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
318 // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
319 // ; US-ASCII characters excluding CTLs,
320 // ; whitespace DQUOTE, comma, semicolon,
322 // We loosen this as spaces and commas are common in cookie values
323 // but we produce a quoted cookie-value in when value starts or ends
324 // with a comma or space.
325 // See https://golang.org/issue/7243 for the discussion.
326 func sanitizeCookieValue(v
string) string {
327 v
= sanitizeOrWarn("Cookie.Value", validCookieValueByte
, v
)
331 if strings
.IndexByte(v
, ' ') >= 0 || strings
.IndexByte(v
, ',') >= 0 {
337 func validCookieValueByte(b
byte) bool {
338 return 0x20 <= b
&& b
< 0x7f && b
!= '"' && b
!= ';' && b
!= '\\'
341 // path-av = "Path=" path-value
342 // path-value = <any CHAR except CTLs or ";">
343 func sanitizeCookiePath(v
string) string {
344 return sanitizeOrWarn("Cookie.Path", validCookiePathByte
, v
)
347 func validCookiePathByte(b
byte) bool {
348 return 0x20 <= b
&& b
< 0x7f && b
!= ';'
351 func sanitizeOrWarn(fieldName
string, valid
func(byte) bool, v
string) string {
353 for i
:= 0; i
< len(v
); i
++ {
357 log
.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v
[i
], fieldName
)
364 buf
:= make([]byte, 0, len(v
))
365 for i
:= 0; i
< len(v
); i
++ {
366 if b
:= v
[i
]; valid(b
) {
373 func parseCookieValue(raw
string, allowDoubleQuote
bool) (string, bool) {
374 // Strip the quotes, if present.
375 if allowDoubleQuote
&& len(raw
) > 1 && raw
[0] == '"' && raw
[len(raw
)-1] == '"' {
376 raw
= raw
[1 : len(raw
)-1]
378 for i
:= 0; i
< len(raw
); i
++ {
379 if !validCookieValueByte(raw
[i
]) {
386 func isCookieNameValid(raw
string) bool {
390 return strings
.IndexFunc(raw
, isNotToken
) < 0