src/net/http/cookiejar/jar.go GO 582 lines View on github.com → Search inside
1// Copyright 2012 The Go Authors. All rights reserved.2// Use of this source code is governed by a BSD-style3// license that can be found in the LICENSE file.45// Package cookiejar implements an in-memory [RFC 6265]-compliant [http.CookieJar].6//7// [RFC 6265]: https://www.rfc-editor.org/info/rfc62658package cookiejar910import (11	"cmp"12	"errors"13	"fmt"14	"net"15	"net/http"16	"net/http/internal/ascii"17	"net/netip"18	"net/url"19	"slices"20	"strings"21	"sync"22	"time"23)2425// PublicSuffixList provides the public suffix of a domain. For example:26//   - the public suffix of "example.com" is "com",27//   - the public suffix of "foo1.foo2.foo3.co.uk" is "co.uk", and28//   - the public suffix of "bar.pvt.k12.ma.us" is "pvt.k12.ma.us".29//30// Implementations of PublicSuffixList must be safe for concurrent use by31// multiple goroutines.32//33// An implementation that always returns "" is valid and may be useful for34// testing but it is not secure: it means that the HTTP server for foo.com can35// set a cookie for bar.com.36//37// A public suffix list implementation is in the package38// [golang.org/x/net/publicsuffix].39type PublicSuffixList interface {40	// PublicSuffix returns the public suffix of domain.41	//42	// TODO: specify which of the caller and callee is responsible for IP43	// addresses, for leading and trailing dots, for case sensitivity, and44	// for IDN/Punycode.45	PublicSuffix(domain string) string4647	// String returns a description of the source of this public suffix48	// list. The description will typically contain something like a time49	// stamp or version number.50	String() string51}5253// Options are the options for creating a new [Jar].54type Options struct {55	// PublicSuffixList is the public suffix list that determines whether56	// an HTTP server can set a cookie for a domain.57	//58	// A nil value is valid and may be useful for testing but it is not59	// secure: it means that the HTTP server for foo.co.uk can set a cookie60	// for bar.co.uk.61	PublicSuffixList PublicSuffixList62}6364// Jar implements the [net/http.CookieJar] interface.65type Jar struct {66	psList PublicSuffixList6768	// mu locks the remaining fields.69	mu sync.Mutex7071	// entries is a set of entries, keyed by their eTLD+1 and subkeyed by72	// their name/domain/path.73	entries map[string]map[string]entry7475	// nextSeqNum is the next sequence number assigned to a new cookie76	// created SetCookies.77	nextSeqNum uint6478}7980// New returns a new cookie jar. A nil [*Options] is equivalent to a zero81// Options.82func New(o *Options) (*Jar, error) {83	jar := &Jar{84		entries: make(map[string]map[string]entry),85	}86	if o != nil {87		jar.psList = o.PublicSuffixList88	}89	return jar, nil90}9192// entry is the internal representation of a cookie.93//94// This struct type is not used outside of this package per se, but the exported95// fields are those of RFC 6265.96type entry struct {97	Name       string98	Value      string99	Quoted     bool100	Domain     string101	Path       string102	SameSite   string103	Secure     bool104	HttpOnly   bool105	Persistent bool106	HostOnly   bool107	Expires    time.Time108	Creation   time.Time109	LastAccess time.Time110111	// seqNum is a sequence number so that Cookies returns cookies in a112	// deterministic order, even for cookies that have equal Path length and113	// equal Creation time. This simplifies testing.114	seqNum uint64115}116117// id returns the domain;path;name triple of e as an id.118func (e *entry) id() string {119	return fmt.Sprintf("%s;%s;%s", e.Domain, e.Path, e.Name)120}121122// shouldSend determines whether e's cookie qualifies to be included in a123// request to host/path. It is the caller's responsibility to check if the124// cookie is expired.125func (e *entry) shouldSend(https bool, host, path string) bool {126	return e.domainMatch(host) && e.pathMatch(path) && e.secureMatch(https)127}128129// domainMatch checks whether e's Domain allows sending e back to host.130// It differs from "domain-match" of RFC 6265 section 5.1.3 because we treat131// a cookie with an IP address in the Domain always as a host cookie.132func (e *entry) domainMatch(host string) bool {133	if e.Domain == host {134		return true135	}136	return !e.HostOnly && hasDotSuffix(host, e.Domain)137}138139// pathMatch implements "path-match" according to RFC 6265 section 5.1.4.140func (e *entry) pathMatch(requestPath string) bool {141	if requestPath == e.Path {142		return true143	}144	if strings.HasPrefix(requestPath, e.Path) {145		if e.Path[len(e.Path)-1] == '/' {146			return true // The "/any/" matches "/any/path" case.147		} else if requestPath[len(e.Path)] == '/' {148			return true // The "/any" matches "/any/path" case.149		}150	}151	return false152}153154// secureMatch checks whether a cookie should be sent based on the protocol155// and the Secure flag. Localhost is considered a secure origin regardless156// of protocol, matching browser behavior.157func (e *entry) secureMatch(https bool) bool {158	if !e.Secure {159		// Cookies not marked secure are always sent.160		return true161	}162	// Everything below is about cookies marked secure.163	if https {164		// HTTPS request matches secure cookies.165		return true166	}167	// Consider localhost to be secure like browsers.168	if isLocalhost(e.Domain) {169		return true170	}171	ip, err := netip.ParseAddr(e.Domain)172	if err == nil && ip.IsLoopback() {173		return true174	}175	return false176}177178func isLocalhost(host string) bool {179	host = strings.TrimSuffix(host, ".")180	if idx := strings.LastIndex(host, "."); idx >= 0 {181		host = host[idx+1:]182	}183	return ascii.EqualFold(host, "localhost")184}185186// hasDotSuffix reports whether s ends in "."+suffix.187func hasDotSuffix(s, suffix string) bool {188	return len(s) > len(suffix) && s[len(s)-len(suffix)-1] == '.' && s[len(s)-len(suffix):] == suffix189}190191// Cookies implements the Cookies method of the [http.CookieJar] interface.192//193// It returns an empty slice if the URL's scheme is not HTTP or HTTPS.194func (j *Jar) Cookies(u *url.URL) (cookies []*http.Cookie) {195	return j.cookies(u, time.Now())196}197198// cookies is like Cookies but takes the current time as a parameter.199func (j *Jar) cookies(u *url.URL, now time.Time) (cookies []*http.Cookie) {200	if u.Scheme != "http" && u.Scheme != "https" {201		return cookies202	}203	host, err := canonicalHost(u.Host)204	if err != nil {205		return cookies206	}207	key := jarKey(host, j.psList)208209	j.mu.Lock()210	defer j.mu.Unlock()211212	submap := j.entries[key]213	if submap == nil {214		return cookies215	}216217	https := u.Scheme == "https"218	path := u.Path219	if path == "" {220		path = "/"221	}222223	modified := false224	var selected []entry225	for id, e := range submap {226		if e.Persistent && !e.Expires.After(now) {227			delete(submap, id)228			modified = true229			continue230		}231		if !e.shouldSend(https, host, path) {232			continue233		}234		e.LastAccess = now235		submap[id] = e236		selected = append(selected, e)237		modified = true238	}239	if modified {240		if len(submap) == 0 {241			delete(j.entries, key)242		} else {243			j.entries[key] = submap244		}245	}246247	// sort according to RFC 6265 section 5.4 point 2: by longest248	// path and then by earliest creation time.249	slices.SortFunc(selected, func(a, b entry) int {250		if r := cmp.Compare(b.Path, a.Path); r != 0 {251			return r252		}253		if r := a.Creation.Compare(b.Creation); r != 0 {254			return r255		}256		return cmp.Compare(a.seqNum, b.seqNum)257	})258	for _, e := range selected {259		cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value, Quoted: e.Quoted})260	}261262	return cookies263}264265// SetCookies implements the SetCookies method of the [http.CookieJar] interface.266//267// It does nothing if the URL's scheme is not HTTP or HTTPS.268func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {269	j.setCookies(u, cookies, time.Now())270}271272// setCookies is like SetCookies but takes the current time as parameter.273func (j *Jar) setCookies(u *url.URL, cookies []*http.Cookie, now time.Time) {274	if len(cookies) == 0 {275		return276	}277	if u.Scheme != "http" && u.Scheme != "https" {278		return279	}280	host, err := canonicalHost(u.Host)281	if err != nil {282		return283	}284	key := jarKey(host, j.psList)285	defPath := defaultPath(u.Path)286287	j.mu.Lock()288	defer j.mu.Unlock()289290	submap := j.entries[key]291292	modified := false293	for _, cookie := range cookies {294		e, remove, err := j.newEntry(cookie, now, defPath, host)295		if err != nil {296			continue297		}298		id := e.id()299		if remove {300			if submap != nil {301				if _, ok := submap[id]; ok {302					delete(submap, id)303					modified = true304				}305			}306			continue307		}308		if submap == nil {309			submap = make(map[string]entry)310		}311312		if old, ok := submap[id]; ok {313			e.Creation = old.Creation314			e.seqNum = old.seqNum315		} else {316			e.Creation = now317			e.seqNum = j.nextSeqNum318			j.nextSeqNum++319		}320		e.LastAccess = now321		submap[id] = e322		modified = true323	}324325	if modified {326		if len(submap) == 0 {327			delete(j.entries, key)328		} else {329			j.entries[key] = submap330		}331	}332}333334// canonicalHost strips port from host if present and returns the canonicalized335// host name.336func canonicalHost(host string) (string, error) {337	var err error338	if hasPort(host) {339		host, _, err = net.SplitHostPort(host)340		if err != nil {341			return "", err342		}343	}344	// Strip trailing dot from fully qualified domain names.345	host = strings.TrimSuffix(host, ".")346	encoded, err := toASCII(host)347	if err != nil {348		return "", err349	}350	// We know this is ascii, no need to check.351	lower, _ := ascii.ToLower(encoded)352	return lower, nil353}354355// hasPort reports whether host contains a port number. host may be a host356// name, an IPv4 or an IPv6 address.357func hasPort(host string) bool {358	colons := strings.Count(host, ":")359	if colons == 0 {360		return false361	}362	if colons == 1 {363		return true364	}365	return host[0] == '[' && strings.Contains(host, "]:")366}367368// jarKey returns the key to use for a jar.369func jarKey(host string, psl PublicSuffixList) string {370	if isIP(host) {371		return host372	}373374	var i int375	if psl == nil {376		i = strings.LastIndex(host, ".")377		if i <= 0 {378			return host379		}380	} else {381		suffix := psl.PublicSuffix(host)382		if suffix == host {383			return host384		}385		i = len(host) - len(suffix)386		if i <= 0 || host[i-1] != '.' {387			// The provided public suffix list psl is broken.388			// Storing cookies under host is a safe stopgap.389			return host390		}391		// Only len(suffix) is used to determine the jar key from392		// here on, so it is okay if psl.PublicSuffix("www.buggy.psl")393		// returns "com" as the jar key is generated from host.394	}395	prevDot := strings.LastIndex(host[:i-1], ".")396	return host[prevDot+1:]397}398399// isIP reports whether host is an IP address.400func isIP(host string) bool {401	if strings.ContainsAny(host, ":%") {402		// Probable IPv6 address.403		// Hostnames can't contain : or %, so this is definitely not a valid host.404		// Treating it as an IP is the more conservative option, and avoids the risk405		// of interpreting ::1%.www.example.com as a subdomain of www.example.com.406		return true407	}408	return net.ParseIP(host) != nil409}410411// defaultPath returns the directory part of a URL's path according to412// RFC 6265 section 5.1.4.413func defaultPath(path string) string {414	if len(path) == 0 || path[0] != '/' {415		return "/" // Path is empty or malformed.416	}417418	i := strings.LastIndex(path, "/") // Path starts with "/", so i != -1.419	if i == 0 {420		return "/" // Path has the form "/abc".421	}422	return path[:i] // Path is either of form "/abc/xyz" or "/abc/xyz/".423}424425// newEntry creates an entry from an http.Cookie c. now is the current time and426// is compared to c.Expires to determine deletion of c. defPath and host are the427// default-path and the canonical host name of the URL c was received from.428//429// remove records whether the jar should delete this cookie, as it has already430// expired with respect to now. In this case, e may be incomplete, but it will431// be valid to call e.id (which depends on e's Name, Domain and Path).432//433// A malformed c.Domain will result in an error.434func (j *Jar) newEntry(c *http.Cookie, now time.Time, defPath, host string) (e entry, remove bool, err error) {435	e.Name = c.Name436437	if c.Path == "" || c.Path[0] != '/' {438		e.Path = defPath439	} else {440		e.Path = c.Path441	}442443	e.Domain, e.HostOnly, err = j.domainAndType(host, c.Domain)444	if err != nil {445		return e, false, err446	}447448	// MaxAge takes precedence over Expires.449	if c.MaxAge < 0 {450		return e, true, nil451	} else if c.MaxAge > 0 {452		e.Expires = now.Add(time.Duration(c.MaxAge) * time.Second)453		e.Persistent = true454	} else {455		if c.Expires.IsZero() {456			e.Expires = endOfTime457			e.Persistent = false458		} else {459			if !c.Expires.After(now) {460				return e, true, nil461			}462			e.Expires = c.Expires463			e.Persistent = true464		}465	}466467	e.Value = c.Value468	e.Quoted = c.Quoted469	e.Secure = c.Secure470	e.HttpOnly = c.HttpOnly471472	switch c.SameSite {473	case http.SameSiteDefaultMode:474		e.SameSite = "SameSite"475	case http.SameSiteStrictMode:476		e.SameSite = "SameSite=Strict"477	case http.SameSiteLaxMode:478		e.SameSite = "SameSite=Lax"479	}480481	return e, false, nil482}483484var (485	errIllegalDomain   = errors.New("cookiejar: illegal cookie domain attribute")486	errMalformedDomain = errors.New("cookiejar: malformed cookie domain attribute")487)488489// endOfTime is the time when session (non-persistent) cookies expire.490// This instant is representable in most date/time formats (not just491// Go's time.Time) and should be far enough in the future.492var endOfTime = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)493494// domainAndType determines the cookie's domain and hostOnly attribute.495func (j *Jar) domainAndType(host, domain string) (string, bool, error) {496	if domain == "" {497		// No domain attribute in the SetCookie header indicates a498		// host cookie.499		return host, true, nil500	}501502	if isIP(host) {503		// RFC 6265 is not super clear here, a sensible interpretation504		// is that cookies with an IP address in the domain-attribute505		// are allowed.506507		// RFC 6265 section 5.2.3 mandates to strip an optional leading508		// dot in the domain-attribute before processing the cookie.509		//510		// Most browsers don't do that for IP addresses, only curl511		// (version 7.54) and IE (version 11) do not reject a512		//     Set-Cookie: a=1; domain=.127.0.0.1513		// This leading dot is optional and serves only as hint for514		// humans to indicate that a cookie with "domain=.bbc.co.uk"515		// would be sent to every subdomain of bbc.co.uk.516		// It just doesn't make sense on IP addresses.517		// The other processing and validation steps in RFC 6265 just518		// collapse to:519		if host != domain {520			return "", false, errIllegalDomain521		}522523		// According to RFC 6265 such cookies should be treated as524		// domain cookies.525		// As there are no subdomains of an IP address the treatment526		// according to RFC 6265 would be exactly the same as that of527		// a host-only cookie. Contemporary browsers (and curl) do528		// allows such cookies but treat them as host-only cookies.529		// So do we as it just doesn't make sense to label them as530		// domain cookies when there is no domain; the whole notion of531		// domain cookies requires a domain name to be well defined.532		return host, true, nil533	}534535	// From here on: If the cookie is valid, it is a domain cookie (with536	// the one exception of a public suffix below).537	// See RFC 6265 section 5.2.3.538	domain = strings.TrimPrefix(domain, ".")539540	if len(domain) == 0 || domain[0] == '.' {541		// Received either "Domain=." or "Domain=..some.thing",542		// both are illegal.543		return "", false, errMalformedDomain544	}545546	domain, isASCII := ascii.ToLower(domain)547	if !isASCII {548		// Received non-ASCII domain, e.g. "perché.com" instead of "xn--perch-fsa.com"549		return "", false, errMalformedDomain550	}551552	if domain[len(domain)-1] == '.' {553		// We received stuff like "Domain=www.example.com.".554		// Browsers do handle such stuff (actually differently) but555		// RFC 6265 seems to be clear here (e.g. section 4.1.2.3) in556		// requiring a reject.  4.1.2.3 is not normative, but557		// "Domain Matching" (5.1.3) and "Canonicalized Host Names"558		// (5.1.2) are.559		return "", false, errMalformedDomain560	}561562	// See RFC 6265 section 5.3 #5.563	if j.psList != nil {564		if ps := j.psList.PublicSuffix(domain); ps != "" && !hasDotSuffix(domain, ps) {565			if host == domain {566				// This is the one exception in which a cookie567				// with a domain attribute is a host cookie.568				return host, true, nil569			}570			return "", false, errIllegalDomain571		}572	}573574	// The domain must domain-match host: www.mycompany.com cannot575	// set cookies for .ourcompetitors.com.576	if host != domain && !hasDotSuffix(host, domain) {577		return "", false, errIllegalDomain578	}579580	return domain, false, nil581}

Code quality findings 6

Ensure errors are handled or logged
warning correctness unhandled-error
if err != nil {
Ensure errors are handled or logged
warning correctness unhandled-error
if err != nil {
Defer inside loop; deferred calls accumulate until the function returns, not until the loop iteration ends. This can cause resource leaks
warning correctness defer-in-loop
defer j.mu.Unlock()
Ensure errors are handled or logged
warning correctness unhandled-error
if err != nil {
Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for id, e := range submap {
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
cookies = append(cookies, &http.Cookie{Name: e.Name, Value: e.Value, Quoted: e.Quoted})

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.