Commit 8c6489bc authored by Volker Dobler's avatar Volker Dobler Committed by Nigel Tao

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
parent fe51d09b
......@@ -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
}
// 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)
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment