Files
goget/internal/cookie/jar.go
T

202 lines
4.7 KiB
Go
Raw Permalink Normal View History

//go:build linux || freebsd
// +build linux freebsd
package cookie
import (
"bufio"
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"codeberg.org/petrbalvin/goget/internal/core"
"golang.org/x/net/publicsuffix"
)
// Jar wraps http.CookieJar with Netscape format import/export.
type Jar struct {
*cookiejar.Jar
mu sync.Mutex
cookies map[string][]*http.Cookie // domain -> cookies (for export)
}
// NewJar creates a new cookie jar with public suffix support.
func NewJar() (*Jar, error) {
jar, err := cookiejar.New(&cookiejar.Options{
PublicSuffixList: publicsuffix.List,
})
if err != nil {
return nil, err
}
return &Jar{
Jar: jar,
cookies: make(map[string][]*http.Cookie),
}, nil
}
// SetCookies stores cookies in the jar and tracks them for export.
func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
j.Jar.SetCookies(u, cookies)
j.mu.Lock()
defer j.mu.Unlock()
for _, c := range cookies {
// Key the export map by the cookie's effective domain, not the
// URL hostname. A cookie set for "example.com" via a request to
// "www.example.com" must round-trip through SaveToFile/LoadFromFile
// keyed by "example.com", otherwise the second SetCookies call
// (from LoadFromFile) lands under a different map key and the
// cookies appear to be silently lost between sessions.
key := cookieStorageKey(c, u)
j.cookies[key] = append(j.cookies[key], c)
}
}
// cookieStorageKey returns the canonical (no leading dot) domain used as
// the storage key for the export map. It prefers the cookie's own Domain
// attribute (which reflects the cookie's actual scope) and falls back to
// the URL hostname for host-only cookies (those without a Domain attr).
func cookieStorageKey(c *http.Cookie, u *url.URL) string {
if c.Domain != "" {
return strings.TrimPrefix(c.Domain, ".")
}
return u.Hostname()
}
// LoadFromFile loads cookies from a file in Netscape format.
func (j *Jar) LoadFromFile(path string) error {
if path == "" {
return nil
}
file, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("failed to open cookie jar: %w", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
fields := strings.Split(line, "\t")
if len(fields) < 7 {
continue
}
domain := fields[0]
path := fields[2]
expiresStr := fields[4]
name := fields[5]
value := fields[6]
expires, err := strconv.ParseInt(expiresStr, 10, 64)
if err != nil {
continue
}
if expires > 0 && time.Now().Unix() > expires {
continue
}
// Strip the leading dot from the Netscape-format domain so the
// round-trip back through SetCookies uses a stable key. Go's
// net/url strips the leading dot from Hostname() since Go 1.20,
// but matching that behaviour here makes the export map key
// independent of Go stdlib version.
domainBare := strings.TrimPrefix(domain, ".")
scheme := "https"
if fields[3] != "TRUE" {
scheme = "http"
}
u := &url.URL{
Scheme: scheme,
Host: domainBare,
Path: path,
}
cookie := &http.Cookie{
Name: name,
Value: value,
Path: path,
Domain: domainBare,
Expires: time.Unix(expires, 0),
Secure: fields[3] == "TRUE",
}
j.SetCookies(u, []*http.Cookie{cookie})
}
return scanner.Err()
}
// SaveToFile saves cookies to a file in Netscape format.
func (j *Jar) SaveToFile(path string) error {
if path == "" {
return nil
}
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("failed to create cookie jar: %w", err)
}
defer file.Close()
fmt.Fprintln(file, "# Netscape HTTP Cookie File")
fmt.Fprintf(file, "# Generated by goget %s at %s\n", core.Version, time.Now().Format(time.RFC3339))
fmt.Fprintln(file, "# Format: domain flag path secure expiration name value")
fmt.Fprintln(file)
j.mu.Lock()
defer j.mu.Unlock()
for domain, cookies := range j.cookies {
for _, c := range cookies {
secure := "FALSE"
if c.Secure {
secure = "TRUE"
}
// The map key is always stored without a leading dot, but
// the Netscape format expects one — always emit it.
expires := c.Expires.Unix()
if expires <= 0 {
expires = time.Now().Add(365 * 24 * time.Hour).Unix()
}
fmt.Fprintf(file, ".%s\t%s\t%s\t%s\t%d\t%s\t%s\n",
domain,
"TRUE",
c.Path,
secure,
expires,
c.Name,
c.Value,
)
}
}
return nil
}
// GetCookieCount returns the number of cookies for a given URL.
func (j *Jar) GetCookieCount(u *url.URL) int {
return len(j.Cookies(u))
}