Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
17 changes: 11 additions & 6 deletions 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 Expand Up @@ -1446,7 +1446,8 @@ func Test_Client_Cookie_With_Server(t *testing.T) {

handler := func(c fiber.Ctx) error {
return c.SendString(
c.Cookies("k1") + c.Cookies("k2") + c.Cookies("k3") + c.Cookies("k4"))
c.Cookies("k1") + c.Cookies("k2") + c.Cookies("k3") + c.Cookies("k4"),
)
}

wrapAgent := func(c *Client) {
Expand All @@ -1464,7 +1465,8 @@ func Test_Client_Cookie_With_Server(t *testing.T) {
func Test_Client_CookieJar(t *testing.T) {
handler := func(c fiber.Ctx) error {
return c.SendString(
c.Cookies("k1") + c.Cookies("k2") + c.Cookies("k3"))
c.Cookies("k1") + c.Cookies("k2") + c.Cookies("k3"),
)
}

jar := AcquireCookieJar()
Expand All @@ -1491,7 +1493,8 @@ func Test_Client_CookieJar_Response(t *testing.T) {
Value: "v4",
})
return c.SendString(
c.Cookies("k1") + c.Cookies("k2") + c.Cookies("k3"))
c.Cookies("k1") + c.Cookies("k2") + c.Cookies("k3"),
)
}

jar := AcquireCookieJar()
Expand All @@ -1518,7 +1521,8 @@ func Test_Client_CookieJar_Response(t *testing.T) {
Expires: time.Now().Add(1 * time.Nanosecond),
})
return c.SendString(
c.Cookies("k1") + c.Cookies("k2") + c.Cookies("k3"))
c.Cookies("k1") + c.Cookies("k2") + c.Cookies("k3"),
)
}

jar := AcquireCookieJar()
Expand All @@ -1544,7 +1548,8 @@ func Test_Client_CookieJar_Response(t *testing.T) {
Value: "v2",
})
return c.SendString(
c.Cookies("k1") + c.Cookies("k2"))
c.Cookies("k1") + c.Cookies("k2"),
)
}

jar := AcquireCookieJar()
Expand Down
69 changes: 53 additions & 16 deletions client/cookiejar.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,15 @@ func ReleaseCookieJar(c *CookieJar) {

// CookieJar manages cookie storage for the client. It stores cookies keyed by host.
type CookieJar struct {
Comment thread
gaby marked this conversation as resolved.
hostCookies map[string][]*fasthttp.Cookie
hostCookies map[string][]storedCookie
mu sync.Mutex
}

type storedCookie struct {
cookie *fasthttp.Cookie
hostOnly 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 +86,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 @@ -108,17 +118,24 @@ func (cj *CookieJar) cookiesForRequest(host string, path []byte, secure bool) []
var matched []*fasthttp.Cookie

for domain, cookies := range cj.hostCookies {
if len(cookies) == 0 {
continue
}
if cookies[0].hostOnly && host != domain {
continue
Comment thread
gaby marked this conversation as resolved.
Outdated
}
Comment thread
gaby marked this conversation as resolved.
Outdated
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)

Comment thread
gaby marked this conversation as resolved.
Outdated
if !pathMatch(path, c.Path()) {
continue
Expand Down Expand Up @@ -163,7 +180,7 @@ 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 {
Expand All @@ -180,9 +197,16 @@ func (cj *CookieJar) SetByHost(host []byte, cookies ...*fasthttp.Cookie) {
hostCookies := cj.hostCookies[key]

existing := searchCookieByKeyAndPath(cookie.Key(), cookie.Path(), hostCookies)
hostOnly := len(domain) == 0
Comment thread
gaby marked this conversation as resolved.
Outdated
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
}
}
Comment thread
gaby marked this conversation as resolved.
}
existing.CopyTo(cookie)
cj.hostCookies[key] = hostCookies
Expand Down Expand Up @@ -235,7 +259,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,9 +270,15 @@ 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 {
domain := utils.UnsafeString(domainBytes)
if !domainMatch(hostStr, domain) {
Comment thread
gaby marked this conversation as resolved.
Outdated
Comment thread
gaby marked this conversation as resolved.
Outdated
fasthttp.ReleaseCookie(tmp)
continue
}
key = utils.CopyString(utils.UnsafeString(domainBytes))
tmp.SetDomainBytes(domainBytes)
}
Expand All @@ -257,7 +287,13 @@ func (cj *CookieJar) parseCookiesFromResp(host, _ []byte, resp *fasthttp.Respons
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
}
}
Comment thread
gaby marked this conversation as resolved.
}

c.CopyTo(tmp)
Expand All @@ -266,7 +302,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 +327,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
37 changes: 37 additions & 0 deletions client/cookiejar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,43 @@ func Test_CookieJar_Domain(t *testing.T) {
require.Equal(t, "v", string(cookies[0].Value()))
}

func Test_CookieJar_HostOnlyCookieNotSentToSubdomain(t *testing.T) {
Comment thread
gaby marked this conversation as resolved.
t.Parallel()

jar := &CookieJar{}
origin := fasthttp.AcquireURI()
require.NoError(t, origin.Parse(nil, []byte("http://example.com/")))

c := &fasthttp.Cookie{}
c.SetKey("sid")
c.SetValue("123")
jar.Set(origin, c)

subdomain := fasthttp.AcquireURI()
require.NoError(t, subdomain.Parse(nil, []byte("http://attacker.example.com/")))
require.Empty(t, jar.Get(subdomain))
}

func Test_CookieJar_RejectUnrelatedResponseDomain(t *testing.T) {
t.Parallel()

jar := &CookieJar{}
resp := fasthttp.AcquireResponse()
host := []byte("attacker.invalid")

c := &fasthttp.Cookie{}
c.SetKey("sess")
c.SetValue("evil")
c.SetDomain("victim.example")
resp.Header.SetCookie(c)

jar.parseCookiesFromResp(host, nil, resp)

uri := fasthttp.AcquireURI()
require.NoError(t, uri.Parse(nil, []byte("http://victim.example/")))
require.Empty(t, jar.Get(uri))
}

func Test_CookieJar_Secure(t *testing.T) {
t.Parallel()

Expand Down
Loading