feat: initial goget release — modern IPv6-first download utility
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
//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))
|
||||
}
|
||||
Reference in New Issue
Block a user