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.
15 // A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an
16 // HTTP response or the Cookie header of an HTTP request.
18 // See https://tools.ietf.org/html/rfc6265 for details.
23 Path
string // optional
24 Domain
string // optional
25 Expires time
.Time
// optional
26 RawExpires
string // for reading cookies only
28 // MaxAge=0 means no 'Max-Age' attribute specified.
29 // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
30 // MaxAge>0 means Max-Age attribute present and given in seconds
36 Unparsed
[]string // Raw text of unparsed attribute-value pairs
39 // SameSite allows a server define a cookie attribute making it impossible to
40 // the browser send this cookie along with cross-site requests. The main goal
41 // is mitigate the risk of cross-origin information leakage, and provides some
42 // protection against cross-site request forgery attacks.
44 // See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
48 SameSiteDefaultMode SameSite
= iota + 1
53 // readSetCookies parses all "Set-Cookie" values from
54 // the header h and returns the successfully parsed Cookies.
55 func readSetCookies(h Header
) []*Cookie
{
56 cookieCount
:= len(h
["Set-Cookie"])
60 cookies
:= make([]*Cookie
, 0, cookieCount
)
61 for _
, line
:= range h
["Set-Cookie"] {
62 parts
:= strings
.Split(strings
.TrimSpace(line
), ";")
63 if len(parts
) == 1 && parts
[0] == "" {
66 parts
[0] = strings
.TrimSpace(parts
[0])
67 j
:= strings
.Index(parts
[0], "=")
71 name
, value
:= parts
[0][:j
], parts
[0][j
+1:]
72 if !isCookieNameValid(name
) {
75 value
, ok
:= parseCookieValue(value
, true)
84 for i
:= 1; i
< len(parts
); i
++ {
85 parts
[i
] = strings
.TrimSpace(parts
[i
])
86 if len(parts
[i
]) == 0 {
90 attr
, val
:= parts
[i
], ""
91 if j
:= strings
.Index(attr
, "="); j
>= 0 {
92 attr
, val
= attr
[:j
], attr
[j
+1:]
94 lowerAttr
:= strings
.ToLower(attr
)
95 val
, ok
= parseCookieValue(val
, false)
97 c
.Unparsed
= append(c
.Unparsed
, parts
[i
])
102 lowerVal
:= strings
.ToLower(val
)
105 c
.SameSite
= SameSiteLaxMode
107 c
.SameSite
= SameSiteStrictMode
109 c
.SameSite
= SameSiteDefaultMode
122 secs
, err
:= strconv
.Atoi(val
)
123 if err
!= nil || secs
!= 0 && val
[0] == '0' {
133 exptime
, err
:= time
.Parse(time
.RFC1123
, val
)
135 exptime
, err
= time
.Parse("Mon, 02-Jan-2006 15:04:05 MST", val
)
137 c
.Expires
= time
.Time
{}
141 c
.Expires
= exptime
.UTC()
147 c
.Unparsed
= append(c
.Unparsed
, parts
[i
])
149 cookies
= append(cookies
, c
)
154 // SetCookie adds a Set-Cookie header to the provided ResponseWriter's headers.
155 // The provided cookie must have a valid Name. Invalid cookies may be
157 func SetCookie(w ResponseWriter
, cookie
*Cookie
) {
158 if v
:= cookie
.String(); v
!= "" {
159 w
.Header().Add("Set-Cookie", v
)
163 // String returns the serialization of the cookie for use in a Cookie
164 // header (if only Name and Value are set) or a Set-Cookie response
165 // header (if other fields are set).
166 // If c is nil or c.Name is invalid, the empty string is returned.
167 func (c
*Cookie
) String() string {
168 if c
== nil ||
!isCookieNameValid(c
.Name
) {
171 var b strings
.Builder
172 b
.WriteString(sanitizeCookieName(c
.Name
))
174 b
.WriteString(sanitizeCookieValue(c
.Value
))
177 b
.WriteString("; Path=")
178 b
.WriteString(sanitizeCookiePath(c
.Path
))
180 if len(c
.Domain
) > 0 {
181 if validCookieDomain(c
.Domain
) {
182 // A c.Domain containing illegal characters is not
183 // sanitized but simply dropped which turns the cookie
184 // into a host-only cookie. A leading dot is okay
185 // but won't be sent.
190 b
.WriteString("; Domain=")
193 log
.Printf("net/http: invalid Cookie.Domain %q; dropping domain attribute", c
.Domain
)
196 var buf
[len(TimeFormat
)]byte
197 if validCookieExpires(c
.Expires
) {
198 b
.WriteString("; Expires=")
199 b
.Write(c
.Expires
.UTC().AppendFormat(buf
[:0], TimeFormat
))
202 b
.WriteString("; Max-Age=")
203 b
.Write(strconv
.AppendInt(buf
[:0], int64(c
.MaxAge
), 10))
204 } else if c
.MaxAge
< 0 {
205 b
.WriteString("; Max-Age=0")
208 b
.WriteString("; HttpOnly")
211 b
.WriteString("; Secure")
214 case SameSiteDefaultMode
:
215 b
.WriteString("; SameSite")
216 case SameSiteLaxMode
:
217 b
.WriteString("; SameSite=Lax")
218 case SameSiteStrictMode
:
219 b
.WriteString("; SameSite=Strict")
224 // readCookies parses all "Cookie" values from the header h and
225 // returns the successfully parsed Cookies.
227 // if filter isn't empty, only cookies of that name are returned
228 func readCookies(h Header
, filter
string) []*Cookie
{
229 lines
, ok
:= h
["Cookie"]
234 cookies
:= []*Cookie
{}
235 for _
, line
:= range lines
{
236 parts
:= strings
.Split(strings
.TrimSpace(line
), ";")
237 if len(parts
) == 1 && parts
[0] == "" {
240 // Per-line attributes
241 for i
:= 0; i
< len(parts
); i
++ {
242 parts
[i
] = strings
.TrimSpace(parts
[i
])
243 if len(parts
[i
]) == 0 {
246 name
, val
:= parts
[i
], ""
247 if j
:= strings
.Index(name
, "="); j
>= 0 {
248 name
, val
= name
[:j
], name
[j
+1:]
250 if !isCookieNameValid(name
) {
253 if filter
!= "" && filter
!= name
{
256 val
, ok
:= parseCookieValue(val
, true)
260 cookies
= append(cookies
, &Cookie
{Name
: name
, Value
: val
})
266 // validCookieDomain returns whether v is a valid cookie domain-value.
267 func validCookieDomain(v
string) bool {
268 if isCookieDomainName(v
) {
271 if net
.ParseIP(v
) != nil && !strings
.Contains(v
, ":") {
277 // validCookieExpires returns whether v is a valid cookie expires-value.
278 func validCookieExpires(t time
.Time
) bool {
279 // IETF RFC 6265 Section 5.1.1.5, the year must not be less than 1601
280 return t
.Year() >= 1601
283 // isCookieDomainName returns whether s is a valid domain name or a valid
284 // domain name with a leading dot '.'. It is almost a direct copy of
285 // package net's isDomainName.
286 func isCookieDomainName(s
string) bool {
295 // A cookie a domain attribute may start with a leading dot.
299 ok
:= false // Ok once we've seen a letter.
301 for i
:= 0; i
< len(s
); i
++ {
306 case 'a' <= c
&& c
<= 'z' ||
'A' <= c
&& c
<= 'Z':
307 // No '_' allowed here (in contrast to package net).
310 case '0' <= c
&& c
<= '9':
314 // Byte before dash cannot be dot.
320 // Byte before dot cannot be dot, dash.
321 if last
== '.' || last
== '-' {
324 if partlen
> 63 || partlen
== 0 {
331 if last
== '-' || partlen
> 63 {
338 var cookieNameSanitizer
= strings
.NewReplacer("\n", "-", "\r", "-")
340 func sanitizeCookieName(n
string) string {
341 return cookieNameSanitizer
.Replace(n
)
344 // https://tools.ietf.org/html/rfc6265#section-4.1.1
345 // cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
346 // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
347 // ; US-ASCII characters excluding CTLs,
348 // ; whitespace DQUOTE, comma, semicolon,
350 // We loosen this as spaces and commas are common in cookie values
351 // but we produce a quoted cookie-value in when value starts or ends
352 // with a comma or space.
353 // See https://golang.org/issue/7243 for the discussion.
354 func sanitizeCookieValue(v
string) string {
355 v
= sanitizeOrWarn("Cookie.Value", validCookieValueByte
, v
)
359 if strings
.IndexByte(v
, ' ') >= 0 || strings
.IndexByte(v
, ',') >= 0 {
365 func validCookieValueByte(b
byte) bool {
366 return 0x20 <= b
&& b
< 0x7f && b
!= '"' && b
!= ';' && b
!= '\\'
369 // path-av = "Path=" path-value
370 // path-value = <any CHAR except CTLs or ";">
371 func sanitizeCookiePath(v
string) string {
372 return sanitizeOrWarn("Cookie.Path", validCookiePathByte
, v
)
375 func validCookiePathByte(b
byte) bool {
376 return 0x20 <= b
&& b
< 0x7f && b
!= ';'
379 func sanitizeOrWarn(fieldName
string, valid
func(byte) bool, v
string) string {
381 for i
:= 0; i
< len(v
); i
++ {
385 log
.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v
[i
], fieldName
)
392 buf
:= make([]byte, 0, len(v
))
393 for i
:= 0; i
< len(v
); i
++ {
394 if b
:= v
[i
]; valid(b
) {
401 func parseCookieValue(raw
string, allowDoubleQuote
bool) (string, bool) {
402 // Strip the quotes, if present.
403 if allowDoubleQuote
&& len(raw
) > 1 && raw
[0] == '"' && raw
[len(raw
)-1] == '"' {
404 raw
= raw
[1 : len(raw
)-1]
406 for i
:= 0; i
< len(raw
); i
++ {
407 if !validCookieValueByte(raw
[i
]) {
414 func isCookieNameValid(raw
string) bool {
418 return strings
.IndexFunc(raw
, isNotToken
) < 0