aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPetar Maymounkov <petarm@gmail.com>2011-03-06 15:02:06 -0500
committerRuss Cox <rsc@golang.org>2011-03-06 15:02:06 -0500
commit6afe7eba32c06022ef11ab0307d6b3361b59a8f0 (patch)
tree664cb2c72087b1d20ec5470701d5ca7dc38c4698
parenteeb8d00c867570a500029cd113e6e34119c54766 (diff)
downloadgo-6afe7eba32c06022ef11ab0307d6b3361b59a8f0.tar.gz
go-6afe7eba32c06022ef11ab0307d6b3361b59a8f0.zip
http: add cookie support
R=rsc1, mattn, bradfitzwork, pascal, bradfitzgo CC=golang-dev https://golang.org/cl/4214042
-rw-r--r--src/pkg/http/Makefile1
-rw-r--r--src/pkg/http/cookie.go336
-rw-r--r--src/pkg/http/cookie_test.go96
-rw-r--r--src/pkg/http/request.go9
-rw-r--r--src/pkg/http/response.go9
5 files changed, 451 insertions, 0 deletions
diff --git a/src/pkg/http/Makefile b/src/pkg/http/Makefile
index 1167d8ef6b..389b042227 100644
--- a/src/pkg/http/Makefile
+++ b/src/pkg/http/Makefile
@@ -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
index 0000000000..ff75c47c92
--- /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
+}
diff --git a/src/pkg/http/cookie_test.go b/src/pkg/http/cookie_test.go
new file mode 100644
index 0000000000..363c841bb0
--- /dev/null
+++ b/src/pkg/http/cookie_test.go
@@ -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
+ }
+ }
+}
diff --git a/src/pkg/http/request.go b/src/pkg/http/request.go
index 22b19959dd..2f6e33ae9b 100644
--- a/src/pkg/http/request.go
+++ b/src/pkg/http/request.go
@@ -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
}
diff --git a/src/pkg/http/response.go b/src/pkg/http/response.go
index 3f919c86a3..4fd00ad61e 100644
--- a/src/pkg/http/response.go
+++ b/src/pkg/http/response.go
@@ -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")