diff --git a/go_backend/extension_cookie_jar_test.go b/go_backend/extension_cookie_jar_test.go new file mode 100644 index 00000000..3d3e54bd --- /dev/null +++ b/go_backend/extension_cookie_jar_test.go @@ -0,0 +1,57 @@ +package gobackend + +import ( + "net/http" + "net/url" + "testing" + "time" +) + +func TestSimpleCookieJarReplacesAndExpiresCookies(t *testing.T) { + jar, err := newSimpleCookieJar() + if err != nil { + t.Fatalf("newSimpleCookieJar: %v", err) + } + u, _ := url.Parse("https://api.example.com/path") + + jar.SetCookies(u, []*http.Cookie{{Name: "session", Value: "old", Path: "/"}}) + jar.SetCookies(u, []*http.Cookie{{Name: "session", Value: "new", Path: "/"}}) + cookies := jar.Cookies(u) + if len(cookies) != 1 || cookies[0].Value != "new" { + t.Fatalf("replacement cookies = %#v", cookies) + } + + jar.SetCookies(u, []*http.Cookie{{ + Name: "session", + Value: "expired", + Path: "/", + Expires: time.Now().Add(-time.Hour), + MaxAge: -1, + }}) + if cookies := jar.Cookies(u); len(cookies) != 0 { + t.Fatalf("expired cookies = %#v", cookies) + } +} + +func TestSimpleCookieJarClearAndScope(t *testing.T) { + jar, err := newSimpleCookieJar() + if err != nil { + t.Fatalf("newSimpleCookieJar: %v", err) + } + apiURL, _ := url.Parse("https://api.example.com/private/resource") + otherURL, _ := url.Parse("https://other.example.com/private/resource") + publicURL, _ := url.Parse("https://api.example.com/public") + + jar.SetCookies(apiURL, []*http.Cookie{{Name: "session", Value: "value", Path: "/private"}}) + if len(jar.Cookies(apiURL)) != 1 { + t.Fatal("expected cookie for matching host and path") + } + if len(jar.Cookies(otherURL)) != 0 || len(jar.Cookies(publicURL)) != 0 { + t.Fatal("cookie escaped its host or path scope") + } + + jar.Clear() + if len(jar.Cookies(apiURL)) != 0 { + t.Fatal("Clear retained cookies") + } +} diff --git a/go_backend/extension_runtime.go b/go_backend/extension_runtime.go index d3f11dc1..52d6f78b 100644 --- a/go_backend/extension_runtime.go +++ b/go_backend/extension_runtime.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "net/http" + "net/http/cookiejar" "net/url" "slices" "strconv" @@ -494,27 +495,36 @@ func isPrivateIPAddr(ip net.IP) bool { } type simpleCookieJar struct { - cookies map[string][]*http.Cookie - mu sync.RWMutex + mu sync.RWMutex + jar *cookiejar.Jar } func newSimpleCookieJar() (*simpleCookieJar, error) { - return &simpleCookieJar{ - cookies: make(map[string][]*http.Cookie), - }, nil + jar, err := cookiejar.New(nil) + if err != nil { + return nil, err + } + return &simpleCookieJar{jar: jar}, nil } func (j *simpleCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) { - j.mu.Lock() - defer j.mu.Unlock() - key := u.Host - j.cookies[key] = append(j.cookies[key], cookies...) + j.mu.RLock() + defer j.mu.RUnlock() + j.jar.SetCookies(u, cookies) } func (j *simpleCookieJar) Cookies(u *url.URL) []*http.Cookie { j.mu.RLock() defer j.mu.RUnlock() - return j.cookies[u.Host] + return j.jar.Cookies(u) + +} + +func (j *simpleCookieJar) Clear() { + jar, _ := cookiejar.New(nil) + j.mu.Lock() + j.jar = jar + j.mu.Unlock() } func (r *extensionRuntime) SetSettings(settings map[string]any) { diff --git a/go_backend/extension_runtime_http.go b/go_backend/extension_runtime_http.go index c4d1118a..988f0eba 100644 --- a/go_backend/extension_runtime_http.go +++ b/go_backend/extension_runtime_http.go @@ -292,9 +292,7 @@ func (r *extensionRuntime) httpMethodShortcut(method string, call goja.FunctionC func (r *extensionRuntime) httpClearCookies(call goja.FunctionCall) goja.Value { if jar, ok := r.cookieJar.(*simpleCookieJar); ok { - jar.mu.Lock() - jar.cookies = make(map[string][]*http.Cookie) - jar.mu.Unlock() + jar.Clear() GoLog("[Extension:%s] Cookies cleared\n", r.extensionID) return r.vm.ToValue(true) }