diff options
author | Ondřej Surý <ondrej@sury.org> | 2011-04-20 15:44:41 +0200 |
---|---|---|
committer | Ondřej Surý <ondrej@sury.org> | 2011-04-20 15:44:41 +0200 |
commit | 50104cc32a498f7517a51c8dc93106c51c7a54b4 (patch) | |
tree | 47af80be259cc7c45d0eaec7d42e61fa38c8e4fb /src/pkg/http/cookie.go | |
parent | c072558b90f1bbedc2022b0f30c8b1ac4712538e (diff) | |
download | golang-upstream/2011.03.07.1.tar.gz |
Imported Upstream version 2011.03.07.1upstream/2011.03.07.1
Diffstat (limited to 'src/pkg/http/cookie.go')
-rw-r--r-- | src/pkg/http/cookie.go | 336 |
1 files changed, 336 insertions, 0 deletions
diff --git a/src/pkg/http/cookie.go b/src/pkg/http/cookie.go new file mode 100644 index 000000000..ff75c47c9 --- /dev/null +++ b/src/pkg/http/cookie.go @@ -0,0 +1,336 @@ +// 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" + "io" + "os" + "sort" + "strconv" + "strings" + "time" +) + +// A note on Version=0 vs. Version=1 cookies +// +// The difference between Set-Cookie and Set-Cookie2 is hard to discern from the +// RFCs as it is not stated explicitly. There seem to be three standards +// lingering on the web: Netscape, RFC 2109 (aka Version=0) and RFC 2965 (aka +// Version=1). It seems that Netscape and RFC 2109 are the same thing, hereafter +// Version=0 cookies. +// +// In general, Set-Cookie2 is a superset of Set-Cookie. It has a few new +// attributes like HttpOnly and Secure. To be meticulous, if a server intends +// to use these, it needs to send a Set-Cookie2. However, it is most likely +// most modern browsers will not complain seeing an HttpOnly attribute in a +// Set-Cookie header. +// +// Both RFC 2109 and RFC 2965 use Cookie in the same way - two send cookie +// values from clients to servers - and the allowable attributes seem to be the +// same. +// +// The Cookie2 header is used for a different purpose. If a client suspects that +// the server speaks Version=1 (RFC 2965) then along with the Cookie header +// lines, you can also send: +// +// Cookie2: $Version="1" +// +// in order to suggest to the server that you understand Version=1 cookies. At +// which point the server may continue responding with Set-Cookie2 headers. +// When a client sends the (above) Cookie2 header line, it must be prepated to +// understand incoming Set-Cookie2. +// +// This implementation of cookies supports neither Set-Cookie2 nor Cookie2 +// headers. However, it parses Version=1 Cookies (along with Version=0) as well +// as Set-Cookie headers which utilize the full Set-Cookie2 syntax. + +// TODO(petar): Explicitly forbid parsing of Set-Cookie attributes +// starting with '$', which have been used to hack into broken +// servers using the eventual Request headers containing those +// invalid attributes that may overwrite intended $Version, $Path, +// etc. attributes. +// TODO(petar): Read 'Set-Cookie2' headers and prioritize them over equivalent +// 'Set-Cookie' headers. 'Set-Cookie2' headers are still extremely rare. + +// A Cookie represents an RFC 2965 HTTP cookie as sent in +// the Set-Cookie header of an HTTP response or the Cookie header +// of an HTTP request. +// The Set-Cookie2 and Cookie2 headers are unimplemented. +type Cookie struct { + Name string + Value string + Path string + Domain string + Comment string + Version int + Expires time.Time + RawExpires string + MaxAge int // Max age in seconds + 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, removes the successfully parsed values from the +// "Set-Cookie" key in h and returns the parsed Cookies. +func readSetCookies(h Header) []*Cookie { + cookies := []*Cookie{} + var unparsedLines []string + for _, line := range h["Set-Cookie"] { + parts := strings.Split(strings.TrimSpace(line), ";", -1) + if len(parts) == 1 && parts[0] == "" { + continue + } + parts[0] = strings.TrimSpace(parts[0]) + j := strings.Index(parts[0], "=") + if j < 0 { + unparsedLines = append(unparsedLines, line) + continue + } + name, value := parts[0][:j], parts[0][j+1:] + value, err := URLUnescape(value) + if err != nil { + unparsedLines = append(unparsedLines, line) + continue + } + c := &Cookie{ + Name: name, + Value: value, + MaxAge: -1, // Not specified + 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:] + val, err = URLUnescape(val) + if err != nil { + c.Unparsed = append(c.Unparsed, parts[i]) + continue + } + } + switch strings.ToLower(attr) { + case "secure": + c.Secure = true + continue + case "httponly": + c.HttpOnly = true + continue + case "comment": + c.Comment = val + continue + case "domain": + c.Domain = val + // TODO: Add domain parsing + continue + case "max-age": + secs, err := strconv.Atoi(val) + if err != nil || secs < 0 { + break + } + c.MaxAge = secs + continue + case "expires": + c.RawExpires = val + exptime, err := time.Parse(time.RFC1123, val) + if err != nil { + c.Expires = time.Time{} + break + } + c.Expires = *exptime + continue + case "path": + c.Path = val + // TODO: Add path parsing + continue + case "version": + c.Version, err = strconv.Atoi(val) + if err != nil { + c.Version = 0 + break + } + continue + } + c.Unparsed = append(c.Unparsed, parts[i]) + } + cookies = append(cookies, c) + } + h["Set-Cookie"] = unparsedLines, unparsedLines != nil + return cookies +} + +// writeSetCookies writes the wire representation of the set-cookies +// to w. Each cookie is written on a separate "Set-Cookie: " line. +// This choice is made because HTTP parsers tend to have a limit on +// line-length, so it seems safer to place cookies on separate lines. +func writeSetCookies(w io.Writer, kk []*Cookie) os.Error { + if kk == nil { + return nil + } + lines := make([]string, 0, len(kk)) + var b bytes.Buffer + for _, c := range kk { + b.Reset() + // TODO(petar): c.Value (below) should be unquoted if it is recognized as quoted + fmt.Fprintf(&b, "%s=%s", CanonicalHeaderKey(c.Name), c.Value) + if c.Version > 0 { + fmt.Fprintf(&b, "Version=%d; ", c.Version) + } + if len(c.Path) > 0 { + fmt.Fprintf(&b, "; Path=%s", URLEscape(c.Path)) + } + if len(c.Domain) > 0 { + fmt.Fprintf(&b, "; Domain=%s", URLEscape(c.Domain)) + } + if len(c.Expires.Zone) > 0 { + fmt.Fprintf(&b, "; Expires=%s", c.Expires.Format(time.RFC1123)) + } + if c.MaxAge >= 0 { + fmt.Fprintf(&b, "; Max-Age=%d", c.MaxAge) + } + if c.HttpOnly { + fmt.Fprintf(&b, "; HttpOnly") + } + if c.Secure { + fmt.Fprintf(&b, "; Secure") + } + if len(c.Comment) > 0 { + fmt.Fprintf(&b, "; Comment=%s", URLEscape(c.Comment)) + } + lines = append(lines, "Set-Cookie: "+b.String()+"\r\n") + } + sort.SortStrings(lines) + for _, l := range lines { + if _, err := io.WriteString(w, l); err != nil { + return err + } + } + return nil +} + +// readCookies parses all "Cookie" values from +// the header h, removes the successfully parsed values from the +// "Cookie" key in h and returns the parsed Cookies. +func readCookies(h Header) []*Cookie { + cookies := []*Cookie{} + lines, ok := h["Cookie"] + if !ok { + return cookies + } + unparsedLines := []string{} + for _, line := range lines { + parts := strings.Split(strings.TrimSpace(line), ";", -1) + if len(parts) == 1 && parts[0] == "" { + continue + } + // Per-line attributes + var lineCookies = make(map[string]string) + var version int + var path string + var domain string + var comment string + var httponly bool + for i := 0; i < len(parts); i++ { + parts[i] = strings.TrimSpace(parts[i]) + if len(parts[i]) == 0 { + continue + } + attr, val := parts[i], "" + var err os.Error + if j := strings.Index(attr, "="); j >= 0 { + attr, val = attr[:j], attr[j+1:] + val, err = URLUnescape(val) + if err != nil { + continue + } + } + switch strings.ToLower(attr) { + case "$httponly": + httponly = true + case "$version": + version, err = strconv.Atoi(val) + if err != nil { + version = 0 + continue + } + case "$domain": + domain = val + // TODO: Add domain parsing + case "$path": + path = val + // TODO: Add path parsing + case "$comment": + comment = val + default: + lineCookies[attr] = val + } + } + if len(lineCookies) == 0 { + unparsedLines = append(unparsedLines, line) + } + for n, v := range lineCookies { + cookies = append(cookies, &Cookie{ + Name: n, + Value: v, + Path: path, + Domain: domain, + Comment: comment, + Version: version, + HttpOnly: httponly, + MaxAge: -1, + Raw: line, + }) + } + } + h["Cookie"] = unparsedLines, len(unparsedLines) > 0 + return cookies +} + +// writeCookies writes the wire representation of the cookies +// to w. Each cookie is written on a separate "Cookie: " line. +// This choice is made because HTTP parsers tend to have a limit on +// line-length, so it seems safer to place cookies on separate lines. +func writeCookies(w io.Writer, kk []*Cookie) os.Error { + lines := make([]string, 0, len(kk)) + var b bytes.Buffer + for _, c := range kk { + b.Reset() + n := c.Name + if c.Version > 0 { + fmt.Fprintf(&b, "$Version=%d; ", c.Version) + } + // TODO(petar): c.Value (below) should be unquoted if it is recognized as quoted + fmt.Fprintf(&b, "%s=%s", CanonicalHeaderKey(n), c.Value) + if len(c.Path) > 0 { + fmt.Fprintf(&b, "; $Path=%s", URLEscape(c.Path)) + } + if len(c.Domain) > 0 { + fmt.Fprintf(&b, "; $Domain=%s", URLEscape(c.Domain)) + } + if c.HttpOnly { + fmt.Fprintf(&b, "; $HttpOnly") + } + if len(c.Comment) > 0 { + fmt.Fprintf(&b, "; $Comment=%s", URLEscape(c.Comment)) + } + lines = append(lines, "Cookie: "+b.String()+"\r\n") + } + sort.SortStrings(lines) + for _, l := range lines { + if _, err := io.WriteString(w, l); err != nil { + return err + } + } + return nil +} |