Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ func TestClientResetClearsState(t *testing.T) {
client := New()

jar := AcquireCookieJar()
jar.hostCookies = map[string][]*fasthttp.Cookie{"example.com": {}}
jar.hostCookies = map[string][]storedCookie{"example.com": {}}
client.SetCookieJar(jar)

client.SetBaseURL("http://example.com")
Expand Down
151 changes: 127 additions & 24 deletions client/cookiejar.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
utilsbytes "github.com/gofiber/utils/v2/bytes"
utilsstrings "github.com/gofiber/utils/v2/strings"
"github.com/valyala/fasthttp"
"golang.org/x/net/publicsuffix"
)

var cookieJarPool = sync.Pool{
Expand All @@ -36,12 +37,28 @@ func ReleaseCookieJar(c *CookieJar) {
cookieJarPool.Put(c)
}

// CookieJar manages cookie storage for the client. It stores cookies keyed by host.
// CookieJar manages cookie storage for the client.
type CookieJar struct {
Comment thread
gaby marked this conversation as resolved.
hostCookies map[string][]*fasthttp.Cookie
// hostCookies stores wrapped cookies keyed by storage scope:
// host-only cookies use the request host, while domain cookies use the
// accepted Domain attribute.
// If release logic is re-enabled for these entries, iterate as storedCookie
// values and call fasthttp.ReleaseCookie(stored.cookie) on the wrapped cookie.
hostCookies map[string][]storedCookie
mu sync.Mutex
}

type storedCookie struct {
cookie *fasthttp.Cookie
hostOnly bool
}

type cookieDomainAcceptance struct {
domain string
hostOnly bool
ok bool
}

// Get returns all cookies stored for a given URI. If there are no cookies for the
// provided host, the returned slice will be nil.
//
Expand Down Expand Up @@ -81,20 +98,25 @@ func (cj *CookieJar) getCookiesByHost(host string) []*fasthttp.Cookie {
defer cj.mu.Unlock()

now := time.Now()
cookies := cj.hostCookies[host]
stored := cj.hostCookies[host]

kept := cookies[:0]
for _, c := range cookies {
kept := stored[:0]
for _, sc := range stored {
c := sc.cookie
// Remove expired cookies.
if !c.Expire().Equal(fasthttp.CookieExpireUnlimited) && c.Expire().Before(now) {
fasthttp.ReleaseCookie(c)
continue
}
kept = append(kept, c)
kept = append(kept, sc)
}
cj.hostCookies[host] = kept

return kept
out := make([]*fasthttp.Cookie, 0, len(kept))
for _, sc := range kept {
out = append(out, sc.cookie)
}
return out
}

// cookiesForRequest returns cookies that match the given host, path and security settings.
Expand All @@ -104,22 +126,30 @@ func (cj *CookieJar) cookiesForRequest(host string, path []byte, secure bool) []
cj.mu.Lock()
defer cj.mu.Unlock()

host = utilsstrings.ToLower(host)
now := time.Now()
var matched []*fasthttp.Cookie

for domain, cookies := range cj.hostCookies {
if len(cookies) == 0 {
continue
}
if !domainMatch(host, domain) {
continue
}

kept := cookies[:0]
for _, c := range cookies {
for _, sc := range cookies {
c := sc.cookie
if !c.Expire().Equal(fasthttp.CookieExpireUnlimited) && c.Expire().Before(now) {
fasthttp.ReleaseCookie(c)
continue
}
kept = append(kept, c)
kept = append(kept, sc)

if sc.hostOnly && host != domain {
continue
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if !pathMatch(path, c.Path()) {
continue
}
Expand Down Expand Up @@ -163,28 +193,43 @@ func (cj *CookieJar) SetByHost(host []byte, cookies ...*fasthttp.Cookie) {
defer cj.mu.Unlock()

if cj.hostCookies == nil {
cj.hostCookies = make(map[string][]*fasthttp.Cookie)
cj.hostCookies = make(map[string][]storedCookie)
}

for _, cookie := range cookies {
domain := utils.TrimLeft(cookie.Domain(), '.')
utilsbytes.UnsafeToLower(domain)
key := hostKey
if len(domain) == 0 {
cookie.SetDomain(hostStr)
} else {
key = utils.CopyString(utils.UnsafeString(domain))
cookie.SetDomainBytes(domain)
storedDomain := hostStr
hostOnly := len(domain) == 0
if !hostOnly {
acceptance := acceptCookieDomain(hostStr, utils.UnsafeString(domain))
if !acceptance.ok {
continue
}
hostOnly = acceptance.hostOnly
if !hostOnly {
key = utils.CopyString(acceptance.domain)
storedDomain = acceptance.domain
}
}

hostCookies := cj.hostCookies[key]

existing := searchCookieByKeyAndPath(cookie.Key(), cookie.Path(), hostCookies)
if existing == nil {
existing = fasthttp.AcquireCookie()
hostCookies = append(hostCookies, existing)
hostCookies = append(hostCookies, storedCookie{cookie: existing, hostOnly: hostOnly})
} else {
for i := range hostCookies {
if hostCookies[i].cookie == existing {
hostCookies[i].hostOnly = hostOnly
break
}
}
Comment thread
gaby marked this conversation as resolved.
}
existing.CopyTo(cookie)
existing.SetDomain(storedDomain)
cj.hostCookies[key] = hostCookies
}
}
Expand Down Expand Up @@ -235,7 +280,7 @@ func (cj *CookieJar) parseCookiesFromResp(host, _ []byte, resp *fasthttp.Respons
defer cj.mu.Unlock()

if cj.hostCookies == nil {
cj.hostCookies = make(map[string][]*fasthttp.Cookie)
cj.hostCookies = make(map[string][]storedCookie)
}

now := time.Now()
Expand All @@ -246,18 +291,37 @@ func (cj *CookieJar) parseCookiesFromResp(host, _ []byte, resp *fasthttp.Respons
domainBytes := utils.TrimLeft(tmp.Domain(), '.')
utilsbytes.UnsafeToLower(domainBytes)
key := hostKey
if len(domainBytes) == 0 {
hostOnly := len(domainBytes) == 0
if hostOnly {
tmp.SetDomain(hostStr)
Comment thread
gaby marked this conversation as resolved.
} else {
key = utils.CopyString(utils.UnsafeString(domainBytes))
tmp.SetDomainBytes(domainBytes)
domain := utils.UnsafeString(domainBytes)
acceptance := acceptCookieDomain(hostStr, domain)
if !acceptance.ok {
Comment thread
gaby marked this conversation as resolved.
fasthttp.ReleaseCookie(tmp)
continue
}
hostOnly = acceptance.hostOnly
if hostOnly {
tmp.SetDomain(hostStr)
} else {
key = utils.CopyString(acceptance.domain)
tmp.SetDomain(acceptance.domain)
}
}

cookies := cj.hostCookies[key]
c := searchCookieByKeyAndPath(tmp.Key(), tmp.Path(), cookies)
if c == nil {
c = fasthttp.AcquireCookie()
cookies = append(cookies, c)
cookies = append(cookies, storedCookie{cookie: c, hostOnly: hostOnly})
} else {
for i := range cookies {
if cookies[i].cookie == c {
cookies[i].hostOnly = hostOnly
break
}
}
Comment thread
gaby marked this conversation as resolved.
}

c.CopyTo(tmp)
Expand All @@ -266,7 +330,7 @@ func (cj *CookieJar) parseCookiesFromResp(host, _ []byte, resp *fasthttp.Respons
} else {
kept := cookies[:0]
for _, v := range cookies {
if v != c {
if v.cookie != c {
kept = append(kept, v)
}
}
Expand All @@ -291,8 +355,9 @@ func (cj *CookieJar) Release() {
}

// searchCookieByKeyAndPath looks up a cookie by its key and path from the provided slice of cookies.
func searchCookieByKeyAndPath(key, path []byte, cookies []*fasthttp.Cookie) *fasthttp.Cookie {
for _, c := range cookies {
func searchCookieByKeyAndPath(key, path []byte, cookies []storedCookie) *fasthttp.Cookie {
for _, sc := range cookies {
c := sc.cookie
if bytes.Equal(key, c.Key()) {
if pathMatch(path, c.Path()) {
return c
Expand Down Expand Up @@ -332,3 +397,41 @@ func domainMatch(host, domain string) bool {
}
return strings.HasSuffix(host, "."+domain)
}

// acceptCookieDomain enforces RFC 6265 response-domain acceptance. Trailing-dot,
// exact-match public-suffix, and exact-match IP-literal Domain attributes are
// downgraded to host-only so same-host behavior is preserved without storing
// cookies under shared suffixes or allowing IP suffix matching across
// unrelated hosts.
func acceptCookieDomain(host, domain string) cookieDomainAcceptance {
if strings.HasSuffix(domain, ".") {
return cookieDomainAcceptance{domain: host, hostOnly: true, ok: true}
}

if host == domain {
if isIPLiteral(domain) || isPublicSuffixDomain(domain) {
return cookieDomainAcceptance{domain: host, hostOnly: true, ok: true}
Comment thread
gaby marked this conversation as resolved.
}
return cookieDomainAcceptance{domain: domain, ok: true}
}

if isIPLiteral(host) || isIPLiteral(domain) || isPublicSuffixDomain(domain) || !domainMatch(host, domain) {
Comment thread
gaby marked this conversation as resolved.
return cookieDomainAcceptance{}
}

return cookieDomainAcceptance{domain: domain, ok: true}
}

func isIPLiteral(host string) bool {
if len(host) >= 2 && host[0] == '[' && host[len(host)-1] == ']' {
host = host[1 : len(host)-1]
}

return net.ParseIP(host) != nil
}

func isPublicSuffixDomain(domain string) bool {
suffix, _ := publicsuffix.PublicSuffix(domain)

return suffix == domain
}
Loading
Loading