diff options
author | Petar Maymounkov <petarm@gmail.com> | 2011-03-06 15:02:06 -0500 |
---|---|---|
committer | Russ Cox <rsc@golang.org> | 2011-03-06 15:02:06 -0500 |
commit | 6afe7eba32c06022ef11ab0307d6b3361b59a8f0 (patch) | |
tree | 664cb2c72087b1d20ec5470701d5ca7dc38c4698 | |
parent | eeb8d00c867570a500029cd113e6e34119c54766 (diff) | |
download | go-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/Makefile | 1 | ||||
-rw-r--r-- | src/pkg/http/cookie.go | 336 | ||||
-rw-r--r-- | src/pkg/http/cookie_test.go | 96 | ||||
-rw-r--r-- | src/pkg/http/request.go | 9 | ||||
-rw-r--r-- | src/pkg/http/response.go | 9 |
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") |