// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package http import ( "bytes" "fmt" "strconv" "strings" "time" ) // This implementation is done according to RFC 6265: // // http://tools.ietf.org/html/rfc6265 // A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an // HTTP response or the Cookie header of an HTTP request. type Cookie struct { Name string Value string Path string Domain string Expires time.Time RawExpires string // MaxAge=0 means no 'Max-Age' attribute specified. // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' // MaxAge>0 means Max-Age attribute present and given in seconds MaxAge int Secure bool HttpOnly bool Raw string Unparsed []string // Raw text of unparsed attribute-value pairs } // readSetCookies parses all "Set-Cookie" values from // the header h and returns the successfully parsed Cookies. func readSetCookies(h Header) []*Cookie { cookies := []*Cookie{} for _, line := range h["Set-Cookie"] { parts := strings.Split(strings.TrimSpace(line), ";") if len(parts) == 1 && parts[0] == "" { continue } parts[0] = strings.TrimSpace(parts[0]) j := strings.Index(parts[0], "=") if j < 0 { continue } name, value := parts[0][:j], parts[0][j+1:] if !isCookieNameValid(name) { continue } value, success := parseCookieValue(value) if !success { continue } c := &Cookie{ Name: name, Value: value, Raw: line, } for i := 1; i < len(parts); i++ { parts[i] = strings.TrimSpace(parts[i]) if len(parts[i]) == 0 { continue } attr, val := parts[i], "" if j := strings.Index(attr, "="); j >= 0 { attr, val = attr[:j], attr[j+1:] } lowerAttr := strings.ToLower(attr) parseCookieValueFn := parseCookieValue if lowerAttr == "expires" { parseCookieValueFn = parseCookieExpiresValue } val, success = parseCookieValueFn(val) if !success { c.Unparsed = append(c.Unparsed, parts[i]) continue } switch lowerAttr { case "secure": c.Secure = true continue case "httponly": c.HttpOnly = true continue case "domain": c.Domain = val // TODO: Add domain parsing continue case "max-age": secs, err := strconv.Atoi(val) if err != nil || secs != 0 && val[0] == '0' { break } if secs <= 0 { c.MaxAge = -1 } else { c.MaxAge = secs } continue case "expires": c.RawExpires = val exptime, err := time.Parse(time.RFC1123, val) if err != nil { exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", val) if err != nil { c.Expires = time.Time{} break } } c.Expires = exptime.UTC() continue case "path": c.Path = val // TODO: Add path parsing continue } c.Unparsed = append(c.Unparsed, parts[i]) } cookies = append(cookies, c) } return cookies } // SetCookie adds a Set-Cookie header to the provided ResponseWriter's headers. func SetCookie(w ResponseWriter, cookie *Cookie) { w.Header().Add("Set-Cookie", cookie.String()) } // String returns the serialization of the cookie for use in a Cookie // header (if only Name and Value are set) or a Set-Cookie response // header (if other fields are set). func (c *Cookie) String() string { var b bytes.Buffer fmt.Fprintf(&b, "%s=%s", sanitizeName(c.Name), sanitizeValue(c.Value)) if len(c.Path) > 0 { fmt.Fprintf(&b, "; Path=%s", sanitizeValue(c.Path)) } if len(c.Domain) > 0 { fmt.Fprintf(&b, "; Domain=%s", sanitizeValue(c.Domain)) } if c.Expires.Unix() > 0 { fmt.Fprintf(&b, "; Expires=%s", c.Expires.UTC().Format(time.RFC1123)) } if c.MaxAge > 0 { fmt.Fprintf(&b, "; Max-Age=%d", c.MaxAge) } else if c.MaxAge < 0 { fmt.Fprintf(&b, "; Max-Age=0") } if c.HttpOnly { fmt.Fprintf(&b, "; HttpOnly") } if c.Secure { fmt.Fprintf(&b, "; Secure") } return b.String() } // readCookies parses all "Cookie" values from the header h and // returns the successfully parsed Cookies. // // if filter isn't empty, only cookies of that name are returned func readCookies(h Header, filter string) []*Cookie { cookies := []*Cookie{} lines, ok := h["Cookie"] if !ok { return cookies } for _, line := range lines { parts := strings.Split(strings.TrimSpace(line), ";") if len(parts) == 1 && parts[0] == "" { continue } // Per-line attributes parsedPairs := 0 for i := 0; i < len(parts); i++ { parts[i] = strings.TrimSpace(parts[i]) if len(parts[i]) == 0 { continue } name, val := parts[i], "" if j := strings.Index(name, "="); j >= 0 { name, val = name[:j], name[j+1:] } if !isCookieNameValid(name) { continue } if filter != "" && filter != name { continue } val, success := parseCookieValue(val) if !success { continue } cookies = append(cookies, &Cookie{Name: name, Value: val}) parsedPairs++ } } return cookies } var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-") func sanitizeName(n string) string { return cookieNameSanitizer.Replace(n) } var cookieValueSanitizer = strings.NewReplacer("\n", " ", "\r", " ", ";", " ") func sanitizeValue(v string) string { return cookieValueSanitizer.Replace(v) } func unquoteCookieValue(v string) string { if len(v) > 1 && v[0] == '"' && v[len(v)-1] == '"' { return v[1 : len(v)-1] } return v } func isCookieByte(c byte) bool { switch { case c == 0x21, 0x23 <= c && c <= 0x2b, 0x2d <= c && c <= 0x3a, 0x3c <= c && c <= 0x5b, 0x5d <= c && c <= 0x7e: return true } return false } func isCookieExpiresByte(c byte) (ok bool) { return isCookieByte(c) || c == ',' || c == ' ' } func parseCookieValue(raw string) (string, bool) { return parseCookieValueUsing(raw, isCookieByte) } func parseCookieExpiresValue(raw string) (string, bool) { return parseCookieValueUsing(raw, isCookieExpiresByte) } func parseCookieValueUsing(raw string, validByte func(byte) bool) (string, bool) { raw = unquoteCookieValue(raw) for i := 0; i < len(raw); i++ { if !validByte(raw[i]) { return "", false } } return raw, true } func isCookieNameValid(raw string) bool { for _, c := range raw { if !isToken(byte(c)) { return false } } return true }