summaryrefslogtreecommitdiff
path: root/src/pkg/http/cookie.go
diff options
context:
space:
mode:
authorOndřej Surý <ondrej@sury.org>2011-04-20 15:44:41 +0200
committerOndřej Surý <ondrej@sury.org>2011-04-20 15:44:41 +0200
commit50104cc32a498f7517a51c8dc93106c51c7a54b4 (patch)
tree47af80be259cc7c45d0eaec7d42e61fa38c8e4fb /src/pkg/http/cookie.go
parentc072558b90f1bbedc2022b0f30c8b1ac4712538e (diff)
downloadgolang-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.go336
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
+}