]> Cypherpunks repositories - gostls13.git/commitdiff
http: add cookie support
authorPetar Maymounkov <petarm@gmail.com>
Sun, 6 Mar 2011 20:02:06 +0000 (15:02 -0500)
committerRuss Cox <rsc@golang.org>
Sun, 6 Mar 2011 20:02:06 +0000 (15:02 -0500)
R=rsc1, mattn, bradfitzwork, pascal, bradfitzgo
CC=golang-dev
https://golang.org/cl/4214042

src/pkg/http/Makefile
src/pkg/http/cookie.go [new file with mode: 0644]
src/pkg/http/cookie_test.go [new file with mode: 0644]
src/pkg/http/request.go
src/pkg/http/response.go

index 1167d8ef6b7526987ea8011da47d4ebc713bde6b..389b0422274bba8e559fee92f61d903ee105bdd2 100644 (file)
@@ -8,6 +8,7 @@ TARG=http
 GOFILES=\
        chunked.go\
        client.go\
+       cookie.go\
        dump.go\
        fs.go\
        header.go\
diff --git a/src/pkg/http/cookie.go b/src/pkg/http/cookie.go
new file mode 100644 (file)
index 0000000..ff75c47
--- /dev/null
@@ -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
+}
diff --git a/src/pkg/http/cookie_test.go b/src/pkg/http/cookie_test.go
new file mode 100644 (file)
index 0000000..363c841
--- /dev/null
@@ -0,0 +1,96 @@
+// Copyright 2010 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"
+       "reflect"
+       "testing"
+)
+
+
+var writeSetCookiesTests = []struct {
+       Cookies []*Cookie
+       Raw     string
+}{
+       {
+               []*Cookie{&Cookie{Name: "cookie-1", Value: "v$1", MaxAge: -1}},
+               "Set-Cookie: Cookie-1=v$1\r\n",
+       },
+}
+
+func TestWriteSetCookies(t *testing.T) {
+       for i, tt := range writeSetCookiesTests {
+               var w bytes.Buffer
+               writeSetCookies(&w, tt.Cookies)
+               seen := string(w.Bytes())
+               if seen != tt.Raw {
+                       t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.Raw, seen)
+                       continue
+               }
+       }
+}
+
+var writeCookiesTests = []struct {
+       Cookies []*Cookie
+       Raw     string
+}{
+       {
+               []*Cookie{&Cookie{Name: "cookie-1", Value: "v$1", MaxAge: -1}},
+               "Cookie: Cookie-1=v$1\r\n",
+       },
+}
+
+func TestWriteCookies(t *testing.T) {
+       for i, tt := range writeCookiesTests {
+               var w bytes.Buffer
+               writeCookies(&w, tt.Cookies)
+               seen := string(w.Bytes())
+               if seen != tt.Raw {
+                       t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.Raw, seen)
+                       continue
+               }
+       }
+}
+
+var readSetCookiesTests = []struct {
+       Header  Header
+       Cookies []*Cookie
+}{
+       {
+               Header{"Set-Cookie": {"Cookie-1=v$1"}},
+               []*Cookie{&Cookie{Name: "Cookie-1", Value: "v$1", MaxAge: -1, Raw: "Cookie-1=v$1"}},
+       },
+}
+
+func TestReadSetCookies(t *testing.T) {
+       for i, tt := range readSetCookiesTests {
+               c := readSetCookies(tt.Header)
+               if !reflect.DeepEqual(c, tt.Cookies) {
+                       t.Errorf("#%d readSetCookies: have\n%#v\nwant\n%#v\n", i, c, tt.Cookies)
+                       continue
+               }
+       }
+}
+
+var readCookiesTests = []struct {
+       Header  Header
+       Cookies []*Cookie
+}{
+       {
+               Header{"Cookie": {"Cookie-1=v$1"}},
+               []*Cookie{&Cookie{Name: "Cookie-1", Value: "v$1", MaxAge: -1, Raw: "Cookie-1=v$1"}},
+       },
+}
+
+func TestReadCookies(t *testing.T) {
+       for i, tt := range readCookiesTests {
+               c := readCookies(tt.Header)
+               if !reflect.DeepEqual(c, tt.Cookies) {
+                       t.Errorf("#%d readCookies: have\n%#v\nwant\n%#v\n", i, c, tt.Cookies)
+                       continue
+               }
+       }
+}
index 22b19959ddc4f9f18e8ab680a1dad5072ef2218e..2f6e33ae9b0d7dc7f8b133af44fb45d8cc4db946 100644 (file)
@@ -92,6 +92,9 @@ type Request struct {
        // following a hyphen uppercase and the rest lowercase.
        Header Header
 
+       // Cookie records the HTTP cookies sent with the request.
+       Cookie []*Cookie
+
        // The message body.
        Body io.ReadCloser
 
@@ -249,6 +252,10 @@ func (req *Request) write(w io.Writer, usingProxy bool) os.Error {
                return err
        }
 
+       if err = writeCookies(w, req.Cookie); err != nil {
+               return err
+       }
+
        io.WriteString(w, "\r\n")
 
        // Write body and trailer
@@ -485,6 +492,8 @@ func ReadRequest(b *bufio.Reader) (req *Request, err os.Error) {
                return nil, err
        }
 
+       req.Cookie = readCookies(req.Header)
+
        return req, nil
 }
 
index 3f919c86a3cb775baa6f051439a5d04376594c64..4fd00ad61ea873c1434c7d3157ef2835d2cedffa 100644 (file)
@@ -46,6 +46,9 @@ type Response struct {
        // Keys in the map are canonicalized (see CanonicalHeaderKey).
        Header Header
 
+       // SetCookie records the Set-Cookie requests sent with the response.
+       SetCookie []*Cookie
+
        // Body represents the response body.
        Body io.ReadCloser
 
@@ -124,6 +127,8 @@ func ReadResponse(r *bufio.Reader, requestMethod string) (resp *Response, err os
                return nil, err
        }
 
+       resp.SetCookie = readSetCookies(resp.Header)
+
        return resp, nil
 }
 
@@ -193,6 +198,10 @@ func (resp *Response) Write(w io.Writer) os.Error {
                return err
        }
 
+       if err = writeSetCookies(w, resp.SetCookie); err != nil {
+               return err
+       }
+
        // End-of-header
        io.WriteString(w, "\r\n")