From 8c6489bc27b639487f56392d13c43f95fab0ac20 Mon Sep 17 00:00:00 2001 From: Volker Dobler Date: Wed, 6 Feb 2013 22:37:34 +1100 Subject: [PATCH] exp/cookiejar: infrastructure for upcoming implementation This CL is the first of a handful of CLs which will provide the implementation of cookiejar. It contains several helper functions and the skeleton of Cookies and SetCookies. Proper host name handling requires the ToASCII transformation from package idna which currently lives in the go.net subrepo. This CL thus contains just a TODO for this issue. R=nigeltao, rsc, bradfitz CC=golang-dev https://golang.org/cl/7287046 --- src/pkg/exp/cookiejar/jar.go | 172 ++++++++++++++++++++++++++++-- src/pkg/exp/cookiejar/jar_test.go | 109 +++++++++++++++++++ 2 files changed, 275 insertions(+), 6 deletions(-) create mode 100644 src/pkg/exp/cookiejar/jar_test.go diff --git a/src/pkg/exp/cookiejar/jar.go b/src/pkg/exp/cookiejar/jar.go index 7f6053b666..dd9cc33e89 100644 --- a/src/pkg/exp/cookiejar/jar.go +++ b/src/pkg/exp/cookiejar/jar.go @@ -6,8 +6,12 @@ package cookiejar import ( + "net" "net/http" "net/url" + "strings" + "sync" + "time" ) // PublicSuffixList provides the public suffix of a domain. For example: @@ -49,26 +53,182 @@ type Options struct { // Jar implements the http.CookieJar interface from the net/http package. type Jar struct { psList PublicSuffixList + + // mu locks the remaining fields. + mu sync.Mutex + + // entries is a set of entries, keyed by their eTLD+1 and subkeyed by + // their name/domain/path. + entries map[string]map[string]entry } // New returns a new cookie jar. A nil *Options is equivalent to a zero // Options. func New(o *Options) (*Jar, error) { - // TODO. - return nil, nil + jar := &Jar{ + entries: make(map[string]map[string]entry), + } + if o != nil { + jar.psList = o.PublicSuffixList + } + return jar, nil +} + +// entry is the internal representation of a cookie. +// The fields are those of RFC 6265. +type entry struct { + Name string + Value string + Domain string + Path string + Secure bool + HttpOnly bool + Persistent bool + HostOnly bool + Expires time.Time + Creation time.Time + LastAccess time.Time } // Cookies implements the Cookies method of the http.CookieJar interface. // // It returns an empty slice if the URL's scheme is not HTTP or HTTPS. -func (j *Jar) Cookies(u *url.URL) []*http.Cookie { - // TODO. - return nil +func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) { + if u.Scheme != "http" && u.Scheme != "https" { + return cookies + } + host, err := canonicalHost(u.Host) + if err != nil { + return cookies + } + key := jarKey(host, j.psList) + + j.mu.Lock() + defer j.mu.Unlock() + + submap := j.entries[key] + if submap == nil { + return cookies + } + + modified := false + for _, _ = range submap { + // TODO: handle expired cookies + // TODO: handle selection of cookies + } + if modified { + if len(submap) == 0 { + delete(j.entries, key) + } else { + j.entries[key] = submap + } + } + + // TODO: proper sorting based on Path length (and Creation) + + return cookies } // SetCookies implements the SetCookies method of the http.CookieJar interface. // // It does nothing if the URL's scheme is not HTTP or HTTPS. func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) { - // TODO. + if len(cookies) == 0 { + return + } + if u.Scheme != "http" && u.Scheme != "https" { + return + } + host, err := canonicalHost(u.Host) + if err != nil { + return + } + key := jarKey(host, j.psList) + if key == "" { + return + } + + j.mu.Lock() + defer j.mu.Unlock() + + submap := j.entries[key] + + modified := false + for _, _ = range cookies { + // TODO: create, update or delete entries in submap + } + + if modified { + if len(submap) == 0 { + delete(j.entries, key) + } else { + j.entries[key] = submap + } + } +} + +// canonicalHost strips port from host if present and returns the canonicalized +// host name as defined by RFC 6265 section 5.1.2. +func canonicalHost(host string) (string, error) { + var err error + host = strings.ToLower(host) + if hasPort(host) { + host, _, err = net.SplitHostPort(host) + if err != nil { + return "", err + } + } + + if strings.HasSuffix(host, ".") { + // Strip trailing dot from fully qualified domain names. + host = host[:len(host)-1] + } + + // TODO: the "canonicalized host name" of RFC 6265 requires the idna ToASCII + // transformation. Possible solutions: + // - promote package idna from go.net to go and import "net/idna" + // - document behavior as a BUG + + return host, nil +} + +// hasPort returns whether host contains a port number. host may be a host +// name, an IPv4 or an IPv6 address. +func hasPort(host string) bool { + colons := strings.Count(host, ":") + if colons == 0 { + return false + } + if colons == 1 { + return true + } + return host[0] == '[' && strings.Contains(host, "]:") +} + +// jarKey returns the key to use for a jar. +func jarKey(host string, psl PublicSuffixList) string { + if isIP(host) { + return host + } + if psl == nil { + // Key cookies under TLD of host. + return host[1+strings.LastIndex(host, "."):] + } + suffix := psl.PublicSuffix(host) + if suffix == host { + return host + } + i := len(host) - len(suffix) + if i <= 0 || host[i-1] != '.' { + // The provided public suffix list psl is broken. + // Storing cookies under host is a safe stopgap. + return host + } + prevDot := strings.LastIndex(host[:i-1], ".") + return host[prevDot+1:] +} + +// isIP returns whether host is an IP address. +func isIP(host string) bool { + return net.ParseIP(host) != nil } diff --git a/src/pkg/exp/cookiejar/jar_test.go b/src/pkg/exp/cookiejar/jar_test.go new file mode 100644 index 0000000000..14f4a08099 --- /dev/null +++ b/src/pkg/exp/cookiejar/jar_test.go @@ -0,0 +1,109 @@ +// Copyright 2013 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 cookiejar + +import ( + "strings" + "testing" +) + +// testPSL implements PublicSuffixList with just two rules: "co.uk" +// and the default rule "*". +type testPSL struct{} + +func (testPSL) String() string { + return "testPSL" +} +func (testPSL) PublicSuffix(d string) string { + if d == "co.uk" || strings.HasSuffix(d, ".co.uk") { + return "co.uk" + } + return d[strings.LastIndex(d, ".")+1:] +} + +var canonicalHostTests = map[string]string{ + "www.example.com": "www.example.com", + "WWW.EXAMPLE.COM": "www.example.com", + "wWw.eXAmple.CoM": "www.example.com", + "www.example.com:80": "www.example.com", + "192.168.0.10": "192.168.0.10", + "192.168.0.5:8080": "192.168.0.5", + "2001:4860:0:2001::68": "2001:4860:0:2001::68", + "[2001:4860:0:::68]:8080": "2001:4860:0:::68", + // "www.bücher.de": "www.xn--bcher-kva.de", // TODO de-comment once proper idna is available + "www.example.com.": "www.example.com", +} + +func TestCanonicalHost(t *testing.T) { + for h, want := range canonicalHostTests { + got, _ := canonicalHost(h) + if got != want { + t.Errorf("%q: got %q, want %q", h, got, want) + } + // TODO handle errors + } +} + +var hasPortTests = map[string]bool{ + "www.example.com": false, + "www.example.com:80": true, + "127.0.0.1": false, + "127.0.0.1:8080": true, + "2001:4860:0:2001::68": false, + "[2001::0:::68]:80": true, +} + +func TestHasPort(t *testing.T) { + for host, want := range hasPortTests { + if got := hasPort(host); got != want { + t.Errorf("%q: got %t, want %t", host, got, want) + } + } +} + +var jarKeyTests = map[string]string{ + "foo.www.example.com": "example.com", + "www.example.com": "example.com", + "example.com": "example.com", + "com": "com", + "foo.www.bbc.co.uk": "bbc.co.uk", + "www.bbc.co.uk": "bbc.co.uk", + "bbc.co.uk": "bbc.co.uk", + "co.uk": "co.uk", + "uk": "uk", + "192.168.0.5": "192.168.0.5", +} + +func TestJarKey(t *testing.T) { + for host, want := range jarKeyTests { + if got := jarKey(host, testPSL{}); got != want { + t.Errorf("%q: got %q, want %q", host, got, want) + } + } + + for _, host := range []string{"www.example.com", "example.com", "com"} { + if got := jarKey(host, nil); got != "com" { + t.Errorf(`%q: got %q, want "com"`, host, got) + } + } +} + +var isIPTests = map[string]bool{ + "127.0.0.1": true, + "1.2.3.4": true, + "2001:4860:0:2001::68": true, + "example.com": false, + "1.1.1.300": false, + "www.foo.bar.net": false, + "123.foo.bar.net": false, +} + +func TestIsIP(t *testing.T) { + for host, want := range isIPTests { + if got := isIP(host); got != want { + t.Errorf("%q: got %t, want %t", host, got, want) + } + } +}