202 lines
4.7 KiB
Go
202 lines
4.7 KiB
Go
//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))
|
|
}
|