From 498e6923484550e56a95f4886e24d257f68a35cf Mon Sep 17 00:00:00 2001 From: xiazemin <465474307@qq.com> Date: Wed, 11 Oct 2023 00:35:22 +0800 Subject: [PATCH 1/3] 1 --- README.md | 10 +++--- auto_example_test.go | 6 ++-- browser/all/import.go | 32 +++++++++---------- browser/browsh/browsh.go | 6 ++-- browser/browsh/find.go | 6 ++-- browser/chrome/chrome.go | 6 ++-- browser/chrome/chrome_test.go | 6 ++-- browser/chrome/find.go | 8 ++--- browser/chromium/chromium.go | 6 ++-- browser/chromium/find.go | 8 ++--- browser/dillo/dillo.go | 6 ++-- browser/dillo/dillo_test.go | 2 +- browser/dillo/find.go | 6 ++-- browser/edge/edge.go | 8 ++--- browser/edge/find.go | 12 +++---- browser/elinks/elinks.go | 4 +-- browser/elinks/elinks_test.go | 2 +- browser/elinks/find.go | 4 +-- browser/epiphany/cookiestore.go | 2 +- browser/epiphany/epiphany.go | 6 ++-- browser/epiphany/find.go | 4 +-- browser/firefox/find.go | 8 ++--- browser/firefox/firefox.go | 6 ++-- browser/firefox/firefox_test.go | 2 +- browser/ie/find.go | 8 ++--- browser/ie/ie.go | 6 ++-- browser/ie/ie_test.go | 2 +- browser/konqueror/find.go | 4 +-- browser/konqueror/konqueror.go | 4 +-- browser/konqueror/konqueror_test.go | 2 +- browser/lynx/find.go | 6 ++-- browser/lynx/lynx.go | 6 ++-- browser/lynx/lynx_test.go | 2 +- browser/netscape/find.go | 8 ++--- browser/netscape/netscape.go | 6 ++-- browser/netscape/netscape_test.go | 2 +- browser/opera/cookies4dat.go | 2 +- browser/opera/cookiestore.go | 2 +- browser/opera/find.go | 6 ++-- browser/opera/opera.go | 8 ++--- browser/safari/find.go | 4 +-- browser/safari/safari.go | 6 ++-- browser/safari/safari_test.go | 4 +-- browser/uzbl/find.go | 6 ++-- browser/uzbl/uzbl.go | 6 ++-- browser/uzbl/uzbl_test.go | 2 +- browser/w3m/find.go | 4 +-- browser/w3m/w3m.go | 4 +-- browser/w3m/w3m_test.go | 2 +- chrome_example_test.go | 2 +- cookiejar_example_test.go | 4 +-- cookiestores_example_test.go | 4 +-- export_example_test.go | 2 +- filter_example_test.go | 2 +- filtercookies_example_test.go | 4 +-- find.go | 8 ++--- go.mod | 6 ++-- go.sum | 49 +++-------------------------- internal/chrome/chrome.go | 6 ++-- internal/chrome/cookiestore.go | 2 +- internal/cookies/cookiejar.go | 2 +- internal/cookies/cookiestore.go | 2 +- internal/firefox/cookiestore.go | 2 +- internal/firefox/firefox.go | 4 +-- internal/ie/cookiestore.go | 6 ++-- internal/ie/ese.go | 4 +-- internal/ie/find/find_windows.go | 6 ++-- internal/ie/ie.go | 2 +- internal/ie/iecache.go | 8 ++--- internal/ie/textcookies.go | 6 ++-- internal/netscape/cookiestore.go | 2 +- internal/netscape/netscape.go | 2 +- 72 files changed, 186 insertions(+), 227 deletions(-) diff --git a/README.md b/README.md index 899b574..6292c11 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # kooky -[![PkgGoDev](https://pkg.go.dev/badge/github.com/browserutils/kooky)](https://pkg.go.dev/github.com/browserutils/kooky) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/xiazemin/kooky)](https://pkg.go.dev/github.com/xiazemin/kooky) [![Go Report Card](https://goreportcard.com/badge/zellyn/kooky)](https://goreportcard.com/report/zellyn/kooky) ![Lines of code](https://img.shields.io/tokei/lines/github/zellyn/kooky) [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) @@ -49,8 +49,8 @@ package main import ( "fmt" - "github.com/browserutils/kooky" - _ "github.com/browserutils/kooky/browser/all" // register cookie store finders! + "github.com/xiazemin/kooky" + _ "github.com/xiazemin/kooky/browser/all" // register cookie store finders! ) func main() { @@ -74,7 +74,7 @@ import ( "log" "os" - "github.com/browserutils/kooky/browser/chrome" + "github.com/xiazemin/kooky/browser/chrome" ) func main() { @@ -100,7 +100,7 @@ import ( "log" "os" - "github.com/browserutils/kooky/browser/safari" + "github.com/xiazemin/kooky/browser/safari" ) func main() { diff --git a/auto_example_test.go b/auto_example_test.go index 98f8a6f..d98f82e 100644 --- a/auto_example_test.go +++ b/auto_example_test.go @@ -3,9 +3,9 @@ package kooky_test import ( "fmt" - "github.com/browserutils/kooky" - _ "github.com/browserutils/kooky/browser/all" // This registers all cookiestore finders! - // _ "github.com/browserutils/kooky/browser/chrome" // load only the chrome cookiestore finder + "github.com/xiazemin/kooky" + _ "github.com/xiazemin/kooky/browser/all" // This registers all cookiestore finders! + // _ "github.com/xiazemin/kooky/browser/chrome" // load only the chrome cookiestore finder ) func ExampleReadCookies_all() { diff --git a/browser/all/import.go b/browser/all/import.go index 7f0fdc9..ee2948b 100644 --- a/browser/all/import.go +++ b/browser/all/import.go @@ -1,20 +1,20 @@ package all import ( - _ "github.com/browserutils/kooky/browser/browsh" - _ "github.com/browserutils/kooky/browser/chrome" - _ "github.com/browserutils/kooky/browser/chromium" - _ "github.com/browserutils/kooky/browser/dillo" - _ "github.com/browserutils/kooky/browser/edge" - _ "github.com/browserutils/kooky/browser/elinks" - _ "github.com/browserutils/kooky/browser/epiphany" - _ "github.com/browserutils/kooky/browser/firefox" - _ "github.com/browserutils/kooky/browser/ie" - _ "github.com/browserutils/kooky/browser/konqueror" - _ "github.com/browserutils/kooky/browser/lynx" - _ "github.com/browserutils/kooky/browser/netscape" - _ "github.com/browserutils/kooky/browser/opera" - _ "github.com/browserutils/kooky/browser/safari" - _ "github.com/browserutils/kooky/browser/uzbl" - _ "github.com/browserutils/kooky/browser/w3m" + _ "github.com/xiazemin/kooky/browser/browsh" + _ "github.com/xiazemin/kooky/browser/chrome" + _ "github.com/xiazemin/kooky/browser/chromium" + _ "github.com/xiazemin/kooky/browser/dillo" + _ "github.com/xiazemin/kooky/browser/edge" + _ "github.com/xiazemin/kooky/browser/elinks" + _ "github.com/xiazemin/kooky/browser/epiphany" + _ "github.com/xiazemin/kooky/browser/firefox" + _ "github.com/xiazemin/kooky/browser/ie" + _ "github.com/xiazemin/kooky/browser/konqueror" + _ "github.com/xiazemin/kooky/browser/lynx" + _ "github.com/xiazemin/kooky/browser/netscape" + _ "github.com/xiazemin/kooky/browser/opera" + _ "github.com/xiazemin/kooky/browser/safari" + _ "github.com/xiazemin/kooky/browser/uzbl" + _ "github.com/xiazemin/kooky/browser/w3m" ) diff --git a/browser/browsh/browsh.go b/browser/browsh/browsh.go index ee8010d..9fa352e 100644 --- a/browser/browsh/browsh.go +++ b/browser/browsh/browsh.go @@ -4,9 +4,9 @@ package browsh import ( "net/http" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/firefox" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/firefox" ) func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { diff --git a/browser/browsh/find.go b/browser/browsh/find.go index bcb8073..bfbf488 100644 --- a/browser/browsh/find.go +++ b/browser/browsh/find.go @@ -6,9 +6,9 @@ import ( "os" "path/filepath" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/firefox" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/firefox" ) type browshFinder struct{} diff --git a/browser/chrome/chrome.go b/browser/chrome/chrome.go index 9d1cd87..d1f4914 100644 --- a/browser/chrome/chrome.go +++ b/browser/chrome/chrome.go @@ -3,9 +3,9 @@ package chrome import ( "net/http" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/chrome" - "github.com/browserutils/kooky/internal/cookies" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/chrome" + "github.com/xiazemin/kooky/internal/cookies" ) func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { diff --git a/browser/chrome/chrome_test.go b/browser/chrome/chrome_test.go index e3f60db..2bb7b05 100644 --- a/browser/chrome/chrome_test.go +++ b/browser/chrome/chrome_test.go @@ -4,9 +4,9 @@ import ( "testing" "time" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/chrome" - "github.com/browserutils/kooky/internal/testutils" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/chrome" + "github.com/xiazemin/kooky/internal/testutils" ) // d18f6247db68045dfbab126d814baf2cf1512141391 diff --git a/browser/chrome/find.go b/browser/chrome/find.go index 7198fab..c18eb6c 100644 --- a/browser/chrome/find.go +++ b/browser/chrome/find.go @@ -1,10 +1,10 @@ package chrome import ( - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/chrome" - "github.com/browserutils/kooky/internal/chrome/find" - "github.com/browserutils/kooky/internal/cookies" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/chrome" + "github.com/xiazemin/kooky/internal/chrome/find" + "github.com/xiazemin/kooky/internal/cookies" ) type chromeFinder struct{} diff --git a/browser/chromium/chromium.go b/browser/chromium/chromium.go index 02b3696..ebabb61 100644 --- a/browser/chromium/chromium.go +++ b/browser/chromium/chromium.go @@ -3,9 +3,9 @@ package chromium import ( "net/http" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/chrome" - "github.com/browserutils/kooky/internal/cookies" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/chrome" + "github.com/xiazemin/kooky/internal/cookies" ) func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { diff --git a/browser/chromium/find.go b/browser/chromium/find.go index 0b069b3..5cb2339 100644 --- a/browser/chromium/find.go +++ b/browser/chromium/find.go @@ -1,10 +1,10 @@ package chromium import ( - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/chrome" - "github.com/browserutils/kooky/internal/chrome/find" - "github.com/browserutils/kooky/internal/cookies" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/chrome" + "github.com/xiazemin/kooky/internal/chrome/find" + "github.com/xiazemin/kooky/internal/cookies" ) type chromiumFinder struct{} diff --git a/browser/dillo/dillo.go b/browser/dillo/dillo.go index 774a5b0..5fc3269 100644 --- a/browser/dillo/dillo.go +++ b/browser/dillo/dillo.go @@ -3,9 +3,9 @@ package dillo import ( "net/http" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/netscape" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/netscape" ) func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { diff --git a/browser/dillo/dillo_test.go b/browser/dillo/dillo_test.go index ae46913..091b31e 100644 --- a/browser/dillo/dillo_test.go +++ b/browser/dillo/dillo_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/browserutils/kooky/internal/testutils" + "github.com/xiazemin/kooky/internal/testutils" ) func TestReadCookies(t *testing.T) { diff --git a/browser/dillo/find.go b/browser/dillo/find.go index ba591e2..fb3d0bf 100644 --- a/browser/dillo/find.go +++ b/browser/dillo/find.go @@ -6,9 +6,9 @@ import ( "os" "path/filepath" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/netscape" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/netscape" ) type dilloFinder struct{} diff --git a/browser/edge/edge.go b/browser/edge/edge.go index 8b85db8..2474422 100644 --- a/browser/edge/edge.go +++ b/browser/edge/edge.go @@ -8,10 +8,10 @@ import ( "net/http" "os" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/chrome" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/ie" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/chrome" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/ie" ) func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { diff --git a/browser/edge/find.go b/browser/edge/find.go index 9d8e10e..c4577a4 100644 --- a/browser/edge/find.go +++ b/browser/edge/find.go @@ -7,12 +7,12 @@ import ( "os" "path/filepath" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/chrome" - "github.com/browserutils/kooky/internal/chrome/find" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/ie" - _ "github.com/browserutils/kooky/internal/ie/find" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/chrome" + "github.com/xiazemin/kooky/internal/chrome/find" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/ie" + _ "github.com/xiazemin/kooky/internal/ie/find" ) // TODO !windows platforms diff --git a/browser/elinks/elinks.go b/browser/elinks/elinks.go index ff5eed9..0178ce9 100644 --- a/browser/elinks/elinks.go +++ b/browser/elinks/elinks.go @@ -8,8 +8,8 @@ import ( "strings" "time" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" ) type elinksCookieStore struct { diff --git a/browser/elinks/elinks_test.go b/browser/elinks/elinks_test.go index 5d9f200..c5ccba3 100644 --- a/browser/elinks/elinks_test.go +++ b/browser/elinks/elinks_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/browserutils/kooky/internal/testutils" + "github.com/xiazemin/kooky/internal/testutils" ) func TestReadCookies(t *testing.T) { diff --git a/browser/elinks/find.go b/browser/elinks/find.go index 92e340e..72b0be6 100644 --- a/browser/elinks/find.go +++ b/browser/elinks/find.go @@ -6,8 +6,8 @@ import ( "os" "path/filepath" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" ) type elinksFinder struct{} diff --git a/browser/epiphany/cookiestore.go b/browser/epiphany/cookiestore.go index 5226c87..e5f6d3a 100644 --- a/browser/epiphany/cookiestore.go +++ b/browser/epiphany/cookiestore.go @@ -3,8 +3,8 @@ package epiphany import ( "errors" - "github.com/browserutils/kooky/internal/cookies" "github.com/go-sqlite/sqlite3" + "github.com/xiazemin/kooky/internal/cookies" ) type epiphanyCookieStore struct { diff --git a/browser/epiphany/epiphany.go b/browser/epiphany/epiphany.go index 4921859..abc45d7 100644 --- a/browser/epiphany/epiphany.go +++ b/browser/epiphany/epiphany.go @@ -6,9 +6,9 @@ import ( "net/http" "time" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/utils" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/utils" ) func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { diff --git a/browser/epiphany/find.go b/browser/epiphany/find.go index cc5c8ff..0073858 100644 --- a/browser/epiphany/find.go +++ b/browser/epiphany/find.go @@ -6,8 +6,8 @@ import ( "os" "path/filepath" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" ) type epiphanyFinder struct{} diff --git a/browser/firefox/find.go b/browser/firefox/find.go index 9930585..11f0372 100644 --- a/browser/firefox/find.go +++ b/browser/firefox/find.go @@ -1,10 +1,10 @@ package firefox import ( - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/firefox" - "github.com/browserutils/kooky/internal/firefox/find" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/firefox" + "github.com/xiazemin/kooky/internal/firefox/find" ) type firefoxFinder struct{} diff --git a/browser/firefox/firefox.go b/browser/firefox/firefox.go index 6329625..67cb9e4 100644 --- a/browser/firefox/firefox.go +++ b/browser/firefox/firefox.go @@ -3,9 +3,9 @@ package firefox import ( "net/http" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/firefox" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/firefox" ) func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { diff --git a/browser/firefox/firefox_test.go b/browser/firefox/firefox_test.go index dcf6e67..2484031 100644 --- a/browser/firefox/firefox_test.go +++ b/browser/firefox/firefox_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/browserutils/kooky/internal/testutils" + "github.com/xiazemin/kooky/internal/testutils" ) func TestReadCookies(t *testing.T) { diff --git a/browser/ie/find.go b/browser/ie/find.go index 4cd7d3d..d6b6498 100644 --- a/browser/ie/find.go +++ b/browser/ie/find.go @@ -1,10 +1,10 @@ package ie import ( - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/ie" - _ "github.com/browserutils/kooky/internal/ie/find" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/ie" + _ "github.com/xiazemin/kooky/internal/ie/find" ) type ieFinder struct{} diff --git a/browser/ie/ie.go b/browser/ie/ie.go index d00a715..0f78b38 100644 --- a/browser/ie/ie.go +++ b/browser/ie/ie.go @@ -4,9 +4,9 @@ import ( "net/http" "os" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/ie" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/ie" ) func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { diff --git a/browser/ie/ie_test.go b/browser/ie/ie_test.go index 84717a4..17865d1 100644 --- a/browser/ie/ie_test.go +++ b/browser/ie/ie_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/browserutils/kooky/internal/testutils" + "github.com/xiazemin/kooky/internal/testutils" ) func TestReadCookies(t *testing.T) { diff --git a/browser/konqueror/find.go b/browser/konqueror/find.go index 6665af5..bcd7826 100644 --- a/browser/konqueror/find.go +++ b/browser/konqueror/find.go @@ -6,8 +6,8 @@ import ( "os" "path/filepath" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" ) type konquerorFinder struct{} diff --git a/browser/konqueror/konqueror.go b/browser/konqueror/konqueror.go index fa08f26..1758579 100644 --- a/browser/konqueror/konqueror.go +++ b/browser/konqueror/konqueror.go @@ -8,8 +8,8 @@ import ( "strings" "time" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" "golang.org/x/text/encoding/charmap" ) diff --git a/browser/konqueror/konqueror_test.go b/browser/konqueror/konqueror_test.go index b46f02e..3866900 100644 --- a/browser/konqueror/konqueror_test.go +++ b/browser/konqueror/konqueror_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/browserutils/kooky/internal/testutils" + "github.com/xiazemin/kooky/internal/testutils" ) func TestReadCookies(t *testing.T) { diff --git a/browser/lynx/find.go b/browser/lynx/find.go index 8dfbda6..ed98a69 100644 --- a/browser/lynx/find.go +++ b/browser/lynx/find.go @@ -11,9 +11,9 @@ import ( "path/filepath" "strings" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/netscape" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/netscape" ) type lynxFinder struct{} diff --git a/browser/lynx/lynx.go b/browser/lynx/lynx.go index 8bd5f7b..420e6ec 100644 --- a/browser/lynx/lynx.go +++ b/browser/lynx/lynx.go @@ -3,9 +3,9 @@ package lynx import ( "net/http" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/netscape" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/netscape" ) func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { diff --git a/browser/lynx/lynx_test.go b/browser/lynx/lynx_test.go index 9820e86..9abb35e 100644 --- a/browser/lynx/lynx_test.go +++ b/browser/lynx/lynx_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/browserutils/kooky/internal/testutils" + "github.com/xiazemin/kooky/internal/testutils" ) func TestReadCookies(t *testing.T) { diff --git a/browser/netscape/find.go b/browser/netscape/find.go index eab09f7..6589b9d 100644 --- a/browser/netscape/find.go +++ b/browser/netscape/find.go @@ -1,10 +1,10 @@ package netscape import ( - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/firefox/find" - "github.com/browserutils/kooky/internal/netscape" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/firefox/find" + "github.com/xiazemin/kooky/internal/netscape" ) type netscapeFinder struct{} diff --git a/browser/netscape/netscape.go b/browser/netscape/netscape.go index 8c6d445..279c5cd 100644 --- a/browser/netscape/netscape.go +++ b/browser/netscape/netscape.go @@ -3,9 +3,9 @@ package netscape import ( "net/http" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/netscape" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/netscape" ) // This ReadCookies() function returns an additional boolean "strict" telling diff --git a/browser/netscape/netscape_test.go b/browser/netscape/netscape_test.go index 73d65fd..45711c6 100644 --- a/browser/netscape/netscape_test.go +++ b/browser/netscape/netscape_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/browserutils/kooky/internal/testutils" + "github.com/xiazemin/kooky/internal/testutils" ) func TestReadCookies(t *testing.T) { diff --git a/browser/opera/cookies4dat.go b/browser/opera/cookies4dat.go index bb80064..beccddb 100644 --- a/browser/opera/cookies4dat.go +++ b/browser/opera/cookies4dat.go @@ -8,7 +8,7 @@ import ( "math/bits" "time" - "github.com/browserutils/kooky" + "github.com/xiazemin/kooky" ) type fileHeader struct { diff --git a/browser/opera/cookiestore.go b/browser/opera/cookiestore.go index 753eddd..ae4f4b0 100644 --- a/browser/opera/cookiestore.go +++ b/browser/opera/cookiestore.go @@ -5,7 +5,7 @@ import ( "io" "os" - "github.com/browserutils/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/cookies" ) type operaCookieStore struct { diff --git a/browser/opera/find.go b/browser/opera/find.go index 5e3d73d..43b2c4f 100644 --- a/browser/opera/find.go +++ b/browser/opera/find.go @@ -3,9 +3,9 @@ package opera import ( "path/filepath" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/chrome" - "github.com/browserutils/kooky/internal/cookies" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/chrome" + "github.com/xiazemin/kooky/internal/cookies" ) type operaFinder struct{} diff --git a/browser/opera/opera.go b/browser/opera/opera.go index 3fdd7db..1951de7 100644 --- a/browser/opera/opera.go +++ b/browser/opera/opera.go @@ -4,10 +4,10 @@ import ( "errors" "net/http" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/chrome" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/utils" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/chrome" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/utils" ) func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { diff --git a/browser/safari/find.go b/browser/safari/find.go index d1f33a4..56899d9 100644 --- a/browser/safari/find.go +++ b/browser/safari/find.go @@ -3,8 +3,8 @@ package safari import ( - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" ) type safariFinder struct{} diff --git a/browser/safari/safari.go b/browser/safari/safari.go index 192f425..6ce7c70 100644 --- a/browser/safari/safari.go +++ b/browser/safari/safari.go @@ -12,9 +12,9 @@ import ( "io" "net/http" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/timex" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/timex" ) type fileHeader struct { diff --git a/browser/safari/safari_test.go b/browser/safari/safari_test.go index 24df1ed..b17b513 100644 --- a/browser/safari/safari_test.go +++ b/browser/safari/safari_test.go @@ -4,8 +4,8 @@ import ( "testing" "time" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/testutils" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/testutils" ) // d18f6247db68045dfbab126d814baf2cf1512141391 diff --git a/browser/uzbl/find.go b/browser/uzbl/find.go index e24ef1c..19829e3 100644 --- a/browser/uzbl/find.go +++ b/browser/uzbl/find.go @@ -7,9 +7,9 @@ import ( "os" "path/filepath" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/netscape" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/netscape" ) type uzblFinder struct{} diff --git a/browser/uzbl/uzbl.go b/browser/uzbl/uzbl.go index f01d325..cecab3e 100644 --- a/browser/uzbl/uzbl.go +++ b/browser/uzbl/uzbl.go @@ -3,9 +3,9 @@ package uzbl import ( "net/http" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/netscape" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/netscape" ) func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { diff --git a/browser/uzbl/uzbl_test.go b/browser/uzbl/uzbl_test.go index 27e0abd..f2b9fd5 100644 --- a/browser/uzbl/uzbl_test.go +++ b/browser/uzbl/uzbl_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/browserutils/kooky/internal/testutils" + "github.com/xiazemin/kooky/internal/testutils" ) func TestReadCookies(t *testing.T) { diff --git a/browser/w3m/find.go b/browser/w3m/find.go index b8901d2..beb72a7 100644 --- a/browser/w3m/find.go +++ b/browser/w3m/find.go @@ -6,8 +6,8 @@ import ( "os" "path/filepath" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" ) type w3mFinder struct{} diff --git a/browser/w3m/w3m.go b/browser/w3m/w3m.go index 0d0e481..f49ee70 100644 --- a/browser/w3m/w3m.go +++ b/browser/w3m/w3m.go @@ -8,8 +8,8 @@ import ( "strings" "time" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" ) type w3mCookieStore struct { diff --git a/browser/w3m/w3m_test.go b/browser/w3m/w3m_test.go index df92ad3..34e9d02 100644 --- a/browser/w3m/w3m_test.go +++ b/browser/w3m/w3m_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/browserutils/kooky/internal/testutils" + "github.com/xiazemin/kooky/internal/testutils" ) func TestReadCookies(t *testing.T) { diff --git a/chrome_example_test.go b/chrome_example_test.go index a37940d..de01ddc 100644 --- a/chrome_example_test.go +++ b/chrome_example_test.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - "github.com/browserutils/kooky/browser/chrome" + "github.com/xiazemin/kooky/browser/chrome" ) // on macOS: diff --git a/cookiejar_example_test.go b/cookiejar_example_test.go index 64355cd..2260f8a 100644 --- a/cookiejar_example_test.go +++ b/cookiejar_example_test.go @@ -8,8 +8,8 @@ import ( "net/url" "strings" - "github.com/browserutils/kooky" - _ "github.com/browserutils/kooky/browser/firefox" + "github.com/xiazemin/kooky" + _ "github.com/xiazemin/kooky/browser/firefox" ) func Example_cookieJar() { diff --git a/cookiestores_example_test.go b/cookiestores_example_test.go index be3a106..61560b2 100644 --- a/cookiestores_example_test.go +++ b/cookiestores_example_test.go @@ -3,8 +3,8 @@ package kooky_test import ( "fmt" - "github.com/browserutils/kooky" - _ "github.com/browserutils/kooky/browser/all" // register cookiestore finders + "github.com/xiazemin/kooky" + _ "github.com/xiazemin/kooky/browser/all" // register cookiestore finders ) func ExampleFindAllCookieStores() { diff --git a/export_example_test.go b/export_example_test.go index 84e2ce9..5237af6 100644 --- a/export_example_test.go +++ b/export_example_test.go @@ -4,7 +4,7 @@ import ( "net/http" "os" - "github.com/browserutils/kooky" + "github.com/xiazemin/kooky" ) var cookieFile = `cookies.txt` diff --git a/filter_example_test.go b/filter_example_test.go index 0795495..2124ba1 100644 --- a/filter_example_test.go +++ b/filter_example_test.go @@ -5,7 +5,7 @@ import ( "net/http" "regexp" - "github.com/browserutils/kooky" + "github.com/xiazemin/kooky" ) // example regex matching base64 strings diff --git a/filtercookies_example_test.go b/filtercookies_example_test.go index 2b1a879..bf762c4 100644 --- a/filtercookies_example_test.go +++ b/filtercookies_example_test.go @@ -1,8 +1,8 @@ package kooky_test import ( - "github.com/browserutils/kooky" - _ "github.com/browserutils/kooky/browser/all" // register cookiestore finders + "github.com/xiazemin/kooky" + _ "github.com/xiazemin/kooky/browser/all" // register cookiestore finders ) var cookieName = `NID` diff --git a/find.go b/find.go index a89782b..49a9185 100644 --- a/find.go +++ b/find.go @@ -47,11 +47,11 @@ func RegisterFinder(browser string, finder CookieStoreFinder) { // // Register cookie store finders for all browsers like this: // -// import _ "github.com/browserutils/kooky/browser/all" +// import _ "github.com/xiazemin/kooky/browser/all" // // Or only a specific browser: // -// import _ "github.com/browserutils/kooky/browser/chrome" +// import _ "github.com/xiazemin/kooky/browser/chrome" func FindAllCookieStores() []CookieStore { var ret []CookieStore @@ -93,11 +93,11 @@ func FindAllCookieStores() []CookieStore { // // Register cookie store finders for all browsers like this: // -// import _ "github.com/browserutils/kooky/browser/all" +// import _ "github.com/xiazemin/kooky/browser/all" // // Or only a specific browser: // -// import _ "github.com/browserutils/kooky/browser/chrome" +// import _ "github.com/xiazemin/kooky/browser/chrome" func ReadCookies(filters ...Filter) []*Cookie { var ret []*Cookie diff --git a/go.mod b/go.mod index 5c71403..df22025 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/browserutils/kooky +module github.com/xiazemin/kooky go 1.18 @@ -10,7 +10,7 @@ require ( github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100 github.com/spf13/pflag v1.0.5 github.com/zalando/go-keyring v0.2.3 - github.com/zellyn/kooky v0.0.0-20230814063115-d4b42194bf0b + github.com/xiazemin/kooky v0.0.0-20230814063115-d4b42194bf0b golang.org/x/crypto v0.13.0 golang.org/x/net v0.15.0 golang.org/x/sys v0.12.0 @@ -21,8 +21,6 @@ require ( require ( github.com/Velocidex/json v0.0.0-20220224052537-92f3c0326e5a // indirect github.com/Velocidex/yaml/v2 v2.2.8 // indirect - github.com/alessio/shellescape v1.4.1 // indirect - github.com/danieljoos/wincred v1.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gonuts/binary v0.2.0 // indirect diff --git a/go.sum b/go.sum index c55a0d9..1e65efc 100644 --- a/go.sum +++ b/go.sum @@ -1,50 +1,36 @@ github.com/Velocidex/json v0.0.0-20220224052537-92f3c0326e5a h1:AeXPUzhU0yhID/v5JJEIkjaE85ASe+Vh4Kuv1RSLL+4= github.com/Velocidex/json v0.0.0-20220224052537-92f3c0326e5a/go.mod h1:ukJBuruT9b24pdgZwWDvOaCYHeS03B7oQPCUWh25bwM= -github.com/Velocidex/ordereddict v0.0.0-20191106020901-97c468e5e403/go.mod h1:pxJpvN5ISMtDwrdIdqnJ3ZrjIngCw+WT6gfNil6Zjvo= github.com/Velocidex/ordereddict v0.0.0-20220107075049-3dbe58412844/go.mod h1:Y5Tfx5SKGOzkulpqfonrdILSPIuNg+GqKE/DhVJgnpg= -github.com/Velocidex/ordereddict v0.0.0-20220411103415-79032cf99b1d h1:XnifmnLRjinjYzZgbog7LQr2XbmEFI81kuiufpn+JUQ= -github.com/Velocidex/ordereddict v0.0.0-20220411103415-79032cf99b1d/go.mod h1:XJDUbaGh2U9e0z78L5O2OXf1hE1wSxnJ7nSlQmA+bIs= github.com/Velocidex/ordereddict v0.0.0-20230909174157-2aa49cc5d11d h1:fn372EqKyazBxYUP5HPpBi3jId4MXuppEypEALGfvEk= github.com/Velocidex/ordereddict v0.0.0-20230909174157-2aa49cc5d11d/go.mod h1:+MqO5UMBemyFSm+yRXslbpFTwPUDhFHUf7HPV92twg4= github.com/Velocidex/yaml/v2 v2.2.8 h1:GUrSy4SBJ6RjGt43k6MeBKtw2z/27gh4A3hfFmFY3No= github.com/Velocidex/yaml/v2 v2.2.8/go.mod h1:PlXIg/Pxmoja48C1vMHo7C5pauAZvLq/UEPOQ3DsjS4= github.com/alecthomas/assert v1.0.0/go.mod h1:va/d2JC+M7F6s+80kl/R3G7FUiW6JzUO+hPhLyJ36ZY= github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= -github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= -github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48= github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.1.1 h1:87P60cSmareLAxMc4Hro0r2RBY4ROm0dYwkJNpS4pPs= github.com/alecthomas/repr v0.1.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= -github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= -github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/bobesa/go-domain-util v0.0.0-20190911083921-4033b5f7dd89 h1:2pkAuIM8OF1fy4ToFpMnI4oE+VeUNRbGrpSLKshK0oQ= github.com/bobesa/go-domain-util v0.0.0-20190911083921-4033b5f7dd89/go.mod h1:/09nEjna1UMoasyyQDhOrIn8hi2v2kiJglPWed1idck= -github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= -github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= -github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-ini/ini v1.66.4 h1:dKjMqkcbkzfddhIhyglTPgMoJnkvmG+bSLrU9cTHc5M= -github.com/go-ini/ini v1.66.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-sqlite/sqlite3 v0.0.0-20180313105335-53dd8e640ee7 h1:ow5vK9Q/DSKkxbEIJHBST6g+buBDwdaDIyk1dGGwpQo= github.com/go-sqlite/sqlite3 v0.0.0-20180313105335-53dd8e640ee7/go.mod h1:JxSQ+SvsjFb+p8Y+bn+GhTkiMfKVGBD0fq43ms2xw04= -github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gonuts/binary v0.2.0 h1:caITwMWAoQWlL0RNvv2lTU/AHqAJlVuu6nZmNgfbKW4= github.com/gonuts/binary v0.2.0/go.mod h1:kM+CtBrCGDSKdv8WXTuCUsw+loiy8f/QEI8YCCC0M/E= -github.com/keybase/go-keychain v0.0.0-20220408132150-ad3b4a8fd4a7 h1:ttxQhWhqiYEOVLMhmhIRQnZDLmYaBJVP7goucV3FJxM= -github.com/keybase/go-keychain v0.0.0-20220408132150-ad3b4a8fd4a7/go.mod h1:enrU/ug069Om7vWxuFE6nikLI2BZNwevMiGSo43Kt5w= github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100 h1:rG3VnJUnAWyiv7qYmmdOdSapzz6HM+zb9/uRFr0T5EM= github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100/go.mod h1:qDHUvIjGZJUtdPtuP4WMu5/U4aVWbFw1MhlkJqCGmCQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -59,61 +45,36 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/zalando/go-keyring v0.2.1 h1:MBRN/Z8H4U5wEKXiD67YbDAr5cj/DOStmSga70/2qKc= -github.com/zalando/go-keyring v0.2.1/go.mod h1:g63M2PPn0w5vjmEbwAX3ib5I+41zdm4esSETOn9Y6Dw= github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= -github.com/zellyn/kooky v0.0.0-20230814063115-d4b42194bf0b h1:PC1NpdF3TGAXnj+aIBMowu+J82GFKldtsQ8zA3j7RN8= -github.com/zellyn/kooky v0.0.0-20230814063115-d4b42194bf0b/go.mod h1:6JjIozsIKDJ2H0S0ePLIoVbhhBvI15a/oFlMYsfIsSA= -golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM= -golang.org/x/crypto v0.0.0-20220408190544-5352b0902921/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +github.com/xiazemin/kooky v0.0.0-20230814063115-d4b42194bf0b h1:PC1NpdF3TGAXnj+aIBMowu+J82GFKldtsQ8zA3j7RN8= +github.com/xiazemin/kooky v0.0.0-20230814063115-d4b42194bf0b/go.mod h1:6JjIozsIKDJ2H0S0ePLIoVbhhBvI15a/oFlMYsfIsSA= golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c= -golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw= -golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -www.velocidex.com/golang/go-ese v0.1.0 h1:LObdPh6uoAAbz50MqF4WdJHAn9+Fcr/9kyW0fMsjxlc= -www.velocidex.com/golang/go-ese v0.1.0/go.mod h1:d3PHzQhyhe+AO9RYBnDKZ40As15T+38zr++Dnv4ufuc= www.velocidex.com/golang/go-ese v0.2.0 h1:8/hzEMupfqEF0oMi1/EzsMN1xLN0GBFcB3GqxqRnb9s= www.velocidex.com/golang/go-ese v0.2.0/go.mod h1:6fC9T6UGLbM7icuA0ugomU5HbFC5XA5I30zlWtZT8YE= diff --git a/internal/chrome/chrome.go b/internal/chrome/chrome.go index d4835aa..02bf577 100644 --- a/internal/chrome/chrome.go +++ b/internal/chrome/chrome.go @@ -12,9 +12,9 @@ import ( "golang.org/x/crypto/pbkdf2" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/timex" - "github.com/browserutils/kooky/internal/utils" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/timex" + "github.com/xiazemin/kooky/internal/utils" ) // Thanks to https://gist.github.com/dacort/bd6a5116224c594b14db diff --git a/internal/chrome/cookiestore.go b/internal/chrome/cookiestore.go index d49ae66..83a7055 100644 --- a/internal/chrome/cookiestore.go +++ b/internal/chrome/cookiestore.go @@ -5,7 +5,7 @@ import ( "github.com/go-sqlite/sqlite3" - "github.com/browserutils/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/cookies" ) type CookieStore struct { diff --git a/internal/cookies/cookiejar.go b/internal/cookies/cookiejar.go index dbdda65..d1fc4b6 100644 --- a/internal/cookies/cookiejar.go +++ b/internal/cookies/cookiejar.go @@ -9,7 +9,7 @@ import ( "golang.org/x/net/publicsuffix" - "github.com/browserutils/kooky" + "github.com/xiazemin/kooky" ) var ( diff --git a/internal/cookies/cookiestore.go b/internal/cookies/cookiestore.go index 1648b67..c71c8e5 100644 --- a/internal/cookies/cookiestore.go +++ b/internal/cookies/cookiestore.go @@ -5,7 +5,7 @@ import ( "io" "os" - "github.com/browserutils/kooky" + "github.com/xiazemin/kooky" ) // kooky.CookieStore without http.CookieJar and SubJar() diff --git a/internal/firefox/cookiestore.go b/internal/firefox/cookiestore.go index a7490d0..1a228fc 100644 --- a/internal/firefox/cookiestore.go +++ b/internal/firefox/cookiestore.go @@ -5,8 +5,8 @@ import ( "os" "path/filepath" - "github.com/browserutils/kooky/internal/cookies" "github.com/go-sqlite/sqlite3" + "github.com/xiazemin/kooky/internal/cookies" ) type CookieStore struct { diff --git a/internal/firefox/firefox.go b/internal/firefox/firefox.go index 57634cc..6762ec6 100644 --- a/internal/firefox/firefox.go +++ b/internal/firefox/firefox.go @@ -7,8 +7,8 @@ import ( "strings" "time" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/utils" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/utils" "github.com/bobesa/go-domain-util/domainutil" ) diff --git a/internal/ie/cookiestore.go b/internal/ie/cookiestore.go index 3faa0d4..44586ef 100644 --- a/internal/ie/cookiestore.go +++ b/internal/ie/cookiestore.go @@ -5,9 +5,9 @@ import ( "io" "os" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/utils" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/utils" "www.velocidex.com/golang/go-ese/parser" ) diff --git a/internal/ie/ese.go b/internal/ie/ese.go index 51a8bf2..180486c 100644 --- a/internal/ie/ese.go +++ b/internal/ie/ese.go @@ -6,8 +6,8 @@ import ( "errors" "strings" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/timex" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/timex" "github.com/Velocidex/ordereddict" "www.velocidex.com/golang/go-ese/parser" diff --git a/internal/ie/find/find_windows.go b/internal/ie/find/find_windows.go index 7ef9124..cec7a38 100644 --- a/internal/ie/find/find_windows.go +++ b/internal/ie/find/find_windows.go @@ -7,9 +7,9 @@ import ( "path/filepath" "sync" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/ie" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/ie" ) type finder struct { diff --git a/internal/ie/ie.go b/internal/ie/ie.go index 1b724c0..6894c5d 100644 --- a/internal/ie/ie.go +++ b/internal/ie/ie.go @@ -3,7 +3,7 @@ package ie import ( "errors" - "github.com/browserutils/kooky" + "github.com/xiazemin/kooky" ) func (s *CookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cookie, error) { diff --git a/internal/ie/iecache.go b/internal/ie/iecache.go index 4322e9d..a1a68b8 100644 --- a/internal/ie/iecache.go +++ b/internal/ie/iecache.go @@ -9,10 +9,10 @@ import ( "strings" "time" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/bytesx" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/timex" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/bytesx" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/timex" ) // index.dat parser diff --git a/internal/ie/textcookies.go b/internal/ie/textcookies.go index b3c9b2e..4718222 100644 --- a/internal/ie/textcookies.go +++ b/internal/ie/textcookies.go @@ -6,9 +6,9 @@ import ( "strconv" "strings" - "github.com/browserutils/kooky" - "github.com/browserutils/kooky/internal/cookies" - "github.com/browserutils/kooky/internal/timex" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/timex" ) type TextCookieStore struct { diff --git a/internal/netscape/cookiestore.go b/internal/netscape/cookiestore.go index ca3957b..a653eeb 100644 --- a/internal/netscape/cookiestore.go +++ b/internal/netscape/cookiestore.go @@ -1,7 +1,7 @@ package netscape import ( - "github.com/browserutils/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/cookies" ) type CookieStore struct { diff --git a/internal/netscape/netscape.go b/internal/netscape/netscape.go index 434a96b..1cb05b4 100644 --- a/internal/netscape/netscape.go +++ b/internal/netscape/netscape.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/browserutils/kooky" + "github.com/xiazemin/kooky" ) const httpOnlyPrefix = `#HttpOnly_` From 615b14ce60ccc0dcb145bfde778b7671ed14e832 Mon Sep 17 00:00:00 2001 From: xiazemin <465474307@qq.com> Date: Wed, 11 Oct 2023 00:38:53 +0800 Subject: [PATCH 2/3] 1 --- cmd/kooky/kooky.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/kooky/kooky.go b/cmd/kooky/kooky.go index 04bbb8f..93b211b 100644 --- a/cmd/kooky/kooky.go +++ b/cmd/kooky/kooky.go @@ -8,8 +8,8 @@ import ( "strings" "text/tabwriter" - "github.com/zellyn/kooky" - _ "github.com/zellyn/kooky/browser/all" + "github.com/xiazemin/kooky" + _ "github.com/xiazemin/kooky/browser/all" "github.com/spf13/pflag" ) From df61cbb063d22d53f136fe3954e81323e76d0ca0 Mon Sep 17 00:00:00 2001 From: xiazemin Date: Tue, 29 Oct 2024 20:36:24 +0800 Subject: [PATCH 3/3] 1 --- Makefile | 27 +++ NOTES.md | 10 + README.md | 45 ++-- auto_example_test.go | 2 +- browser/browsh/browsh.go | 29 +-- browser/browsh/find.go | 24 ++- browser/chrome/chrome.go | 29 +-- browser/chrome/chrome_test.go | 7 +- browser/chrome/find.go | 29 +-- browser/chromium/chromium.go | 29 +-- browser/chromium/find.go | 30 +-- browser/dillo/dillo.go | 29 +-- browser/dillo/dillo_test.go | 3 +- browser/dillo/find.go | 31 +-- browser/edge/edge.go | 27 +-- browser/edge/find.go | 109 ++++------ browser/edge/find_darwin.go | 20 ++ browser/edge/find_linux.go | 19 ++ browser/edge/find_mobile.go | 7 + browser/edge/find_others.go | 9 + browser/edge/find_windows.go | 51 +++++ browser/elinks/elinks.go | 86 ++++---- browser/elinks/elinks_test.go | 3 +- browser/elinks/find.go | 24 ++- browser/epiphany/cookiestore.go | 7 +- browser/epiphany/epiphany.go | 176 +++++++--------- browser/epiphany/find.go | 32 +-- browser/firefox/find.go | 29 ++- browser/firefox/firefox.go | 29 +-- browser/firefox/firefox_test.go | 8 +- browser/ie/find.go | 28 +-- browser/ie/find_others.go | 4 +- browser/ie/find_windows.go | 15 +- browser/ie/ie.go | 27 +-- browser/ie/ie_test.go | 3 +- browser/konqueror/find.go | 67 +++--- browser/konqueror/konqueror.go | 247 +++++++++++----------- browser/konqueror/konqueror_test.go | 3 +- browser/lynx/find.go | 174 ++++++++-------- browser/lynx/lynx.go | 29 +-- browser/lynx/lynx_test.go | 14 +- browser/netscape/find.go | 30 +-- browser/netscape/find_others.go | 7 +- browser/netscape/find_unix.go | 12 +- browser/netscape/netscape.go | 50 ++--- browser/netscape/netscape_test.go | 6 +- browser/opera/cookies4dat.go | 121 ++++++----- browser/opera/cookiestore.go | 4 +- browser/opera/find.go | 52 ++--- browser/opera/find_darwin.go | 21 +- browser/opera/find_unix.go | 22 +- browser/opera/find_windows.go | 33 ++- browser/opera/opera.go | 37 +--- browser/safari/find.go | 38 ++-- browser/safari/find_darwin.go | 12 +- browser/safari/find_windows.go | 6 +- browser/safari/safari.go | 150 +++++++------- browser/safari/safari_test.go | 6 +- browser/uzbl/find.go | 89 ++++---- browser/uzbl/uzbl.go | 29 +-- browser/uzbl/uzbl_test.go | 11 +- browser/w3m/find.go | 24 ++- browser/w3m/w3m.go | 124 +++++------ browser/w3m/w3m_test.go | 3 +- chrome_example_test.go | 8 +- cmd/kooky/kooky.go | 163 +++++++++------ cookiejar_example_test.go | 17 +- cookiestores_example_test.go | 7 +- export.go | 89 +++++--- export_example_test.go | 3 +- filter.go | 156 +++++++++++--- filter_example_test.go | 5 +- filtercookies_example_test.go | 23 ++- find.go | 249 ++++++++++++++-------- firstmatch_nid_test.go | 27 +++ go.mod | 16 +- go.sum | 33 ++- internal/chrome/chrome.go | 131 +++++++----- internal/chrome/chrome_darwin.go | 7 +- internal/chrome/chrome_darwin_cgo.go | 4 +- internal/chrome/chrome_notwindows.go | 3 +- internal/chrome/chrome_windows.go | 4 +- internal/chrome/cookiestore.go | 8 +- internal/chrome/derivatives.go | 53 +++++ internal/chrome/find/find.go | 145 +++++++------ internal/chrome/find/find_android.go | 21 +- internal/chrome/find/find_darwin.go | 24 ++- internal/chrome/find/find_others.go | 10 +- internal/chrome/find/find_unix.go | 30 ++- internal/chrome/find/find_windows.go | 30 +-- internal/cookies/cookiejar.go | 64 ++++-- internal/cookies/cookiestore.go | 41 +++- internal/firefox/cookiestore.go | 9 +- internal/firefox/find/find.go | 90 ++++---- internal/firefox/find/find_darwin.go | 9 +- internal/firefox/find/find_others.go | 4 +- internal/firefox/find/find_unix.go | 16 +- internal/firefox/find/find_windows.go | 12 +- internal/firefox/firefox.go | 168 +++++++-------- internal/ie/cookiestore.go | 16 +- internal/ie/ese.go | 207 ++++++++++--------- internal/ie/find/find_windows.go | 115 ++++++----- internal/ie/ie.go | 9 +- internal/ie/iecache.go | 74 +++---- internal/ie/textcookies.go | 176 +++++++++------- internal/iterx/iter.go | 78 +++++++ internal/netscape/cookiestore.go | 10 +- internal/netscape/netscape.go | 105 +++++++--- internal/utils/magiclite.go | 2 +- internal/utils/open.go | 5 + internal/utils/open_others.go | 7 + internal/utils/open_windows.go | 112 ++++++++++ internal/utils/tablerow.go | 71 ++++--- kooky.go | 286 +++++++++++++++++++++++++- 114 files changed, 3199 insertions(+), 2211 deletions(-) create mode 100644 Makefile create mode 100644 NOTES.md create mode 100644 browser/edge/find_darwin.go create mode 100644 browser/edge/find_linux.go create mode 100644 browser/edge/find_mobile.go create mode 100644 browser/edge/find_others.go create mode 100644 browser/edge/find_windows.go create mode 100644 firstmatch_nid_test.go create mode 100644 internal/chrome/derivatives.go create mode 100644 internal/iterx/iter.go create mode 100644 internal/utils/open.go create mode 100644 internal/utils/open_others.go create mode 100644 internal/utils/open_windows.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..66a124f --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +# unix only atm + +ifeq ($(GOOS),windows) +EXT = ".exe" +else +SRC=$(shell find . -type d \( -path ./vendor -o -path ./testdata \) -prune -o -name '*.go' -print) +endif + + +.PHONY: build +build: kooky + +.PHONY: all +all: kooky + + +kooky: ${SRC} + @env GOWORK=off GOEXPERIMENT=rangefunc go build -trimpath -ldflags '-w -s' -o kooky${EXT} ./cmd/kooky + + +.PHONY: test +test: + @env GOWORK=off GOEXPERIMENT=rangefunc go test -count=1 -timeout=30s ./... | grep -v '^? ' + +.PHONY: clean +clean: + @rm -f -- kooky kooky.exe kooky.test diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..84ce252 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,10 @@ +### TODO + +- [ ] Set up CI +- [x] Make it work on Windows. (Look at + [this](https://play.golang.org/p/fknP9AuLU-) and + [this](https://github.com/cfstras/chromecsv/blob/master/crypt_windows.go) + to learn how to decrypt.) +- [x] Handle rows in Chrome's cookie DB with other than 14 columns (?) +- [ ] Figure out what to do with quoted values, like the "bcookie" cookie from slideshare.net + (related (?): [go issue #46443: net/http: add field Cookie.Quoted bool](https://github.com/golang/go/issues/46443)) diff --git a/README.md b/README.md index 6292c11..cbd1ce0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # kooky [![PkgGoDev](https://pkg.go.dev/badge/github.com/xiazemin/kooky)](https://pkg.go.dev/github.com/xiazemin/kooky) -[![Go Report Card](https://goreportcard.com/badge/zellyn/kooky)](https://goreportcard.com/report/zellyn/kooky) -![Lines of code](https://img.shields.io/tokei/lines/github/zellyn/kooky) +[![Go Report Card](https://goreportcard.com/badge/browserutils/kooky)](https://goreportcard.com/report/browserutils/kooky) +![Lines of code](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.codetabs.com%2Fv1%2Floc%2F%3Fgithub%3Dbrowserutils%2Fkooky%26ignored%3Dvendor%2Ctestdata&query=%24%5B%3F(%40.language%3D%3D%22Go%22)%5D.linesOfCode&logo=Go&label=lines%20of%20code&cacheSeconds=3600) [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/) @@ -30,15 +30,6 @@ Some functions might not yet be implemented on some platforms. PRs more than welcome. -## TODOs - -- [ ] Set up CI -- [x] Make it work on Windows. (Look at - [this](https://play.golang.org/p/fknP9AuLU-) and - [this](https://github.com/cfstras/chromecsv/blob/master/crypt_windows.go) - to learn how to decrypt.) -- [x] Handle rows in Chrome's cookie DB with other than 14 columns (?) - ## Example usage ### Any Browser - Cookie Filter Usage @@ -56,9 +47,9 @@ import ( func main() { // uses registered finders to find cookie store files in default locations // applies the passed filters "Valid", "DomainHasSuffix()" and "Name()" in order to the cookies - cookies := kooky.ReadCookies(kooky.Valid, kooky.DomainHasSuffix(`google.com`), kooky.Name(`NID`)) + cookiesSeq := kooky.TraverseCookies(context.TODO(), kooky.Valid, kooky.DomainHasSuffix(`google.com`), kooky.Name(`NID`)).OnlyCookies() - for _, cookie := range cookies { + for cookie := range cookiesSeq { fmt.Println(cookie.Domain, cookie.Name, cookie.Value) } } @@ -80,11 +71,8 @@ import ( func main() { dir, _ := os.UserConfigDir() // "//Library/Application Support/" cookiesFile := dir + "/Google/Chrome/Default/Cookies" - cookies, err := chrome.ReadCookies(cookiesFile) - if err != nil { - log.Fatal(err) - } - for _, cookie := range cookies { + cookiesSeq := chrome.TraverseCookies(cookiesFile).OnlyCookies() + for cookie := range cookiesSeq { fmt.Println(cookie) } } @@ -105,25 +93,22 @@ import ( func main() { dir, _ := os.UserHomeDir() - cookiesFile := dir + "/Library/Cookies/Cookies.binarycookies" - cookies, err := safari.ReadCookies(cookiesFile) - if err != nil { - log.Fatal(err) - } - for _, cookie := range cookies { + cookiesFile := dir + "/Library/Containers/com.apple.Safari/Data/Library/Cookies/Cookies.binarycookies" + cookiesSeq := safari.TraverseCookies(cookiesFile).OnlyCookies() + for cookie := range cookiesSeq { fmt.Println(cookie) } } ``` ## Thanks/references -- Thanks to [@dacort](http://github.com/dacort) for MacOS cookie decrypting +- Thanks to [@dacort](https://github.com/dacort) for MacOS cookie decrypting code at https://gist.github.com/dacort/bd6a5116224c594b14db. -- Thanks to [@as0ler](http://github.com/as0ler) - (and originally [@satishb3](http://github.com/satishb3) I believe) for +- Thanks to [@as0ler](https://github.com/as0ler) + (and originally [@satishb3](https://github.com/satishb3) I believe) for Safari cookie-reading Python code at https://github.com/as0ler/BinaryCookieReader. - Thanks to all the people who have contributed functionality and fixes: - - [@srlehn](http://github.com/srlehn) - many fixes, Linux support for Chrome, added about a dozen browsers! - - [@zippoxer](http://github.com/zippoxer) - Windows support for Chrome - - [@adamdecaf](http://github.com/adamdecaf) - Firefox support + - [@srlehn](https://github.com/srlehn) - many fixes, Linux support for Chrome, added about a dozen browsers! + - [@zippoxer](https://github.com/zippoxer) - Windows support for Chrome + - [@adamdecaf](https://github.com/adamdecaf) - Firefox support - [@barnardb](https://github.com/barnardb) - better row abstraction, fixing column length errors diff --git a/auto_example_test.go b/auto_example_test.go index d98f82e..8761fce 100644 --- a/auto_example_test.go +++ b/auto_example_test.go @@ -12,7 +12,7 @@ func ExampleReadCookies_all() { // try to find cookie stores in default locations and // read the cookies from them. // decryption is handled automatically. - cookies := kooky.ReadCookies() + cookies := kooky.AllCookies() for _, cookie := range cookies { fmt.Println(cookie) diff --git a/browser/browsh/browsh.go b/browser/browsh/browsh.go index 9fa352e..4169534 100644 --- a/browser/browsh/browsh.go +++ b/browser/browsh/browsh.go @@ -2,36 +2,19 @@ package browsh import ( - "net/http" + "context" "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/cookies" "github.com/xiazemin/kooky/internal/firefox" ) -func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { - s, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer s.Close() - - return s.ReadCookies(filters...) +func ReadCookies(ctx context.Context, filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { + return cookies.SingleRead(cookieStore, filename, filters...).ReadAllCookies(ctx) } -// CookieJar returns an initiated http.CookieJar based on the cookies stored by -// the browsh browser. Set cookies are memory stored and do not modify any -// browser files. -func CookieJar(filename string, filters ...kooky.Filter) (http.CookieJar, error) { - j, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer j.Close() - if err := j.InitJar(); err != nil { - return nil, err - } - return j, nil +func TraverseCookies(filename string, filters ...kooky.Filter) kooky.CookieSeq { + return cookies.SingleRead(cookieStore, filename, filters...) } // CookieStore has to be closed with CookieStore.Close() after use. @@ -44,5 +27,5 @@ func cookieStore(filename string, filters ...kooky.Filter) (*cookies.CookieJar, s.FileNameStr = filename s.BrowserStr = `browsh` - return &cookies.CookieJar{CookieStore: s}, nil + return cookies.NewCookieJar(s, filters...), nil } diff --git a/browser/browsh/find.go b/browser/browsh/find.go index bfbf488..0cf1f46 100644 --- a/browser/browsh/find.go +++ b/browser/browsh/find.go @@ -19,14 +19,15 @@ func init() { kooky.RegisterFinder(`browsh`, &browshFinder{}) } -func (f *browshFinder) FindCookieStores() ([]kooky.CookieStore, error) { - dotConfig, err := os.UserConfigDir() - if err != nil { - return nil, err - } - - var ret = []kooky.CookieStore{ - &cookies.CookieJar{ +func (f *browshFinder) FindCookieStores() kooky.CookieStoreSeq { + return func(yield func(kooky.CookieStore, error) bool) { + dotConfig, err := os.UserConfigDir() + if err != nil { + _ = yield(nil, err) + return + } + + st := &cookies.CookieJar{ CookieStore: &firefox.CookieStore{ DefaultCookieStore: cookies.DefaultCookieStore{ BrowserStr: `browsh`, @@ -34,8 +35,9 @@ func (f *browshFinder) FindCookieStores() ([]kooky.CookieStore, error) { FileNameStr: filepath.Join(dotConfig, `browsh`, `firefox_profile`, `cookies.sqlite`), }, }, - }, + } + if !yield(st, nil) { + return + } } - - return ret, nil } diff --git a/browser/chrome/chrome.go b/browser/chrome/chrome.go index d1f4914..4d4d3ad 100644 --- a/browser/chrome/chrome.go +++ b/browser/chrome/chrome.go @@ -1,36 +1,19 @@ package chrome import ( - "net/http" + "context" "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/chrome" "github.com/xiazemin/kooky/internal/cookies" ) -func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { - s, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer s.Close() - - return s.ReadCookies(filters...) +func ReadCookies(ctx context.Context, filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { + return cookies.SingleRead(cookieStore, filename, filters...).ReadAllCookies(ctx) } -// CookieJar returns an initiated http.CookieJar based on the cookies stored by -// the Chrome browser. Set cookies are memory stored and do not modify any -// browser files. -func CookieJar(filename string, filters ...kooky.Filter) (http.CookieJar, error) { - j, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer j.Close() - if err := j.InitJar(); err != nil { - return nil, err - } - return j, nil +func TraverseCookies(filename string, filters ...kooky.Filter) kooky.CookieSeq { + return cookies.SingleRead(cookieStore, filename, filters...) } // CookieStore has to be closed with CookieStore.Close() after use. @@ -43,5 +26,5 @@ func cookieStore(filename string, filters ...kooky.Filter) (*cookies.CookieJar, s.FileNameStr = filename s.BrowserStr = `chrome` - return &cookies.CookieJar{CookieStore: s}, nil + return cookies.NewCookieJar(s, filters...), nil } diff --git a/browser/chrome/chrome_test.go b/browser/chrome/chrome_test.go index 2bb7b05..6dcff2f 100644 --- a/browser/chrome/chrome_test.go +++ b/browser/chrome/chrome_test.go @@ -1,6 +1,7 @@ package chrome import ( + "context" "testing" "time" @@ -9,7 +10,6 @@ import ( "github.com/xiazemin/kooky/internal/testutils" ) -// d18f6247db68045dfbab126d814baf2cf1512141391 func TestReadCookies(t *testing.T) { testCookiesPath, err := testutils.GetTestDataFilePath("chrome-macos-cookie-db.sqlite") // this test file was created on macos if err != nil { @@ -24,7 +24,7 @@ func TestReadCookies(t *testing.T) { oldPassword := s.SetKeyringPassword([]byte("ChromeSafeStoragePasswrd")) defer s.SetKeyringPassword(oldPassword) - cookies, err := s.ReadCookies() + cookies, err := s.TraverseCookies().ReadAllCookies(context.Background()) if err != nil { t.Fatal(err) } @@ -32,7 +32,8 @@ func TestReadCookies(t *testing.T) { domain := "news.ycombinator.com" name := "user" - cookies = kooky.FilterCookies(cookies, kooky.Domain(domain), kooky.Name(name)) + ctx := context.Background() + cookies = kooky.FilterCookies(ctx, cookies, kooky.Domain(domain), kooky.Name(name)).Collect(ctx) if len(cookies) == 0 { t.Fatalf("Found no cookies with domain=%q, name=%q", domain, name) } diff --git a/browser/chrome/find.go b/browser/chrome/find.go index c18eb6c..42073cd 100644 --- a/browser/chrome/find.go +++ b/browser/chrome/find.go @@ -15,17 +15,16 @@ func init() { kooky.RegisterFinder(`chrome`, &chromeFinder{}) } -func (f *chromeFinder) FindCookieStores() ([]kooky.CookieStore, error) { - files, err := find.FindChromeCookieStoreFiles() - if err != nil { - return nil, err - } - - var ret []kooky.CookieStore - for _, file := range files { - ret = append( - ret, - &cookies.CookieJar{ +func (f *chromeFinder) FindCookieStores() kooky.CookieStoreSeq { + return func(yield func(kooky.CookieStore, error) bool) { + for file, err := range find.FindChromeCookieStoreFiles() { + if err != nil { + if !yield(nil, err) { + return + } + continue + } + st := &cookies.CookieJar{ CookieStore: &chrome.CookieStore{ DefaultCookieStore: cookies.DefaultCookieStore{ BrowserStr: file.Browser, @@ -35,8 +34,10 @@ func (f *chromeFinder) FindCookieStores() ([]kooky.CookieStore, error) { FileNameStr: file.Path, }, }, - }, - ) + } + if !yield(st, nil) { + return + } + } } - return ret, nil } diff --git a/browser/chromium/chromium.go b/browser/chromium/chromium.go index ebabb61..37593c9 100644 --- a/browser/chromium/chromium.go +++ b/browser/chromium/chromium.go @@ -1,36 +1,19 @@ package chromium import ( - "net/http" + "context" "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/chrome" "github.com/xiazemin/kooky/internal/cookies" ) -func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { - s, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer s.Close() - - return s.ReadCookies(filters...) +func ReadCookies(ctx context.Context, filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { + return cookies.SingleRead(cookieStore, filename, filters...).ReadAllCookies(ctx) } -// CookieJar returns an initiated http.CookieJar based on the cookies stored by -// the Chromium browser. Set cookies are memory stored and do not modify any -// browser files. -func CookieJar(filename string, filters ...kooky.Filter) (http.CookieJar, error) { - j, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer j.Close() - if err := j.InitJar(); err != nil { - return nil, err - } - return j, nil +func TraverseCookies(filename string, filters ...kooky.Filter) kooky.CookieSeq { + return cookies.SingleRead(cookieStore, filename, filters...) } // CookieStore has to be closed with CookieStore.Close() after use. @@ -43,5 +26,5 @@ func cookieStore(filename string, filters ...kooky.Filter) (*cookies.CookieJar, s.FileNameStr = filename s.BrowserStr = `chromium` - return &cookies.CookieJar{CookieStore: s}, nil + return cookies.NewCookieJar(s, filters...), nil } diff --git a/browser/chromium/find.go b/browser/chromium/find.go index 5cb2339..29dbe68 100644 --- a/browser/chromium/find.go +++ b/browser/chromium/find.go @@ -15,17 +15,16 @@ func init() { kooky.RegisterFinder(`chromium`, &chromiumFinder{}) } -func (f *chromiumFinder) FindCookieStores() ([]kooky.CookieStore, error) { - files, err := find.FindChromiumCookieStoreFiles() - if err != nil { - return nil, err - } - - var ret []kooky.CookieStore - for _, file := range files { - ret = append( - ret, - &cookies.CookieJar{ +func (f *chromiumFinder) FindCookieStores() kooky.CookieStoreSeq { + return func(yield func(kooky.CookieStore, error) bool) { + for file, err := range find.FindChromiumCookieStoreFiles() { + if err != nil { + if !yield(nil, err) { + return + } + continue + } + st := &cookies.CookieJar{ CookieStore: &chrome.CookieStore{ DefaultCookieStore: cookies.DefaultCookieStore{ BrowserStr: file.Browser, @@ -35,9 +34,10 @@ func (f *chromiumFinder) FindCookieStores() ([]kooky.CookieStore, error) { FileNameStr: file.Path, }, }, - }, - ) + } + if !yield(st, nil) { + return + } + } } - - return ret, nil } diff --git a/browser/dillo/dillo.go b/browser/dillo/dillo.go index 5fc3269..1d4df8e 100644 --- a/browser/dillo/dillo.go +++ b/browser/dillo/dillo.go @@ -1,36 +1,19 @@ package dillo import ( - "net/http" + "context" "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/cookies" "github.com/xiazemin/kooky/internal/netscape" ) -func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { - s, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer s.Close() - - return s.ReadCookies(filters...) +func ReadCookies(ctx context.Context, filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { + return cookies.SingleRead(cookieStore, filename, filters...).ReadAllCookies(ctx) } -// CookieJar returns an initiated http.CookieJar based on the cookies stored by -// the Dillo browser. Set cookies are memory stored and do not modify any -// browser files. -func CookieJar(filename string, filters ...kooky.Filter) (http.CookieJar, error) { - j, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer j.Close() - if err := j.InitJar(); err != nil { - return nil, err - } - return j, nil +func TraverseCookies(filename string, filters ...kooky.Filter) kooky.CookieSeq { + return cookies.SingleRead(cookieStore, filename, filters...) } // CookieStore has to be closed with CookieStore.Close() after use. @@ -43,5 +26,5 @@ func cookieStore(filename string, filters ...kooky.Filter) (*cookies.CookieJar, s.FileNameStr = filename s.BrowserStr = `dillo` - return &cookies.CookieJar{CookieStore: s}, nil + return cookies.NewCookieJar(s, filters...), nil } diff --git a/browser/dillo/dillo_test.go b/browser/dillo/dillo_test.go index 091b31e..bc536f5 100644 --- a/browser/dillo/dillo_test.go +++ b/browser/dillo/dillo_test.go @@ -1,6 +1,7 @@ package dillo import ( + "context" "testing" "time" @@ -13,7 +14,7 @@ func TestReadCookies(t *testing.T) { t.Fatalf("Failed to load test data file") } - cookies, err := ReadCookies(testCookiesPath) + cookies, err := TraverseCookies(testCookiesPath).ReadAllCookies(context.Background()) if err != nil { t.Fatal(err) } diff --git a/browser/dillo/find.go b/browser/dillo/find.go index fb3d0bf..16744ab 100644 --- a/browser/dillo/find.go +++ b/browser/dillo/find.go @@ -19,17 +19,18 @@ func init() { kooky.RegisterFinder(`dillo`, &dilloFinder{}) } -func (f *dilloFinder) FindCookieStores() ([]kooky.CookieStore, error) { - // https://www.dillo.org/FAQ.html#q16 - // https://www.dillo.org/Cookies.txt - - home, err := os.UserHomeDir() - if err != nil { - return nil, err - } - - var ret = []kooky.CookieStore{ - &cookies.CookieJar{ +func (f *dilloFinder) FindCookieStores() kooky.CookieStoreSeq { + return func(yield func(kooky.CookieStore, error) bool) { + // https://www.dillo.org/FAQ.html#q16 + // https://www.dillo.org/Cookies.txt + + home, err := os.UserHomeDir() + if err != nil { + _ = yield(nil, err) + return + } + + st := &cookies.CookieJar{ CookieStore: &netscape.CookieStore{ DefaultCookieStore: cookies.DefaultCookieStore{ BrowserStr: `dillo`, @@ -37,7 +38,9 @@ func (f *dilloFinder) FindCookieStores() ([]kooky.CookieStore, error) { FileNameStr: filepath.Join(home, `.dillo`, `cookies.txt`), }, }, - }} - - return ret, nil + } + if !yield(st, nil) { + return + } + } } diff --git a/browser/edge/edge.go b/browser/edge/edge.go index 2474422..e2dd289 100644 --- a/browser/edge/edge.go +++ b/browser/edge/edge.go @@ -5,7 +5,7 @@ package edge import ( - "net/http" + "context" "os" "github.com/xiazemin/kooky" @@ -14,29 +14,12 @@ import ( "github.com/xiazemin/kooky/internal/ie" ) -func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { - s, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer s.Close() - - return s.ReadCookies(filters...) +func ReadCookies(ctx context.Context, filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { + return cookies.SingleRead(cookieStore, filename, filters...).ReadAllCookies(ctx) } -// CookieJar returns an initiated http.CookieJar based on the cookies stored by -// the Edge browser. Set cookies are memory stored and do not modify any -// browser files. -func CookieJar(filename string, filters ...kooky.Filter) (http.CookieJar, error) { - j, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer j.Close() - if err := j.InitJar(); err != nil { - return nil, err - } - return j, nil +func TraverseCookies(filename string, filters ...kooky.Filter) kooky.CookieSeq { + return cookies.SingleRead(cookieStore, filename, filters...) } // CookieStore has to be closed with CookieStore.Close() after use. diff --git a/browser/edge/find.go b/browser/edge/find.go index c4577a4..23e2d0c 100644 --- a/browser/edge/find.go +++ b/browser/edge/find.go @@ -1,22 +1,14 @@ -//go:build windows - package edge import ( - "errors" - "os" - "path/filepath" + "runtime" "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/chrome" - "github.com/xiazemin/kooky/internal/chrome/find" + chromefind "github.com/xiazemin/kooky/internal/chrome/find" "github.com/xiazemin/kooky/internal/cookies" - "github.com/xiazemin/kooky/internal/ie" - _ "github.com/xiazemin/kooky/internal/ie/find" ) -// TODO !windows platforms - type edgeFinder struct{} var _ kooky.CookieStoreFinder = (*edgeFinder)(nil) @@ -25,64 +17,47 @@ func init() { kooky.RegisterFinder(`edge`, &edgeFinder{}) } -func (f *edgeFinder) FindCookieStores() ([]kooky.CookieStore, error) { - locApp := os.Getenv(`LocalAppData`) - if len(locApp) == 0 { - return nil, errors.New(`%LocalAppData% is empty`) - } - - var cookiesFiles []kooky.CookieStore - - // Blink based - newRoot := func() ([]string, error) { - return []string{filepath.Join(locApp, `Microsoft`, `Edge`, `User Data`)}, nil - } - blinkCookiesFiles, err := find.FindCookieStoreFiles(newRoot, `edge`) - if err != nil { - return nil, err - } - for _, cookiesFile := range blinkCookiesFiles { - cookiesFiles = append( - cookiesFiles, - &cookies.CookieJar{ - CookieStore: &ie.CookieStore{ - CookieStore: &chrome.CookieStore{ - DefaultCookieStore: cookies.DefaultCookieStore{ - BrowserStr: cookiesFile.Browser, - ProfileStr: cookiesFile.Profile, - OSStr: cookiesFile.OS, - IsDefaultProfileBool: cookiesFile.IsDefaultProfile, - FileNameStr: cookiesFile.Path, - }, - }, +func (f *edgeFinder) FindCookieStores() kooky.CookieStoreSeq { + return func(yield func(kooky.CookieStore, error) bool) { + for file, err := range chromefind.FindCookieStoreFiles(edgeChromiumRoots, `edge`) { + if err != nil { + if !yield(nil, err) { + return + } + } + if file == nil { + continue + } + cookieStore := &chrome.CookieStore{ + DefaultCookieStore: cookies.DefaultCookieStore{ + BrowserStr: file.Browser, + ProfileStr: file.Profile, + OSStr: file.OS, + IsDefaultProfileBool: file.IsDefaultProfile, + FileNameStr: file.Path, }, - }, - ) + } + cookieStore.SetSafeStorage(`Microsoft Edge`, ``) + if !yield(&cookies.CookieJar{CookieStore: cookieStore}, nil) { + return + } + } + + if runtime.GOOS != `windows` || edgeOldCookieStores == nil { + return + } + for oldCookieStore, err := range edgeOldCookieStores { // ESE, text cookies + if err != nil { + if !yield(nil, err) { + return + } + continue + } + if !yield(oldCookieStore, nil) { + return + } + } } - - return cookiesFiles, nil } -/* -https://www.nirsoft.net/utils/edge_cookies_view.html -starting from Fall Creators Update 1709 of Windows 10, the cookies of Microsoft Edge Web browser are stored in the WebCacheV01.dat database -ESE database at %USERPROFILE%\AppData\Local\Microsoft\Windows\WebCache\WebCacheV01.dat (%LocalAppData%\Microsoft\Windows\WebCache\WebCacheV01.dat) -CookieEntryEx_## - -https://www.linkedin.com/pulse/windows-10-microsoft-edge-browser-forensics-brent-muir -https://bsmuir.kinja.com/windows-10-microsoft-edge-browser-forensics-1733533818 - -locations: -%LocalAppData%\Microsoft\Windows\WebCache\WebCacheV01.dat -%LocalAppData%\Microsoft\Edge\User Data\Default - -https://www.foxtonforensics.com/browser-history-examiner/microsoft-edge-history-location -v79+: -Edge Cookies are stored in the 'Cookies' SQLite database, within the 'cookies' table. - -up to v44: -Edge Cookies are stored in the 'WebCacheV01.dat' ESE database, within the 'CookieEntryEx' containers. - -older: -Older versions of Edge stored cookies as separate text files in locations specified within the ESE database. -*/ +var edgeOldCookieStores kooky.CookieStoreSeq diff --git a/browser/edge/find_darwin.go b/browser/edge/find_darwin.go new file mode 100644 index 0000000..54b23b6 --- /dev/null +++ b/browser/edge/find_darwin.go @@ -0,0 +1,20 @@ +//go:build darwin && !ios + +package edge + +import ( + "os" + "path/filepath" +) + +func edgeChromiumRoots(yield func(string, error) bool) { + // "$HOME/Library/Application Support" + cfgDir, err := os.UserConfigDir() + if err != nil { + _ = yield(``, err) + return + } + if !yield(filepath.Join(cfgDir, `Microsoft Edge`), nil) { + return + } +} diff --git a/browser/edge/find_linux.go b/browser/edge/find_linux.go new file mode 100644 index 0000000..93ea6e1 --- /dev/null +++ b/browser/edge/find_linux.go @@ -0,0 +1,19 @@ +//go:build linux && !android + +package edge + +import ( + "os" + "path/filepath" +) + +func edgeChromiumRoots(yield func(string, error) bool) { + cfgDir, err := os.UserConfigDir() + if err != nil { + _ = yield(``, err) + return + } + if !yield(filepath.Join(cfgDir, `microsoft-edge`), nil) { + return + } +} diff --git a/browser/edge/find_mobile.go b/browser/edge/find_mobile.go new file mode 100644 index 0000000..616ae41 --- /dev/null +++ b/browser/edge/find_mobile.go @@ -0,0 +1,7 @@ +//go:build ios || android + +package edge + +import "errors" + +func edgeChromiumRoots(yield func(string, error) bool) { yield(``, errors.New(`not implemented`)) } diff --git a/browser/edge/find_others.go b/browser/edge/find_others.go new file mode 100644 index 0000000..c1ca9d7 --- /dev/null +++ b/browser/edge/find_others.go @@ -0,0 +1,9 @@ +//go:build !windows && !darwin && !linux + +package edge + +import "errors" + +func edgeChromiumRoots(yield func(string, error) bool) { + _ = yield(``, errors.New(`platform not supported`)) +} diff --git a/browser/edge/find_windows.go b/browser/edge/find_windows.go new file mode 100644 index 0000000..ff7c424 --- /dev/null +++ b/browser/edge/find_windows.go @@ -0,0 +1,51 @@ +//go:build windows +// +build windows + +package edge + +import ( + "os" + "path/filepath" + + iefind "github.com/xiazemin/kooky/internal/ie/find" +) + +func edgeChromiumRoots(yield func(string, error) bool) { + // %LocalAppData% + locApp, err := os.UserCacheDir() + if err != nil { + _ = yield(``, err) + return + } + if !yield(filepath.Join(locApp, `Microsoft`, `Edge`, `User Data`), nil) { + return + } +} + +func init() { + edgeOldCookieStores = (&iefind.IEFinder{Browser: `edge`}).FindCookieStores() +} + +/* +https://www.nirsoft.net/utils/edge_cookies_view.html +starting from Fall Creators Update 1709 of Windows 10, the cookies of Microsoft Edge Web browser are stored in the WebCacheV01.dat database +ESE database at %USERPROFILE%\AppData\Local\Microsoft\Windows\WebCache\WebCacheV01.dat (%LocalAppData%\Microsoft\Windows\WebCache\WebCacheV01.dat) +CookieEntryEx_## + +https://www.linkedin.com/pulse/windows-10-microsoft-edge-browser-forensics-brent-muir +https://bsmuir.kinja.com/windows-10-microsoft-edge-browser-forensics-1733533818 + +locations: +%LocalAppData%\Microsoft\Windows\WebCache\WebCacheV01.dat +%LocalAppData%\Microsoft\Edge\User Data\Default + +https://www.foxtonforensics.com/browser-history-examiner/microsoft-edge-history-location +v79+: +Edge Cookies are stored in the 'Cookies' SQLite database, within the 'cookies' table. + +up to v44: +Edge Cookies are stored in the 'WebCacheV01.dat' ESE database, within the 'CookieEntryEx' containers. + +older: +Older versions of Edge stored cookies as separate text files in locations specified within the ESE database. +*/ diff --git a/browser/elinks/elinks.go b/browser/elinks/elinks.go index 0178ce9..3477d33 100644 --- a/browser/elinks/elinks.go +++ b/browser/elinks/elinks.go @@ -2,14 +2,16 @@ package elinks import ( "bufio" + "context" "errors" - "net/http" + "fmt" "strconv" "strings" "time" "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/iterx" ) type elinksCookieStore struct { @@ -18,75 +20,69 @@ type elinksCookieStore struct { var _ cookies.CookieStore = (*elinksCookieStore)(nil) -func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { - s, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer s.Close() +func ReadCookies(ctx context.Context, filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { + return cookies.SingleRead(cookieStore, filename, filters...).ReadAllCookies(ctx) +} - return s.ReadCookies(filters...) +func TraverseCookies(filename string, filters ...kooky.Filter) kooky.CookieSeq { + return cookies.SingleRead(cookieStore, filename, filters...) } -func (s *elinksCookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cookie, error) { +func (s *elinksCookieStore) TraverseCookies(filters ...kooky.Filter) kooky.CookieSeq { if s == nil { - return nil, errors.New(`cookie store is nil`) - } - if err := s.Open(); err != nil { - return nil, err - } else if s.File == nil { - return nil, errors.New(`file is nil`) + return iterx.ErrCookieSeq(errors.New(`cookie store is nil`)) } - var ret []*kooky.Cookie - - scanner := bufio.NewScanner(s.File) - for scanner.Scan() { - line := scanner.Text() + colCnt := 8 + parseLine := func(line string) (*kooky.Cookie, error) { sp := strings.Split(line, "\t") - if len(sp) != 8 { - continue + if l := len(sp); l != colCnt { + return nil, fmt.Errorf(`has %d fields; expected are %d: %q`, l, colCnt, line) } exp, err := strconv.ParseInt(sp[5], 10, 64) if err != nil { - continue + return nil, fmt.Errorf(`Expires field is not an integer: %w`, err) } sec, err := strconv.Atoi(sp[6]) if err != nil { - continue + return nil, fmt.Errorf(`Secure field is not an integer: %w`, err) } cookie := &kooky.Cookie{} - cookie.Name = sp[0] cookie.Value = sp[1] cookie.Path = sp[3] cookie.Domain = sp[4] cookie.Expires = time.Unix(exp, 0) cookie.Secure = sec == 1 + cookie.Browser = s - if !kooky.FilterCookie(cookie, filters...) { - continue - } - - ret = append(ret, cookie) + return cookie, nil } - return ret, nil -} -// CookieJar returns an initiated http.CookieJar based on the cookies stored by -// the ELinks browser. Set cookies are memory stored and do not modify any -// browser files. -func CookieJar(filename string, filters ...kooky.Filter) (http.CookieJar, error) { - j, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer j.Close() - if err := j.InitJar(); err != nil { - return nil, err + return func(yield func(*kooky.Cookie, error) bool) { + if err := s.Open(); err != nil { + yield(nil, err) + return + } else if s.File == nil { + yield(nil, errors.New(`file is nil`)) + return + } + + var lineNr int + scanner := bufio.NewScanner(s.File) + for scanner.Scan() { + line := scanner.Text() + lineNr++ + cookie, err := parseLine(line) + if err != nil { + err = fmt.Errorf(`row %d: `, lineNr) + } + if !iterx.CookieFilterYield(context.Background(), cookie, err, yield, filters...) { + return + } + } } - return j, nil } // CookieStore has to be closed with CookieStore.Close() after use. @@ -99,5 +95,5 @@ func cookieStore(filename string, filters ...kooky.Filter) (*cookies.CookieJar, s.FileNameStr = filename s.BrowserStr = `elinks` - return &cookies.CookieJar{CookieStore: s}, nil + return cookies.NewCookieJar(s, filters...), nil } diff --git a/browser/elinks/elinks_test.go b/browser/elinks/elinks_test.go index c5ccba3..61c5616 100644 --- a/browser/elinks/elinks_test.go +++ b/browser/elinks/elinks_test.go @@ -1,6 +1,7 @@ package elinks import ( + "context" "testing" "time" @@ -13,7 +14,7 @@ func TestReadCookies(t *testing.T) { t.Fatalf("Failed to load test data file") } - cookies, err := ReadCookies(testCookiesPath) + cookies, err := TraverseCookies(testCookiesPath).ReadAllCookies(context.Background()) if err != nil { t.Fatal(err) } diff --git a/browser/elinks/find.go b/browser/elinks/find.go index 72b0be6..9f3e773 100644 --- a/browser/elinks/find.go +++ b/browser/elinks/find.go @@ -18,14 +18,15 @@ func init() { kooky.RegisterFinder(`elinks`, &elinksFinder{}) } -func (f *elinksFinder) FindCookieStores() ([]kooky.CookieStore, error) { - home, err := os.UserHomeDir() - if err != nil { - return nil, err - } - - var ret = []kooky.CookieStore{ - &cookies.CookieJar{ +func (f *elinksFinder) FindCookieStores() kooky.CookieStoreSeq { + return func(yield func(kooky.CookieStore, error) bool) { + home, err := os.UserHomeDir() + if err != nil { + _ = yield(nil, err) + return + } + + st := &cookies.CookieJar{ CookieStore: &elinksCookieStore{ DefaultCookieStore: cookies.DefaultCookieStore{ BrowserStr: `elinks`, @@ -33,8 +34,9 @@ func (f *elinksFinder) FindCookieStores() ([]kooky.CookieStore, error) { FileNameStr: filepath.Join(home, `.elinks`, `cookies`), }, }, - }, + } + if !yield(st, nil) { + return + } } - - return ret, nil } diff --git a/browser/epiphany/cookiestore.go b/browser/epiphany/cookiestore.go index e5f6d3a..25b81f6 100644 --- a/browser/epiphany/cookiestore.go +++ b/browser/epiphany/cookiestore.go @@ -5,6 +5,7 @@ import ( "github.com/go-sqlite/sqlite3" "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/utils" ) type epiphanyCookieStore struct { @@ -22,7 +23,11 @@ func (s *epiphanyCookieStore) Open() error { return nil } - db, err := sqlite3.Open(s.FileNameStr) + f, err := utils.OpenFile(s.FileNameStr) + if err != nil { + return err + } + db, err := sqlite3.OpenFrom(f) if err != nil { return err } diff --git a/browser/epiphany/epiphany.go b/browser/epiphany/epiphany.go index abc45d7..cc72c0b 100644 --- a/browser/epiphany/epiphany.go +++ b/browser/epiphany/epiphany.go @@ -1,134 +1,102 @@ package epiphany import ( + "context" "errors" - "fmt" - "net/http" "time" "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/iterx" "github.com/xiazemin/kooky/internal/utils" ) -func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { - s, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer s.Close() +func ReadCookies(ctx context.Context, filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { + return cookies.SingleRead(cookieStore, filename, filters...).ReadAllCookies(ctx) +} - return s.ReadCookies(filters...) +func TraverseCookies(filename string, filters ...kooky.Filter) kooky.CookieSeq { + return cookies.SingleRead(cookieStore, filename, filters...) } -func (s *epiphanyCookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cookie, error) { +func (s *epiphanyCookieStore) TraverseCookies(filters ...kooky.Filter) kooky.CookieSeq { if s == nil { - return nil, errors.New(`cookie store is nil`) + return iterx.ErrCookieSeq(errors.New(`cookie store is nil`)) } if err := s.Open(); err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } else if s.Database == nil { - return nil, errors.New(`database is nil`) + return iterx.ErrCookieSeq(errors.New(`database is nil`)) } - var cookies []*kooky.Cookie - // Epiphany originally used a Mozilla Gecko backend but later switched to WebKit. // For possible deviations from the firefox database layout // it might be better not to depend on the firefox implementation. - err := utils.VisitTableRows(s.Database, `moz_cookies`, map[string]string{}, func(rowId *int64, row utils.TableRow) error { - cookie := kooky.Cookie{} - var err error - - // Name - cookie.Name, err = row.String(`name`) - if err != nil { - return err - } - - // Value - cookie.Value, err = row.String(`value`) - if err != nil { - return err - } - - // Host - cookie.Domain, err = row.String(`host`) - if err != nil { - return err - } - - // Path - cookie.Path, err = row.String(`path`) - if err != nil { - return err - } - - // Expires - var expiry int64 - exp, err := row.Value(`expiry`) - if err != nil { - return err - } - switch v := exp.(type) { - case int64: - expiry = v - case int32: - expiry = int64(v) - default: - return fmt.Errorf("got unexpected value for Expires %v (type %[1]T)", expiry) - } - cookie.Expires = time.Unix(expiry, 0) - - // Secure - sec, err := row.Value(`isSecure`) - if err != nil { - return err + visitor := func(yield func(*kooky.Cookie, error) bool) func(rowId *int64, row utils.TableRow) error { + return func(rowId *int64, row utils.TableRow) error { + cookie := kooky.Cookie{} + var err error + + // Name + cookie.Name, err = row.String(`name`) + if err != nil { + return err + } + + // Value + cookie.Value, err = row.String(`value`) + if err != nil { + return err + } + + // Host + cookie.Domain, err = row.String(`host`) + if err != nil { + return err + } + + // Path + cookie.Path, err = row.String(`path`) + if err != nil { + return err + } + + // Expires + expiry, err := utils.ValueOrFallback[int64](row, `expiry`, 0, true) + if err != nil { + return err + } + cookie.Expires = time.Unix(expiry, 0) + + // Secure + cookie.Secure, err = row.Bool(`isSecure`) + if err != nil { + return err + } + + // HttpOnly + cookie.HttpOnly, err = row.Bool(`isHttpOnly`) + if err != nil { + return err + } + cookie.Browser = s + + if !iterx.CookieFilterYield(context.Background(), &cookie, nil, yield, filters...) { + return iterx.ErrYieldEnd + } + + return nil } - secInt, okSec := sec.(int) - if !okSec { - return fmt.Errorf("got unexpected value for Secure %v (type %[1]T)", sec) - } - cookie.Secure = secInt > 0 - - // HttpOnly - ho, err := row.Value(`isHttpOnly`) - if err != nil { - return err - } - hoInt, okHO := ho.(int) - if !okHO { - return fmt.Errorf("got unexpected value for HttpOnly %v (type %[1]T)", ho) - } - cookie.HttpOnly = hoInt > 0 - - if kooky.FilterCookie(&cookie, filters...) { - cookies = append(cookies, &cookie) + } + seq := func(yield func(*kooky.Cookie, error) bool) { + err := utils.VisitTableRows(s.Database, `moz_cookies`, map[string]string{}, visitor(yield)) + if !errors.Is(err, iterx.ErrYieldEnd) { + yield(nil, err) } - - return nil - }) - if err != nil { - return nil, err } - return cookies, nil -} - -// CookieJar returns an initiated http.CookieJar based on the cookies stored by -// the Epiphany/Gnome Web browser. Set cookies are memory stored and do not modify any -// browser files. -func CookieJar(filename string, filters ...kooky.Filter) (http.CookieJar, error) { - j, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer j.Close() - if err := j.InitJar(); err != nil { - return nil, err - } - return j, nil + return seq } // CookieStore has to be closed with CookieStore.Close() after use. @@ -141,5 +109,5 @@ func cookieStore(filename string, filters ...kooky.Filter) (*cookies.CookieJar, s.FileNameStr = filename s.BrowserStr = `epiphany` - return &cookies.CookieJar{CookieStore: s}, nil + return cookies.NewCookieJar(s, filters...), nil } diff --git a/browser/epiphany/find.go b/browser/epiphany/find.go index 0073858..589f8b4 100644 --- a/browser/epiphany/find.go +++ b/browser/epiphany/find.go @@ -18,19 +18,17 @@ func init() { kooky.RegisterFinder(`epiphany`, &epiphanyFinder{}) } -func (f *epiphanyFinder) FindCookieStores() ([]kooky.CookieStore, error) { - roots, err := epiphanyRoots() - if err != nil { - return nil, err - } - - var ret []kooky.CookieStore +func (f *epiphanyFinder) FindCookieStores() kooky.CookieStoreSeq { + return func(yield func(kooky.CookieStore, error) bool) { + roots, err := epiphanyRoots() + if err != nil { + _ = yield(nil, err) + return + } - last := len(roots) - 1 - for i, root := range roots { - ret = append( - ret, - &cookies.CookieJar{ + last := len(roots) - 1 + for i, root := range roots { + st := &cookies.CookieJar{ CookieStore: &epiphanyCookieStore{ DefaultCookieStore: cookies.DefaultCookieStore{ BrowserStr: `epiphany`, @@ -38,11 +36,13 @@ func (f *epiphanyFinder) FindCookieStores() ([]kooky.CookieStore, error) { FileNameStr: filepath.Join(root, `cookies.sqlite`), }, }, - }, - ) - } + } + if !yield(st, nil) { + return + } + } - return ret, nil + } } func epiphanyRoots() ([]string, error) { diff --git a/browser/firefox/find.go b/browser/firefox/find.go index 11f0372..4b4a12b 100644 --- a/browser/firefox/find.go +++ b/browser/firefox/find.go @@ -15,17 +15,15 @@ func init() { kooky.RegisterFinder(`firefox`, &firefoxFinder{}) } -func (f *firefoxFinder) FindCookieStores() ([]kooky.CookieStore, error) { - files, err := find.FindFirefoxCookieStoreFiles() - if err != nil { - return nil, err - } - - var ret []kooky.CookieStore - for _, file := range files { - ret = append( - ret, - &cookies.CookieJar{ +func (f *firefoxFinder) FindCookieStores() kooky.CookieStoreSeq { + return func(yield func(kooky.CookieStore, error) bool) { + for file, err := range find.FindFirefoxCookieStoreFiles() { + if err != nil { + if !yield(nil, err) { + return + } + } + st := &cookies.CookieJar{ CookieStore: &firefox.CookieStore{ DefaultCookieStore: cookies.DefaultCookieStore{ BrowserStr: file.Browser, @@ -34,9 +32,10 @@ func (f *firefoxFinder) FindCookieStores() ([]kooky.CookieStore, error) { FileNameStr: file.Path, }, }, - }, - ) + } + if !yield(st, nil) { + return + } + } } - - return ret, nil } diff --git a/browser/firefox/firefox.go b/browser/firefox/firefox.go index 67cb9e4..0065818 100644 --- a/browser/firefox/firefox.go +++ b/browser/firefox/firefox.go @@ -1,36 +1,19 @@ package firefox import ( - "net/http" + "context" "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/cookies" "github.com/xiazemin/kooky/internal/firefox" ) -func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { - s, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer s.Close() - - return s.ReadCookies(filters...) +func ReadCookies(ctx context.Context, filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { + return cookies.SingleRead(cookieStore, filename, filters...).ReadAllCookies(ctx) } -// CookieJar returns an initiated http.CookieJar based on the cookies stored by -// the Firefox browser. Set cookies are memory stored and do not modify any -// browser files. -func CookieJar(filename string, filters ...kooky.Filter) (http.CookieJar, error) { - j, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer j.Close() - if err := j.InitJar(); err != nil { - return nil, err - } - return j, nil +func TraverseCookies(filename string, filters ...kooky.Filter) kooky.CookieSeq { + return cookies.SingleRead(cookieStore, filename, filters...) } // CookieStore has to be closed with CookieStore.Close() after use. @@ -43,5 +26,5 @@ func cookieStore(filename string, filters ...kooky.Filter) (*cookies.CookieJar, s.FileNameStr = filename s.BrowserStr = `firefox` - return &cookies.CookieJar{CookieStore: s}, nil + return cookies.NewCookieJar(s, filters...), nil } diff --git a/browser/firefox/firefox_test.go b/browser/firefox/firefox_test.go index 2484031..08af42f 100644 --- a/browser/firefox/firefox_test.go +++ b/browser/firefox/firefox_test.go @@ -1,6 +1,7 @@ package firefox import ( + "context" "testing" "time" @@ -16,7 +17,8 @@ func TestReadCookies(t *testing.T) { t.Fatalf("Failed to load test data file") } - cookies, err := ReadCookies(testCookiesPath) + ctx := context.Background() + cookies, err := TraverseCookies(testCookiesPath).ReadAllCookies(ctx) if err != nil { t.Fatal(err) } @@ -64,7 +66,7 @@ func TestReadCookies(t *testing.T) { t.Fatalf("Failed to load test data file") } - cookies, err = ReadCookies(testCookiesPath) + cookies, err = TraverseCookies(testCookiesPath).ReadAllCookies(ctx) if err != nil { t.Fatal(err) } @@ -79,7 +81,7 @@ func TestReadCookies(t *testing.T) { } c = cookies[0] - if c.Domain != "google.de" { + if c.Domain != ".google.de" { t.Errorf("c.Domain=%q", c.Domain) } if c.Name != "NID" { diff --git a/browser/ie/find.go b/browser/ie/find.go index d6b6498..f56b38e 100644 --- a/browser/ie/find.go +++ b/browser/ie/find.go @@ -11,17 +11,16 @@ type ieFinder struct{} var _ kooky.CookieStoreFinder = (*ieFinder)(nil) -func init() { - kooky.RegisterFinder(`ie`, &ieFinder{}) -} +func init() { kooky.RegisterFinder(`ie`, &ieFinder{}) } -func (f *ieFinder) FindCookieStores() ([]kooky.CookieStore, error) { - roots, _ := ieRoots() - var cookiesFiles []kooky.CookieStore - for _, root := range roots { - cookiesFiles = append( - cookiesFiles, - &cookies.CookieJar{ +func (f *ieFinder) FindCookieStores() kooky.CookieStoreSeq { + return func(yield func(kooky.CookieStore, error) bool) { + for root, err := range ieRoots { + if err != nil { + _ = yield(nil, err) + return + } + st := &cookies.CookieJar{ CookieStore: &ie.CookieStore{ CookieStore: &ie.TextCookieStore{ DefaultCookieStore: cookies.DefaultCookieStore{ @@ -31,9 +30,10 @@ func (f *ieFinder) FindCookieStores() ([]kooky.CookieStore, error) { }, }, }, - }, - ) + } + if !yield(st, nil) { + return + } + } } - - return cookiesFiles, nil } diff --git a/browser/ie/find_others.go b/browser/ie/find_others.go index 98c969e..2d7a049 100644 --- a/browser/ie/find_others.go +++ b/browser/ie/find_others.go @@ -6,6 +6,4 @@ import "errors" // TODO -func ieRoots() ([]string, error) { - return nil, errors.New(`not implemented`) -} +func ieRoots(yield func(string, error) bool) { _ = yield(``, errors.New(`not implemented`)) } diff --git a/browser/ie/find_windows.go b/browser/ie/find_windows.go index 7622364..14f1bf9 100644 --- a/browser/ie/find_windows.go +++ b/browser/ie/find_windows.go @@ -7,14 +7,17 @@ import ( "path/filepath" ) -func ieRoots() ([]string, error) { +func ieRoots(yield func(string, error) bool) { confDir, err := os.UserConfigDir() if err != nil { - return nil, err + _ = yield(``, err) + return } - return []string{ - filepath.Join(confDir, `Microsoft`, `Windows`, `Cookies`), - filepath.Join(confDir, `Microsoft`, `Windows`, `Cookies`, `Low`), - }, nil + if !yield(filepath.Join(confDir, `Microsoft`, `Windows`, `Cookies`), nil) { + return + } + if !yield(filepath.Join(confDir, `Microsoft`, `Windows`, `Cookies`, `Low`), nil) { + return + } } diff --git a/browser/ie/ie.go b/browser/ie/ie.go index 0f78b38..4a2442c 100644 --- a/browser/ie/ie.go +++ b/browser/ie/ie.go @@ -1,7 +1,7 @@ package ie import ( - "net/http" + "context" "os" "github.com/xiazemin/kooky" @@ -9,29 +9,12 @@ import ( "github.com/xiazemin/kooky/internal/ie" ) -func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { - s, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer s.Close() - - return s.ReadCookies(filters...) +func ReadCookies(ctx context.Context, filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { + return cookies.SingleRead(cookieStore, filename, filters...).ReadAllCookies(ctx) } -// CookieJar returns an initiated http.CookieJar based on the cookies stored by -// the Internet Explorer browser. Set cookies are memory stored and do not modify any -// browser files. -func CookieJar(filename string, filters ...kooky.Filter) (http.CookieJar, error) { - j, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer j.Close() - if err := j.InitJar(); err != nil { - return nil, err - } - return j, nil +func TraverseCookies(filename string, filters ...kooky.Filter) kooky.CookieSeq { + return cookies.SingleRead(cookieStore, filename, filters...) } // CookieStore has to be closed with CookieStore.Close() after use. diff --git a/browser/ie/ie_test.go b/browser/ie/ie_test.go index 17865d1..6fc3dee 100644 --- a/browser/ie/ie_test.go +++ b/browser/ie/ie_test.go @@ -1,6 +1,7 @@ package ie import ( + "context" "testing" "time" @@ -13,7 +14,7 @@ func TestReadCookies(t *testing.T) { t.Fatalf("Failed to load test data file") } - cookies, err := ReadCookies(testCookiesPath) + cookies, err := TraverseCookies(testCookiesPath).ReadAllCookies(context.Background()) if err != nil { t.Fatal(err) } diff --git a/browser/konqueror/find.go b/browser/konqueror/find.go index bcd7826..5ec18ed 100644 --- a/browser/konqueror/find.go +++ b/browser/konqueror/find.go @@ -18,42 +18,47 @@ func init() { kooky.RegisterFinder(`konqueror`, &konquerorFinder{}) } -func (f *konquerorFinder) FindCookieStores() ([]kooky.CookieStore, error) { - roots, err := konquerorRoots() - if err != nil { - return nil, err +func (f *konquerorFinder) FindCookieStores() kooky.CookieStoreSeq { + return func(yield func(kooky.CookieStore, error) bool) { + //var stInner *cookies.DefaultCookieStore + /*defer func() { + if stInner == nil { + return + } + stInner.IsDefaultProfileBool = true + }()*/ + for root, err := range konquerorRoots { + if err != nil { + if !yield(nil, err) { + return + } + } + stInner := &cookies.DefaultCookieStore{ + BrowserStr: `konqueror`, + FileNameStr: filepath.Join(root, `kcookiejar`, `cookies`), + } + st := &cookies.CookieJar{CookieStore: &konquerorCookieStore{DefaultCookieStore: *stInner}} + if !yield(st, nil) { + return + } + } } - - var ret []kooky.CookieStore - - last := len(roots) - 1 - for i, root := range roots { - ret = append( - ret, - &cookies.CookieJar{ - CookieStore: &konquerorCookieStore{ - DefaultCookieStore: cookies.DefaultCookieStore{ - BrowserStr: `konqueror`, - IsDefaultProfileBool: i == last, - FileNameStr: filepath.Join(root, `kcookiejar`, `cookies`), - }, - }, - }, - ) - } - - return ret, nil } -func konquerorRoots() ([]string, error) { - var ret []string +func konquerorRoots(yield func(string, error) bool) { // fallback - if home, err := os.UserHomeDir(); err == nil { - ret = append(ret, filepath.Join(home, `.local`, `share`)) + if home, err := os.UserHomeDir(); err != nil { + if !yield(``, err) { + return + } + } else { + if !yield(filepath.Join(home, `.local`, `share`), nil) { + return + } } if dataDir, ok := os.LookupEnv(`XDG_DATA_HOME`); ok { - ret = append(ret, dataDir) + if !yield(dataDir, nil) { + return + } } - - return ret, nil } diff --git a/browser/konqueror/konqueror.go b/browser/konqueror/konqueror.go index 1758579..1b874fc 100644 --- a/browser/konqueror/konqueror.go +++ b/browser/konqueror/konqueror.go @@ -2,14 +2,15 @@ package konqueror import ( "bufio" + "context" "errors" - "net/http" "strconv" "strings" "time" "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/iterx" "golang.org/x/text/encoding/charmap" ) @@ -20,146 +21,128 @@ type konquerorCookieStore struct { var _ cookies.CookieStore = (*konquerorCookieStore)(nil) -func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { - s, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer s.Close() +func ReadCookies(ctx context.Context, filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { + return cookies.SingleRead(cookieStore, filename, filters...).ReadAllCookies(ctx) +} - return s.ReadCookies(filters...) +func TraverseCookies(filename string, filters ...kooky.Filter) kooky.CookieSeq { + return cookies.SingleRead(cookieStore, filename, filters...) } -func (s *konquerorCookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cookie, error) { +func (s *konquerorCookieStore) TraverseCookies(filters ...kooky.Filter) kooky.CookieSeq { if s == nil { - return nil, errors.New(`cookie store is nil`) + return iterx.ErrCookieSeq(errors.New(`cookie store is nil`)) } - if err := s.Open(); err != nil { - return nil, err - } else if s.File == nil { - return nil, errors.New(`file is nil`) - } - - var ret []*kooky.Cookie - - latin1 := charmap.ISO8859_1.NewDecoder().Reader(s.File) - - scanner := bufio.NewScanner(latin1) - for scanner.Scan() { - line := scanner.Text() - // lines should be at least 97 characters long - // skip comments and domain section headers - if len(line) == 0 || line[0] == '#' || line[0] == '[' { - continue - } - - // Host Domain Path Expires Prot Name Sec Value - - // HOST - sp := strings.SplitN(line, ` `, 2) - if len(sp) != 2 { - continue - } - cookie := &kooky.Cookie{} - // the Domain field is empty if the cookie is not for subdomains. - // in that case the domain string does not start with '.' disallowing subdomains. - cookie.Domain = sp[0] // fallback - - // DOMAIN - sp = strings.SplitN(strings.TrimLeft(sp[1], ` `), `"`, 3) - if len(sp) != 3 { - continue - } - if len(sp[0]) != 0 { - // Domain field is not quoted - continue - } - if len(sp[1]) > 0 { - // regular domain string starting with '.' allowing subdomains - cookie.Domain = sp[1] - } - - // PATH - sp = strings.SplitN(strings.TrimLeft(sp[2], ` `), `"`, 3) - if len(sp) != 3 || len(sp[0]) != 0 { - // Path field is not quoted (if sp[0] empty) - continue - } - cookie.Path = sp[1] - - // EXPIRES - sp = strings.SplitN(strings.TrimLeft(sp[2], ` `), ` `, 2) - if len(sp) != 2 { - continue - } - exp, err := strconv.ParseInt(sp[0], 10, 64) - if err != nil { - continue - } - - // PROT - // skip - sp = strings.SplitN(strings.TrimLeft(sp[1], ` `), ` `, 2) - if len(sp) != 2 { - continue - } - if _, err := strconv.Atoi(sp[0]); err != nil { - continue - } - - // NAME - sp = strings.SplitN(strings.TrimLeft(sp[1], ` `), ` `, 2) - if len(sp) != 2 { - continue - } - cookie.Name = sp[0] - // SEC - sp = strings.SplitN(strings.TrimLeft(sp[1], ` `), ` `, 2) - if len(sp) != 2 { - continue - } - sec, err := strconv.Atoi(sp[0]) - if err != nil { - continue + return func(yield func(*kooky.Cookie, error) bool) { + if err := s.Open(); err != nil { + yield(nil, err) + } else if s.File == nil { + yield(nil, errors.New(`file is nil`)) } - // VALUE - cookie.Value = strings.Trim(sp[1], ` `) - - cookie.Expires = time.Unix(exp, 0) - - const ( - secure int = 1 - httpOnly int = 2 - hasExplicitPath int = 4 - emptyName int = 8 - ) - cookie.Secure = sec&secure != 0 - cookie.HttpOnly = sec&httpOnly != 0 - - if !kooky.FilterCookie(cookie, filters...) { - continue + latin1 := charmap.ISO8859_1.NewDecoder().Reader(s.File) + + scanner := bufio.NewScanner(latin1) + for scanner.Scan() { + line := scanner.Text() + // lines should be at least 97 characters long + // skip comments and domain section headers + if len(line) == 0 || line[0] == '#' || line[0] == '[' { + continue + } + + // Host Domain Path Expires Prot Name Sec Value + + // HOST + sp := strings.SplitN(line, ` `, 2) + if len(sp) != 2 { + continue + } + cookie := &kooky.Cookie{} + // the Domain field is empty if the cookie is not for subdomains. + // in that case the domain string does not start with '.' disallowing subdomains. + cookie.Domain = sp[0] // fallback + + // DOMAIN + sp = strings.SplitN(strings.TrimLeft(sp[1], ` `), `"`, 3) + if len(sp) != 3 { + continue + } + if len(sp[0]) != 0 { + // Domain field is not quoted + continue + } + if len(sp[1]) > 0 { + // regular domain string starting with '.' allowing subdomains + cookie.Domain = sp[1] + } + + // PATH + sp = strings.SplitN(strings.TrimLeft(sp[2], ` `), `"`, 3) + if len(sp) != 3 || len(sp[0]) != 0 { + // Path field is not quoted (if sp[0] empty) + continue + } + cookie.Path = sp[1] + + // EXPIRES + sp = strings.SplitN(strings.TrimLeft(sp[2], ` `), ` `, 2) + if len(sp) != 2 { + continue + } + exp, err := strconv.ParseInt(sp[0], 10, 64) + if err != nil { + continue + } + + // PROT + // skip + sp = strings.SplitN(strings.TrimLeft(sp[1], ` `), ` `, 2) + if len(sp) != 2 { + continue + } + if _, err := strconv.Atoi(sp[0]); err != nil { + continue + } + + // NAME + sp = strings.SplitN(strings.TrimLeft(sp[1], ` `), ` `, 2) + if len(sp) != 2 { + continue + } + cookie.Name = sp[0] + + // SEC + sp = strings.SplitN(strings.TrimLeft(sp[1], ` `), ` `, 2) + if len(sp) != 2 { + continue + } + sec, err := strconv.Atoi(sp[0]) + if err != nil { + continue + } + + // VALUE + cookie.Value = strings.Trim(sp[1], ` `) + + cookie.Expires = time.Unix(exp, 0) + + const ( + secure int = 1 + httpOnly int = 2 + hasExplicitPath int = 4 + emptyName int = 8 + ) + cookie.Secure = sec&secure != 0 + cookie.HttpOnly = sec&httpOnly != 0 + cookie.Browser = s + + if !iterx.CookieFilterYield(context.Background(), cookie, nil, yield, filters...) { + return + } } - - ret = append(ret, cookie) - } - return ret, nil -} - -// CookieJar returns an initiated http.CookieJar based on the cookies stored by -// the Konqueror browser. Set cookies are memory stored and do not modify any -// browser files. -func CookieJar(filename string, filters ...kooky.Filter) (http.CookieJar, error) { - j, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer j.Close() - if err := j.InitJar(); err != nil { - return nil, err } - return j, nil } // CookieStore has to be closed with CookieStore.Close() after use. @@ -172,7 +155,7 @@ func cookieStore(filename string, filters ...kooky.Filter) (*cookies.CookieJar, s.FileNameStr = filename s.BrowserStr = `konqueror` - return &cookies.CookieJar{CookieStore: s}, nil + return cookies.NewCookieJar(s, filters...), nil } /* diff --git a/browser/konqueror/konqueror_test.go b/browser/konqueror/konqueror_test.go index 3866900..ba18e75 100644 --- a/browser/konqueror/konqueror_test.go +++ b/browser/konqueror/konqueror_test.go @@ -1,6 +1,7 @@ package konqueror import ( + "context" "testing" "time" @@ -13,7 +14,7 @@ func TestReadCookies(t *testing.T) { t.Fatalf("Failed to load test data file") } - cookies, err := ReadCookies(testCookiesPath) + cookies, err := TraverseCookies(testCookiesPath).ReadAllCookies(context.Background()) if err != nil { t.Fatal(err) } diff --git a/browser/lynx/find.go b/browser/lynx/find.go index ed98a69..9d745df 100644 --- a/browser/lynx/find.go +++ b/browser/lynx/find.go @@ -14,6 +14,7 @@ import ( "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/cookies" "github.com/xiazemin/kooky/internal/netscape" + "github.com/xiazemin/kooky/internal/utils" ) type lynxFinder struct{} @@ -24,17 +25,16 @@ func init() { kooky.RegisterFinder(`lynx`, &lynxFinder{}) } -func (f *lynxFinder) FindCookieStores() ([]kooky.CookieStore, error) { - home, err := os.UserHomeDir() - if err != nil { - return nil, err - } - var ret []kooky.CookieStore +func (f *lynxFinder) FindCookieStores() kooky.CookieStoreSeq { + return func(yield func(kooky.CookieStore, error) bool) { + home, err := os.UserHomeDir() + if err != nil { + _ = yield(nil, err) + return + } - // the default value is ~/.lynx_cookies for most systems, but ~/cookies for MS-DOS - ret = append( - ret, - &cookies.CookieJar{ + // the default value is ~/.lynx_cookies for most systems, but ~/cookies for MS-DOS + st := &cookies.CookieJar{ CookieStore: &netscape.CookieStore{ DefaultCookieStore: cookies.DefaultCookieStore{ BrowserStr: `lynx`, @@ -42,97 +42,93 @@ func (f *lynxFinder) FindCookieStores() ([]kooky.CookieStore, error) { FileNameStr: filepath.Join(home, `.lynx_cookies`), }, }, - }, - ) - - // parse config files so that we don't have to execute lynx -show_cfg - configFiles := []string{ - filepath.Join(`/etc`, `lynx.cfg`), - filepath.Join(`/etc`, `lynx`, `lynx.cfg`), // Debian - } + } + if !yield(st, nil) { + return + } - // INCLUDE:/etc/lynx/local.cfg - // `/etc/lynx/lynx.cfg` includes `/etc/lynx/local.cfg` on Debian - // https://lynx.invisible-island.net/current/README.cookies - // COOKIE_FILE:/path/to/directory/.lynx_cookies // read file (?) - // COOKIE_SAVE_FILE:/path/to/directory/.lynx_cookies // save file + // parse config files so that we don't have to execute lynx -show_cfg + configFiles := []string{ + filepath.Join(`/etc`, `lynx.cfg`), + filepath.Join(`/etc`, `lynx`, `lynx.cfg`), // Debian + } - var includes, cookieFiles, cookieSaveFiles []string - parse := func(configFile string) error { - file, err := os.Open(configFile) - if err != nil { - return err + // INCLUDE:/etc/lynx/local.cfg + // `/etc/lynx/lynx.cfg` includes `/etc/lynx/local.cfg` on Debian + // https://lynx.invisible-island.net/current/README.cookies + // COOKIE_FILE:/path/to/directory/.lynx_cookies // read file (?) + // COOKIE_SAVE_FILE:/path/to/directory/.lynx_cookies // save file + + var primCookieFile string + storeForFile := func(cookieFile string) *cookies.CookieJar { + return &cookies.CookieJar{ + CookieStore: &netscape.CookieStore{ + DefaultCookieStore: cookies.DefaultCookieStore{ + BrowserStr: `lynx`, + // last one probably overwrites earlier configuration + IsDefaultProfileBool: cookieFile == primCookieFile, + FileNameStr: cookieFile, + }, + }, + } } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, `INCLUDE:`) { - sp := strings.Split(line, `:`) - if len(sp) == 2 { - includes = append(includes, sp[1]) - } + + cookieMap := make(map[string]struct{}) + var includes, cookieFiles, cookieSaveFiles []string + parse := func(configFile string) error { + file, err := utils.OpenFile(configFile) + if err != nil { + return err } - if strings.HasPrefix(line, `COOKIE_FILE:`) { - sp := strings.Split(line, `:`) - if len(sp) == 2 { - cookieFiles = append(cookieFiles, sp[1]) + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, `INCLUDE:`) { + sp := strings.Split(line, `:`) + if len(sp) == 2 { + includes = append(includes, sp[1]) + } } - } - if strings.HasPrefix(line, `COOKIE_SAVE_FILE:`) { - sp := strings.Split(line, `:`) - if len(sp) == 2 { - cookieSaveFiles = append(cookieSaveFiles, sp[1]) + if strings.HasPrefix(line, `COOKIE_FILE:`) { + sp := strings.Split(line, `:`) + if len(sp) == 2 { + // cookie file + cookieFile := sp[1] + primCookieFile = cookieFile + cookieFiles = append(cookieFiles, cookieFile) + cookieMap[cookieFile] = struct{}{} + } + } + if strings.HasPrefix(line, `COOKIE_SAVE_FILE:`) { + sp := strings.Split(line, `:`) + if len(sp) == 2 { + // cookie save file + cookieSaveFile := sp[1] + cookieSaveFiles = append(cookieSaveFiles, cookieSaveFile) + cookieMap[cookieSaveFile] = struct{}{} + } } } + return nil } - return nil - } - -configFileLoop: - for _, configFile := range configFiles { - _ = parse(configFile) - } - if len(includes) > 0 { - configFiles = includes - includes = nil - goto configFileLoop - } - - var primCookieFile string - if len(cookieFiles) > 0 { - primCookieFile = cookieFiles[len(cookieFiles)-1] - } - cookieMap := make(map[string]struct{}) - for _, cookieFile := range append(cookieSaveFiles, cookieFiles...) { - if _, exists := cookieMap[cookieFile]; exists { - continue + configFileLoop: + for _, configFile := range configFiles { + _ = parse(configFile) + } + if len(includes) > 0 { + configFiles = includes + includes = nil + goto configFileLoop } - cookieMap[cookieFile] = struct{}{} - } - { - last := len(cookieMap) - 1 - i := 0 + // primCookieFile is now set for cookieFile := range cookieMap { - ret = append( - ret, - &cookies.CookieJar{ - CookieStore: &netscape.CookieStore{ - DefaultCookieStore: cookies.DefaultCookieStore{ - BrowserStr: `lynx`, - // last one probably overwrites earlier configuration - IsDefaultProfileBool: cookieFile == primCookieFile || i == last, - FileNameStr: cookieFile, - }, - }, - }, - ) - i++ + if !yield(storeForFile(cookieFile), nil) { + return + } } } - - return ret, nil } diff --git a/browser/lynx/lynx.go b/browser/lynx/lynx.go index 420e6ec..9dfce92 100644 --- a/browser/lynx/lynx.go +++ b/browser/lynx/lynx.go @@ -1,36 +1,19 @@ package lynx import ( - "net/http" + "context" "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/cookies" "github.com/xiazemin/kooky/internal/netscape" ) -func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { - s, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer s.Close() - - return s.ReadCookies(filters...) +func ReadCookies(ctx context.Context, filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { + return cookies.SingleRead(cookieStore, filename, filters...).ReadAllCookies(ctx) } -// CookieJar returns an initiated http.CookieJar based on the cookies stored by -// the Lynx browser. Set cookies are memory stored and do not modify any -// browser files. -func CookieJar(filename string, filters ...kooky.Filter) (http.CookieJar, error) { - j, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer j.Close() - if err := j.InitJar(); err != nil { - return nil, err - } - return j, nil +func TraverseCookies(filename string, filters ...kooky.Filter) kooky.CookieSeq { + return cookies.SingleRead(cookieStore, filename, filters...) } // CookieStore has to be closed with CookieStore.Close() after use. @@ -43,5 +26,5 @@ func cookieStore(filename string, filters ...kooky.Filter) (*cookies.CookieJar, s.FileNameStr = filename s.BrowserStr = `lynx` - return &cookies.CookieJar{CookieStore: s}, nil + return cookies.NewCookieJar(s, filters...), nil } diff --git a/browser/lynx/lynx_test.go b/browser/lynx/lynx_test.go index 9abb35e..e019255 100644 --- a/browser/lynx/lynx_test.go +++ b/browser/lynx/lynx_test.go @@ -1,9 +1,12 @@ package lynx import ( + "errors" "testing" "time" + "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/browser/netscape" "github.com/xiazemin/kooky/internal/testutils" ) @@ -13,9 +16,14 @@ func TestReadCookies(t *testing.T) { t.Fatalf("Failed to load test data file") } - cookies, err := ReadCookies(testCookiesPath) - if err != nil { - t.Fatal(err) + var cookies []*kooky.Cookie + for cookie, err := range TraverseCookies(testCookiesPath) { + if err != nil && !errors.Is(err, netscape.ErrNotStrict) { + t.Fatal(err) + } + if cookie != nil { + cookies = append(cookies, cookie) + } } if len(cookies) != 1 { diff --git a/browser/netscape/find.go b/browser/netscape/find.go index 6589b9d..63d9d84 100644 --- a/browser/netscape/find.go +++ b/browser/netscape/find.go @@ -15,17 +15,16 @@ func init() { kooky.RegisterFinder(`netscape`, &netscapeFinder{}) } -func (f *netscapeFinder) FindCookieStores() ([]kooky.CookieStore, error) { - files, err := find.FindCookieStoreFiles(netscapeRoots, `netscape`, `cookies.txt`) - if err != nil { - return nil, err - } - - var ret []kooky.CookieStore - for _, file := range files { - ret = append( - ret, - &cookies.CookieJar{ +func (f *netscapeFinder) FindCookieStores() kooky.CookieStoreSeq { + return func(yield func(kooky.CookieStore, error) bool) { + for file, err := range find.FindCookieStoreFiles(netscapeRoots, `netscape`, `cookies.txt`) { + if err != nil { + if !yield(nil, err) { + return + } + continue + } + st := &cookies.CookieJar{ CookieStore: &netscape.CookieStore{ DefaultCookieStore: cookies.DefaultCookieStore{ BrowserStr: file.Browser, @@ -34,9 +33,10 @@ func (f *netscapeFinder) FindCookieStores() ([]kooky.CookieStore, error) { FileNameStr: file.Path, }, }, - }, - ) + } + if !yield(st, nil) { + return + } + } } - - return ret, nil } diff --git a/browser/netscape/find_others.go b/browser/netscape/find_others.go index ac0e982..a5c84ba 100644 --- a/browser/netscape/find_others.go +++ b/browser/netscape/find_others.go @@ -1,9 +1,8 @@ -//+build windows darwin plan9 android js aix +//go:build windows || darwin || plan9 || android || js || aix +// +build windows darwin plan9 android js aix package netscape import "errors" -func netscapeRoots() ([]string, error) { - return nil, errors.New(`not implemented`) -} +func netscapeRoots(yield func(string, error) bool) { yield(``, errors.New(`not implemented`)) } diff --git a/browser/netscape/find_unix.go b/browser/netscape/find_unix.go index 2e4e875..7974ea7 100644 --- a/browser/netscape/find_unix.go +++ b/browser/netscape/find_unix.go @@ -1,4 +1,5 @@ -//+build !windows,!darwin,!plan9,!android,!js,!aix +//go:build !windows && !darwin && !plan9 && !android && !js && !aix +// +build !windows,!darwin,!plan9,!android,!js,!aix package netscape @@ -7,10 +8,13 @@ import ( "path/filepath" ) -func netscapeRoots() ([]string, error) { +func netscapeRoots(yield func(string, error) bool) { home, err := os.UserHomeDir() if err != nil { - return nil, err + _ = yield(``, err) + return + } + if !yield(filepath.Join(home, `.netscape`, `navigator`), nil) { + return } - return []string{filepath.Join(home, `.netscape`, `navigator`)}, nil } diff --git a/browser/netscape/netscape.go b/browser/netscape/netscape.go index 279c5cd..ab202b5 100644 --- a/browser/netscape/netscape.go +++ b/browser/netscape/netscape.go @@ -1,7 +1,7 @@ package netscape import ( - "net/http" + "context" "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/cookies" @@ -10,31 +10,20 @@ import ( // This ReadCookies() function returns an additional boolean "strict" telling // if the file adheres to the netscape cookies.txt format -func ReadCookies(filename string, filters ...kooky.Filter) (c []*kooky.Cookie, strict bool, e error) { - s := &netscape.CookieStore{} - s.FileNameStr = filename - s.BrowserStr = `netscape` - - defer s.Close() - - cookies, err := s.ReadCookies(filters...) - - return cookies, s.IsStrict(), err +func ReadCookies(ctx context.Context, filename string, filters ...kooky.Filter) (_ []*kooky.Cookie, strict bool, _ error) { + cs, str := TraverseCookies(filename, filters...) + cookies, err := cs.ReadAllCookies(ctx) + strict = err == nil && str() + return cookies, strict, err } -// CookieJar returns an initiated http.CookieJar based on the cookies stored by -// the Netscape browser. Set cookies are memory stored and do not modify any -// browser files. -func CookieJar(filename string, filters ...kooky.Filter) (http.CookieJar, error) { - j, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer j.Close() - if err := j.InitJar(); err != nil { - return nil, err - } - return j, nil +// This TraverseCookies() function returns an additional boolean returning func "strict" telling +// if the file adheres to the netscape cookies.txt format +func TraverseCookies(filename string, filters ...kooky.Filter) (_ kooky.CookieSeq, strict func() bool) { + st := cookieStoreBasic(filename) + stFilt := cookies.NewCookieJar(st, filters...) + seq := cookies.ReadCookiesClose(stFilt, filters...) + return seq, st.IsStrict } // CookieStore has to be closed with CookieStore.Close() after use. @@ -43,9 +32,14 @@ func CookieStore(filename string, filters ...kooky.Filter) (kooky.CookieStore, e } func cookieStore(filename string, filters ...kooky.Filter) (*cookies.CookieJar, error) { - s := &netscape.CookieStore{} - s.FileNameStr = filename - s.BrowserStr = `netscape` + return cookies.NewCookieJar(cookieStoreBasic(filename), filters...), nil +} - return &cookies.CookieJar{CookieStore: s}, nil +func cookieStoreBasic(filename string) *netscape.CookieStore { + st := &netscape.CookieStore{} + st.FileNameStr = filename + st.BrowserStr = `netscape` + return st } + +var ErrNotStrict = netscape.ErrNotStrict diff --git a/browser/netscape/netscape_test.go b/browser/netscape/netscape_test.go index 45711c6..d54a695 100644 --- a/browser/netscape/netscape_test.go +++ b/browser/netscape/netscape_test.go @@ -1,6 +1,7 @@ package netscape import ( + "context" "testing" "time" @@ -13,11 +14,12 @@ func TestReadCookies(t *testing.T) { t.Fatalf("Failed to load test data file") } - cookies, isStrict, err := ReadCookies(testCookiesPath) + seq, isStrict := TraverseCookies(testCookiesPath) + cookies, err := seq.ReadAllCookies(context.Background()) if err != nil { t.Fatal(err) } - if !isStrict { + if !isStrict() { t.Error("file not in strict netscape format") } diff --git a/browser/opera/cookies4dat.go b/browser/opera/cookies4dat.go index beccddb..50f908e 100644 --- a/browser/opera/cookies4dat.go +++ b/browser/opera/cookies4dat.go @@ -1,14 +1,16 @@ package opera import ( + "context" "encoding/binary" "errors" - "fmt" "io" "math/bits" + "strconv" "time" "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/iterx" ) type fileHeader struct { @@ -25,40 +27,56 @@ type record struct { } // "cookies4.dat" format -func (s *operaPrestoCookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cookie, error) { +func (s *operaPrestoCookieStore) TraverseCookies(filters ...kooky.Filter) kooky.CookieSeq { if s == nil { - return nil, errors.New(`cookie store is nil`) + return iterx.ErrCookieSeq(errors.New(`cookie store is nil`)) } - if err := s.Open(); err != nil { - return nil, err - } - if _, err := s.File.Seek(0, io.SeekStart); err != nil { - return nil, err - } - - var hdr fileHeader - if err := binary.Read(s.File, binary.BigEndian, &hdr); err != nil { - return nil, err - } - fileFormatVersionMajor := hdr.FileVersionNumber >> 12 - fileFormatVersionMinor := hdr.FileVersionNumber & 0xfff - if fileFormatVersionMajor != 1 || fileFormatVersionMinor != 0 { - return nil, fmt.Errorf(`unsupported file format version %d.%d`, fileFormatVersionMajor, fileFormatVersionMinor) - } - // appVersionMajor := hdr.AppVersionNumber >> 12 - // appVersionMinor := hdr.AppVersionNumber & 0xfff + return func(yield func(*kooky.Cookie, error) bool) { + if err := s.Open(); err != nil { + yield(nil, err) + return + } + if _, err := s.File.Seek(0, io.SeekStart); err != nil { + yield(nil, err) + return + } - p := &processor{ - reader: s.File, - idTagLength: hdr.IDTagLength, - lengthLength: hdr.LengthLength, - } - _, err := p.process() - if err != nil && err != io.EOF { - return nil, err + var hdr fileHeader + if err := binary.Read(s.File, binary.BigEndian, &hdr); err != nil { + yield(nil, err) + return + } + fileFormatVersionMajor := hdr.FileVersionNumber >> 12 + fileFormatVersionMinor := hdr.FileVersionNumber & 0xfff + if fileFormatVersionMajor != 1 || fileFormatVersionMinor != 0 { + yield(nil, errors.New(`unsupported file format version `+ + strconv.Itoa(int(fileFormatVersionMajor))+`.`+strconv.Itoa(int(fileFormatVersionMinor)))) + return + } + // appVersionMajor := hdr.AppVersionNumber >> 12 + // appVersionMinor := hdr.AppVersionNumber & 0xfff + + p := &processor{ + browser: s, + reader: s.File, + idTagLength: hdr.IDTagLength, + lengthLength: hdr.LengthLength, + filters: filters, + } + yld := func(cookie *kooky.Cookie, err error) bool { + return iterx.CookieFilterYield(context.Background(), cookie, err, yield, filters...) + } + _, err := p.process(yld) + if err != nil { + if !p.end && !errors.Is(err, iterx.ErrYieldEnd) && !errors.Is(err, io.EOF) { + _ = yld(nil, err) + } + return + } + if p.end || !yld(p.cookie, nil) { + return + } } - cookies := kooky.FilterCookies(p.cookies, filters...) - return cookies, nil } type processor struct { @@ -69,16 +87,23 @@ type processor struct { payloadLength uint32 domainParts []string path string - cookies []*kooky.Cookie + cookie *kooky.Cookie + filters []kooky.Filter + browser kooky.BrowserInfo + end bool } -func (p *processor) process() (int, error) { +func (p *processor) process(yield func(*kooky.Cookie, error) bool) (int, error) { if p.idTagLength < 1 || p.idTagLength > 4 || p.lengthLength < 1 || p.lengthLength > 4 { - return 0, errors.New(`unexpected byte length values`) + err := errors.New(`unexpected byte length values`) + if !yield(nil, err) { + p.end = true + } + return 0, err } n, tagID, payloadLength, err := getRecord(p.reader, p.idTagLength, p.lengthLength) - isEOF := err == io.EOF + isEOF := errors.Is(err, io.EOF) if isEOF { p.tagID = tagID p.payloadLength = payloadLength @@ -115,7 +140,12 @@ func (p *processor) process() (int, error) { } c.Domain = domain c.Path = p.path - p.cookies = append(p.cookies, c) + c.Browser = p.browser + if !yield(p.cookie, nil) { + p.end = true + return n, err + } + p.cookie = c case tagIDDomainName: p.domainParts = append(p.domainParts, string(payload)) case tagIDDomainEnd: @@ -127,31 +157,22 @@ func (p *processor) process() (int, error) { case tagIDPathStart, tagIDPathEnd: p.path = `` case tagIDCookieName: - if len(p.cookies) > 0 { - p.cookies[len(p.cookies)-1].Name = string(payload) - } + p.cookie.Name = string(payload) case tagIDCookieValue: - if len(p.cookies) > 0 { - p.cookies[len(p.cookies)-1].Value = string(payload) - } + p.cookie.Value = string(payload) case tagIDCookieDateExpiry: if len(payload) != 8 { return n, err } - if len(p.cookies) > 1 { - p.cookies[len(p.cookies)-1].Expires = time.Unix(int64(binary.BigEndian.Uint64(payload)), 0) - } + p.cookie.Expires = time.Unix(int64(binary.BigEndian.Uint64(payload)), 0) case tagIDCookieHTTPSOnly: - if len(p.cookies) > 1 { - p.cookies[len(p.cookies)-1].Secure = true - } + p.cookie.Secure = true } if !isEOF { var n3 int - n3, err = p.process() + n3, err = p.process(yield) n += n3 - } return n, err diff --git a/browser/opera/cookiestore.go b/browser/opera/cookiestore.go index ae4f4b0..c73300b 100644 --- a/browser/opera/cookiestore.go +++ b/browser/opera/cookiestore.go @@ -3,9 +3,9 @@ package opera import ( "errors" "io" - "os" "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/utils" ) type operaCookieStore struct { @@ -32,7 +32,7 @@ func (s *operaPrestoCookieStore) Open() error { return nil } - f, err := os.Open(s.FileNameStr) + f, err := utils.OpenFile(s.FileNameStr) if err != nil { return err } diff --git a/browser/opera/find.go b/browser/opera/find.go index 43b2c4f..d1de9f8 100644 --- a/browser/opera/find.go +++ b/browser/opera/find.go @@ -16,17 +16,15 @@ func init() { kooky.RegisterFinder(`opera`, &operaFinder{}) } -func (f *operaFinder) FindCookieStores() ([]kooky.CookieStore, error) { - var ret []kooky.CookieStore - - roots, err := operaPrestoRoots() - if err != nil { - return nil, err - } - for _, root := range roots { - ret = append( - ret, - &cookies.CookieJar{ +func (f *operaFinder) FindCookieStores() kooky.CookieStoreSeq { + return func(yield func(kooky.CookieStore, error) bool) { + for root, err := range operaPrestoRoots { + if err != nil { + if !yield(nil, err) { + return + } + } + st := &cookies.CookieJar{ CookieStore: &operaCookieStore{ CookieStore: &operaPrestoCookieStore{ DefaultCookieStore: cookies.DefaultCookieStore{ @@ -36,18 +34,19 @@ func (f *operaFinder) FindCookieStores() ([]kooky.CookieStore, error) { }, }, }, - }, - ) - } + } + if !yield(st, nil) { + return + } + } - roots, err = operaBlinkRoots() - if err != nil { - return nil, err - } - for _, root := range roots { - ret = append( - ret, - &cookies.CookieJar{ + for root, err := range operaBlinkRoots { + if err != nil { + if !yield(nil, err) { + return + } + } + st := &cookies.CookieJar{ CookieStore: &operaCookieStore{ CookieStore: &chrome.CookieStore{ DefaultCookieStore: cookies.DefaultCookieStore{ @@ -57,9 +56,10 @@ func (f *operaFinder) FindCookieStores() ([]kooky.CookieStore, error) { }, }, }, - }, - ) + } + if !yield(st, nil) { + return + } + } } - - return ret, nil } diff --git a/browser/opera/find_darwin.go b/browser/opera/find_darwin.go index 2009fd0..e6d900d 100644 --- a/browser/opera/find_darwin.go +++ b/browser/opera/find_darwin.go @@ -1,4 +1,5 @@ -//+build darwin +//go:build darwin +// +build darwin package opera @@ -7,26 +8,32 @@ import ( "path/filepath" ) -func operaPrestoRoots() ([]string, error) { +func operaPrestoRoots(yield func(string, error) bool) { // https://kb.digital-detective.net/display/BF/Location+of+Opera+Presto+Data home, err := os.UserHomeDir() if err != nil { - return nil, err + _ = yield(``, err) + return } // /Users/{user}/Library/Opera/ - return []string{filepath.Join(home, `Library`, `Opera`)}, nil + if !yield(filepath.Join(home, `Library`, `Opera`), nil) { + return + } } -func operaBlinkRoots() ([]string, error) { +func operaBlinkRoots(yield func(string, error) bool) { // https://kb.digital-detective.net/display/BF/Location+of+Opera+Blink+Data home, err := os.UserHomeDir() if err != nil { - return nil, err + _ = yield(``, err) + return } // /Users/{user}/Library/Application Support/com.operasoftware.Opera/ - return []string{filepath.Join(home, `Library`, `Application Support`, `com.operasoftware.Opera`)}, nil + if !yield(filepath.Join(home, `Library`, `Application Support`, `com.operasoftware.Opera`), nil) { + return + } } diff --git a/browser/opera/find_unix.go b/browser/opera/find_unix.go index f75478b..1bbe847 100644 --- a/browser/opera/find_unix.go +++ b/browser/opera/find_unix.go @@ -1,4 +1,5 @@ -//+build !windows,!darwin +//go:build !windows && !darwin +// +build !windows,!darwin package opera @@ -7,21 +8,22 @@ import ( "path/filepath" ) -func operaPrestoRoots() ([]string, error) { +func operaPrestoRoots(yield func(string, error) bool) { // https://kb.digital-detective.net/display/BF/Location+of+Opera+Presto+Data home, err := os.UserHomeDir() if err != nil { - return nil, err + _ = yield(``, err) + return } - return []string{filepath.Join(home, `.opera`)}, nil + _ = yield(filepath.Join(home, `.opera`), nil) } -func operaBlinkRoots() ([]string, error) { +func operaBlinkRoots(yield func(string, error) bool) { // https://kb.digital-detective.net/display/BF/Location+of+Opera+Blink+Data - var dotConfigs, ret []string + var dotConfigs []string // fallback if home, err := os.UserHomeDir(); err == nil { @@ -31,10 +33,8 @@ func operaBlinkRoots() ([]string, error) { dotConfigs = append(dotConfigs, dir) } for _, dotConfig := range dotConfigs { - ret = append( - ret, - filepath.Join(dotConfig, `opera`), - ) + if !yield(filepath.Join(dotConfig, `opera`), nil) { + return + } } - return ret, nil } diff --git a/browser/opera/find_windows.go b/browser/opera/find_windows.go index 7e9c672..72d85c5 100644 --- a/browser/opera/find_windows.go +++ b/browser/opera/find_windows.go @@ -1,4 +1,5 @@ -//+build windows +//go:build windows +// +build windows package opera @@ -8,43 +9,39 @@ import ( "path/filepath" ) -func operaPrestoRoots() ([]string, error) { +func operaPrestoRoots(yield func(string, error) bool) { // https://kb.digital-detective.net/display/BF/Location+of+Opera+Presto+Data appData, ok := os.LookupEnv(`AppData`) if !ok { - return nil, errors.New(`%AppData% not set`) + _ = yield(``, errors.New(`%AppData% not set`)) + return } - var ret []string pathEnds := [][]string{ - {`Opera`, `Opera`}, + {`Opera`, `Opera`}, // TODO check } for _, end := range pathEnds { - ret = append( - ret, - filepath.Join(append([]string{appData}, end...)...), - ) + if !yield(filepath.Join(append([]string{appData}, end...)...), nil) { + return + } } - return ret, nil } -func operaBlinkRoots() ([]string, error) { +func operaBlinkRoots(yield func(string, error) bool) { // Windows XP: %HOMEPATH%\Application Data\Opera Software\Opera Stable\ // Windows 7, 8: %AppData%\Opera Software\Opera Stable\ // https://kb.digital-detective.net/display/BF/Location+of+Opera+Blink+Data appData, ok := os.LookupEnv(`AppData`) if !ok { - return nil, errors.New(`%AppData% not set`) + _ = yield(``, errors.New(`%AppData% not set`)) + return } - var ret []string pathEnds := [][]string{ {`Opera Software`, `Opera Stable`}, } for _, end := range pathEnds { - ret = append( - ret, - filepath.Join(append([]string{appData}, end...)...), - ) + if !yield(filepath.Join(append([]string{appData}, end...)...), nil) { + return + } } - return ret, nil } diff --git a/browser/opera/opera.go b/browser/opera/opera.go index 1951de7..418fca4 100644 --- a/browser/opera/opera.go +++ b/browser/opera/opera.go @@ -2,44 +2,23 @@ package opera import ( "errors" - "net/http" "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/chrome" "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/iterx" "github.com/xiazemin/kooky/internal/utils" ) -func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { - s, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer s.Close() - - return s.ReadCookies(filters...) +func TraverseCookies(filename string, filters ...kooky.Filter) kooky.CookieSeq { + return cookies.SingleRead(cookieStore, filename, filters...) } -func (s *operaCookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cookie, error) { +func (s *operaCookieStore) TraverseCookies(filters ...kooky.Filter) kooky.CookieSeq { if s == nil { - return nil, errors.New(`cookie store is nil`) - } - return s.CookieStore.ReadCookies(filters...) -} - -// CookieJar returns an initiated http.CookieJar based on the cookies stored by -// the Opera browser. Set cookies are memory stored and do not modify any -// browser files. -func CookieJar(filename string, filters ...kooky.Filter) (http.CookieJar, error) { - j, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer j.Close() - if err := j.InitJar(); err != nil { - return nil, err + return iterx.ErrCookieSeq(errors.New(`cookie store is nil`)) } - return j, nil + return s.CookieStore.TraverseCookies(filters...) } // CookieStore has to be closed with CookieStore.Close() after use. @@ -48,7 +27,7 @@ func CookieStore(filename string, filters ...kooky.Filter) (kooky.CookieStore, e } func cookieStore(filename string, filters ...kooky.Filter) (*cookies.CookieJar, error) { - var s operaCookieStore + s := &operaCookieStore{} f, typ, err := utils.DetectFileType(filename) if err != nil { @@ -79,5 +58,5 @@ func cookieStore(filename string, filters ...kooky.Filter) (*cookies.CookieJar, return nil, errors.New(`unknown file type`) } - return &cookies.CookieJar{CookieStore: &s}, nil + return cookies.NewCookieJar(s, filters...), nil } diff --git a/browser/safari/find.go b/browser/safari/find.go index 56899d9..255dbe2 100644 --- a/browser/safari/find.go +++ b/browser/safari/find.go @@ -15,23 +15,27 @@ func init() { kooky.RegisterFinder(`safari`, &safariFinder{}) } -func (f *safariFinder) FindCookieStores() ([]kooky.CookieStore, error) { - fileStr, err := cookieFile() - if err != nil { - return nil, err - } - - var ret = []kooky.CookieStore{ - &cookies.CookieJar{ - CookieStore: &safariCookieStore{ - DefaultCookieStore: cookies.DefaultCookieStore{ - BrowserStr: `safari`, - IsDefaultProfileBool: true, - FileNameStr: fileStr, +func (f *safariFinder) FindCookieStores() kooky.CookieStoreSeq { + return func(yield func(kooky.CookieStore, error) bool) { + fileStrs, err := cookieFiles() + if err != nil { + _ = yield(nil, err) + return + } + + for i, fileStr := range fileStrs { + st := &cookies.CookieJar{ + CookieStore: &safariCookieStore{ + DefaultCookieStore: cookies.DefaultCookieStore{ + BrowserStr: `safari`, + IsDefaultProfileBool: i == 0, + FileNameStr: fileStr, + }, }, - }, - }, + } + if !yield(st, nil) { + return + } + } } - - return ret, nil } diff --git a/browser/safari/find_darwin.go b/browser/safari/find_darwin.go index 32e666f..a0e6fa0 100644 --- a/browser/safari/find_darwin.go +++ b/browser/safari/find_darwin.go @@ -7,10 +7,16 @@ import ( "path/filepath" ) -func cookieFile() (string, error) { +func cookieFiles() ([]string, error) { home, err := os.UserHomeDir() if err != nil { - return ``, err + return nil, err } - return filepath.Join(home, `Library`, `Cookies`, `Cookies.binarycookies`), nil + paths := []string{ + // ~/Library/Containers/com.apple.Safari/Data/Library/Cookies + filepath.Join(home, `Library`, `Containers`, `com.apple.Safari`, `Data`, `Library`, `Cookies`, `Cookies.binarycookies`), + filepath.Join(home, `Library`, `Cookies`, `Cookies.binarycookies`), + } + + return paths, nil } diff --git a/browser/safari/find_windows.go b/browser/safari/find_windows.go index 0d5d190..f616ecd 100644 --- a/browser/safari/find_windows.go +++ b/browser/safari/find_windows.go @@ -9,10 +9,10 @@ import ( "path/filepath" ) -func cookieFile() (string, error) { +func cookieFiles() ([]string, error) { confDir, err := os.UserConfigDir() if err != nil { - return ``, err + return nil, err } - return filepath.Join(confDir, `Apple Computer`, `Safari`, `Cookies`, `Cookies.binarycookies`), nil + return []string{filepath.Join(confDir, `Apple Computer`, `Safari`, `Cookies`, `Cookies.binarycookies`)}, nil } diff --git a/browser/safari/safari.go b/browser/safari/safari.go index 6ce7c70..0680af4 100644 --- a/browser/safari/safari.go +++ b/browser/safari/safari.go @@ -6,14 +6,15 @@ package safari import ( "bufio" "bytes" + "context" "encoding/binary" "errors" "fmt" "io" - "net/http" "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/iterx" "github.com/xiazemin/kooky/internal/timex" ) @@ -47,95 +48,106 @@ type safariCookieStore struct { var _ cookies.CookieStore = (*safariCookieStore)(nil) -func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { - s, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer s.Close() +func ReadCookies(ctx context.Context, filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { + return cookies.SingleRead(cookieStore, filename, filters...).ReadAllCookies(ctx) +} - return s.ReadCookies(filters...) +func TraverseCookies(filename string, filters ...kooky.Filter) kooky.CookieSeq { + return cookies.SingleRead(cookieStore, filename, filters...) } -func (s *safariCookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cookie, error) { - if s == nil { - return nil, errors.New(`cookie store is nil`) - } - if err := s.Open(); err != nil { - return nil, err - } else if s.File == nil { - return nil, errors.New(`file is nil`) - } +func (s *safariCookieStore) TraverseCookies(filters ...kooky.Filter) kooky.CookieSeq { + return func(yield func(*kooky.Cookie, error) bool) { + if s == nil { + yield(nil, errors.New(`cookie store is nil`)) + return + } + if err := s.Open(); err != nil { + yield(nil, err) + return + } + if s.File == nil { + yield(nil, errors.New(`file is nil`)) + return + } - var allCookies []*kooky.Cookie + var header fileHeader + err := binary.Read(s.File, binary.BigEndian, &header) + if err != nil { + yield(nil, fmt.Errorf("error reading header: %v", err)) + return + } + if string(header.Magic[:]) != "cook" { + yield(nil, fmt.Errorf("expected first 4 bytes to be %q; got %q", "cook", string(header.Magic[:]))) + return + } - var header fileHeader - err := binary.Read(s.File, binary.BigEndian, &header) - if err != nil { - return nil, fmt.Errorf("error reading header: %v", err) - } - if string(header.Magic[:]) != "cook" { - return nil, fmt.Errorf("expected first 4 bytes to be %q; got %q", "cook", string(header.Magic[:])) - } + pageSizes := make([]int32, header.NumPages) + if err = binary.Read(s.File, binary.BigEndian, &pageSizes); err != nil { + yield(nil, fmt.Errorf("error reading page sizes: %w", err)) + return + } - pageSizes := make([]int32, header.NumPages) - if err = binary.Read(s.File, binary.BigEndian, &pageSizes); err != nil { - return nil, fmt.Errorf("error reading page sizes: %v", err) - } + // read cookies + for i, pageSize := range pageSizes { + if !s.readPage(s.File, i, pageSize, yield, filters...) { + return + } + } - for i, pageSize := range pageSizes { - if allCookies, err = readPage(s.File, pageSize, allCookies); err != nil { - return nil, fmt.Errorf("error reading page %d: %v", i, err) + // TODO(zellyn): figure out how the checksum works. + var checksum [8]byte + err = binary.Read(s.File, binary.BigEndian, &checksum) + if err != nil { + yield(nil, fmt.Errorf("error reading checksum: %w", err)) + return } } +} - // TODO(zellyn): figure out how the checksum works. - var checksum [8]byte - err = binary.Read(s.File, binary.BigEndian, &checksum) - if err != nil { - return nil, fmt.Errorf("error reading checksum: %v", err) +func (s *safariCookieStore) readPage(f io.Reader, page int, pageSize int32, yield func(*kooky.Cookie, error) bool, filters ...kooky.Filter) bool { + yld := func(c *kooky.Cookie, e error) bool { + if e != nil { + e = fmt.Errorf("error reading page %d: %w", page, e) + } + return iterx.CookieFilterYield(context.Background(), c, e, yield, filters...) } - // Filter cookies by specified filters. - cookies := kooky.FilterCookies(allCookies, filters...) - - return cookies, nil -} - -func readPage(f io.Reader, pageSize int32, cookies []*kooky.Cookie) ([]*kooky.Cookie, error) { bb := make([]byte, pageSize) if _, err := io.ReadFull(f, bb); err != nil { - return nil, err + return yld(nil, err) } r := bytes.NewReader(bb) var header pageHeader if err := binary.Read(r, binary.LittleEndian, &header); err != nil { - return nil, fmt.Errorf("error reading header: %v", err) + return yld(nil, fmt.Errorf("error reading header: %w", err)) } want := [4]byte{0x00, 0x00, 0x01, 0x00} if header.Header != want { - return nil, fmt.Errorf("expected first 4 bytes of page to be %v; got %v", want, header.Header) + return yld(nil, fmt.Errorf("expected first 4 bytes of page to be %v; got %v", want, header.Header)) } cookieOffsets := make([]int32, header.NumCookies) if err := binary.Read(r, binary.LittleEndian, &cookieOffsets); err != nil { - return nil, fmt.Errorf("error reading cookie offsets: %v", err) + return yld(nil, fmt.Errorf("error reading cookie offsets: %w", err)) } for i, cookieOffset := range cookieOffsets { r.Seek(int64(cookieOffset), io.SeekStart) - cookie, err := readCookie(r) + cookie, err := s.readCookie(r) if err != nil { - return nil, fmt.Errorf("cookie %d: %v", i, err) + return yld(nil, fmt.Errorf("cookie %d: %w", i, err)) + } + if !yld(cookie, nil) { + return false } - cookies = append(cookies, cookie) } - return cookies, nil + return true } -func readCookie(r io.ReadSeeker) (*kooky.Cookie, error) { +func (s *safariCookieStore) readCookie(r io.ReadSeeker) (*kooky.Cookie, error) { start, _ := r.Seek(0, io.SeekCurrent) var ch cookieHeader if err := binary.Read(r, binary.LittleEndian, &ch); err != nil { @@ -145,25 +157,24 @@ func readCookie(r io.ReadSeeker) (*kooky.Cookie, error) { expiry := timex.FromSafariTime(ch.ExpirationDate) creation := timex.FromSafariTime(ch.CreationDate) - url, err := readString(r, "url", start, ch.UrlOffset) + url, err := s.readString(r, "url", start, ch.UrlOffset) if err != nil { return nil, err } - name, err := readString(r, "name", start, ch.NameOffset) + name, err := s.readString(r, "name", start, ch.NameOffset) if err != nil { return nil, err } - path, err := readString(r, "path", start, ch.PathOffset) + path, err := s.readString(r, "path", start, ch.PathOffset) if err != nil { return nil, err } - value, err := readString(r, "value", start, ch.ValueOffset) + value, err := s.readString(r, "value", start, ch.ValueOffset) if err != nil { return nil, err } cookie := &kooky.Cookie{} - cookie.Expires = expiry cookie.Creation = creation cookie.Name = name @@ -172,10 +183,12 @@ func readCookie(r io.ReadSeeker) (*kooky.Cookie, error) { cookie.Path = path cookie.Secure = (ch.Flags & 1) > 0 cookie.HttpOnly = (ch.Flags & 4) > 0 + cookie.Browser = s + return cookie, nil } -func readString(r io.ReadSeeker, field string, start int64, offset int32) (string, error) { +func (s *safariCookieStore) readString(r io.ReadSeeker, field string, start int64, offset int32) (string, error) { if _, err := r.Seek(start+int64(offset), io.SeekStart); err != nil { return "", fmt.Errorf("seeking for %q at offset %d", field, offset) } @@ -188,21 +201,6 @@ func readString(r io.ReadSeeker, field string, start int64, offset int32) (strin return value[:len(value)-1], nil } -// CookieJar returns an initiated http.CookieJar based on the cookies stored by -// the Safari browser. Set cookies are memory stored and do not modify any -// browser files. -func CookieJar(filename string, filters ...kooky.Filter) (http.CookieJar, error) { - j, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer j.Close() - if err := j.InitJar(); err != nil { - return nil, err - } - return j, nil -} - // CookieStore has to be closed with CookieStore.Close() after use. func CookieStore(filename string, filters ...kooky.Filter) (kooky.CookieStore, error) { return cookieStore(filename, filters...) @@ -213,5 +211,5 @@ func cookieStore(filename string, filters ...kooky.Filter) (*cookies.CookieJar, s.FileNameStr = filename s.BrowserStr = `safari` - return &cookies.CookieJar{CookieStore: s}, nil + return cookies.NewCookieJar(s, filters...), nil } diff --git a/browser/safari/safari_test.go b/browser/safari/safari_test.go index b17b513..0fb1cce 100644 --- a/browser/safari/safari_test.go +++ b/browser/safari/safari_test.go @@ -1,6 +1,7 @@ package safari import ( + "context" "testing" "time" @@ -15,14 +16,15 @@ func TestReadCookies(t *testing.T) { t.Fatalf("Failed to load test data file") } - cookies, err := ReadCookies(testCookiesPath) + ctx := context.Background() + cookies, err := TraverseCookies(testCookiesPath).ReadAllCookies(ctx) if err != nil { t.Fatal(err) } domain := "news.ycombinator.com" name := "user" - cookies = kooky.FilterCookies(cookies, kooky.Domain(domain), kooky.Name(name)) + cookies = kooky.FilterCookies(ctx, cookies, kooky.Domain(domain), kooky.Name(name)).Collect(ctx) if len(cookies) == 0 { t.Fatalf("Found no cookies with domain=%q, name=%q", domain, name) } diff --git a/browser/uzbl/find.go b/browser/uzbl/find.go index 19829e3..5205062 100644 --- a/browser/uzbl/find.go +++ b/browser/uzbl/find.go @@ -4,6 +4,7 @@ package uzbl import ( + "iter" "os" "path/filepath" @@ -20,57 +21,65 @@ func init() { kooky.RegisterFinder(`uzbl`, &uzblFinder{}) } -func (f *uzblFinder) FindCookieStores() ([]kooky.CookieStore, error) { - roots, err := uzblRoots() - if err != nil { - return nil, err - } - files := []string{`session-cookies.txt`, `cookies.txt`} +// TODO default profile + +func (f *uzblFinder) FindCookieStores() kooky.CookieStoreSeq { + return func(yield func(kooky.CookieStore, error) bool) { + files := []string{`session-cookies.txt`, `cookies.txt`} - var ret []kooky.CookieStore - lastRoot := len(roots) - 1 - lastFile := len(files) - 1 - for i, root := range roots { - for j, filename := range files { - ret = append( - ret, - &cookies.CookieJar{ + for root, err := range uzblRoots() { + if err != nil && !yield(nil, err) { + return + } + for _, filename := range files { + st := &cookies.CookieJar{ CookieStore: &netscape.CookieStore{ DefaultCookieStore: cookies.DefaultCookieStore{ - BrowserStr: `uzbl`, - IsDefaultProfileBool: i == lastRoot && j == lastFile, - FileNameStr: filepath.Join(root, `uzbl`, filename), + BrowserStr: `uzbl`, + FileNameStr: filepath.Join(root, `uzbl`, filename), }, }, - }, - ) + } + if !yield(st, nil) { + return + } + } } } - - return ret, nil } -func uzblRoots() ([]string, error) { - var ret []string - home, errHome := os.UserHomeDir() +func uzblRoots() iter.Seq2[string, error] { + return func(yield func(string, error) bool) { + home, errHome := os.UserHomeDir() - // old location - // fallback - if errHome == nil { - ret = append(ret, filepath.Join(home, `.config`)) - } - if dir, ok := os.LookupEnv(`XDG_CONFIG_HOME`); ok { - ret = append(ret, dir) - } + // old location + // fallback + if errHome != nil { + if !yield(``, errHome) { + return + } + } else { + if !yield(filepath.Join(home, `.config`), nil) { + return + } + } + if dir, ok := os.LookupEnv(`XDG_CONFIG_HOME`); ok { + if !yield(dir, nil) { + return + } + } - // new location - if errHome == nil { - ret = append(ret, filepath.Join(home, `.local`, `share`)) - } + // new location + if errHome == nil { + if !yield(filepath.Join(home, `.local`, `share`), nil) { + return + } + } - if dataDir, ok := os.LookupEnv(`XDG_DATA_HOME`); ok { - ret = append(ret, dataDir) + if dataDir, ok := os.LookupEnv(`XDG_DATA_HOME`); ok { + if !yield(dataDir, nil) { + return + } + } } - - return ret, nil } diff --git a/browser/uzbl/uzbl.go b/browser/uzbl/uzbl.go index cecab3e..3dff6c5 100644 --- a/browser/uzbl/uzbl.go +++ b/browser/uzbl/uzbl.go @@ -1,36 +1,19 @@ package uzbl import ( - "net/http" + "context" "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/cookies" "github.com/xiazemin/kooky/internal/netscape" ) -func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { - s, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer s.Close() - - return s.ReadCookies(filters...) +func ReadCookies(ctx context.Context, filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { + return cookies.SingleRead(cookieStore, filename, filters...).ReadAllCookies(ctx) } -// CookieJar returns an initiated http.CookieJar based on the cookies stored by -// the uzbl browser. Set cookies are memory stored and do not modify any -// browser files. -func CookieJar(filename string, filters ...kooky.Filter) (http.CookieJar, error) { - j, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer j.Close() - if err := j.InitJar(); err != nil { - return nil, err - } - return j, nil +func TraverseCookies(filename string, filters ...kooky.Filter) kooky.CookieSeq { + return cookies.SingleRead(cookieStore, filename, filters...) } // CookieStore has to be closed with CookieStore.Close() after use. @@ -43,5 +26,5 @@ func cookieStore(filename string, filters ...kooky.Filter) (*cookies.CookieJar, s.FileNameStr = filename s.BrowserStr = `uzbl` - return &cookies.CookieJar{CookieStore: s}, nil + return cookies.NewCookieJar(s, filters...), nil } diff --git a/browser/uzbl/uzbl_test.go b/browser/uzbl/uzbl_test.go index f2b9fd5..abcc563 100644 --- a/browser/uzbl/uzbl_test.go +++ b/browser/uzbl/uzbl_test.go @@ -1,9 +1,12 @@ package uzbl import ( + "context" + "errors" "testing" "time" + "github.com/xiazemin/kooky/browser/netscape" "github.com/xiazemin/kooky/internal/testutils" ) @@ -12,8 +15,8 @@ func TestReadCookies(t *testing.T) { if err != nil { t.Fatalf("Failed to load test data file") } - cookies, err := ReadCookies(testCookiesPath) - if err != nil { + cookies, err := TraverseCookies(testCookiesPath).ReadAllCookies(context.Background()) + if err != nil && !errors.Is(err, netscape.ErrNotStrict) { t.Fatal(err) } if len(cookies) != 2 { @@ -51,8 +54,8 @@ func TestReadCookies(t *testing.T) { if err != nil { t.Fatalf("Failed to load test data file") } - cookies, err = ReadCookies(testCookiesPath) - if err != nil { + cookies, err = TraverseCookies(testCookiesPath).ReadAllCookies(context.Background()) + if err != nil && !errors.Is(err, netscape.ErrNotStrict) { t.Fatal(err) } if len(cookies) != 1 { diff --git a/browser/w3m/find.go b/browser/w3m/find.go index beb72a7..18c7cc2 100644 --- a/browser/w3m/find.go +++ b/browser/w3m/find.go @@ -18,14 +18,15 @@ func init() { kooky.RegisterFinder(`w3m`, &w3mFinder{}) } -func (f *w3mFinder) FindCookieStores() ([]kooky.CookieStore, error) { - home, err := os.UserHomeDir() - if err != nil { - return nil, err - } - - var ret = []kooky.CookieStore{ - &cookies.CookieJar{ +func (f *w3mFinder) FindCookieStores() kooky.CookieStoreSeq { + return func(yield func(kooky.CookieStore, error) bool) { + home, err := os.UserHomeDir() + if err != nil { + _ = yield(nil, err) + return + } + + st := &cookies.CookieJar{ CookieStore: &w3mCookieStore{ DefaultCookieStore: cookies.DefaultCookieStore{ BrowserStr: `w3m`, @@ -33,8 +34,9 @@ func (f *w3mFinder) FindCookieStores() ([]kooky.CookieStore, error) { FileNameStr: filepath.Join(home, `.w3m`, `cookie`), }, }, - }, + } + if !yield(st, nil) { + return + } } - - return ret, nil } diff --git a/browser/w3m/w3m.go b/browser/w3m/w3m.go index f49ee70..ebd8497 100644 --- a/browser/w3m/w3m.go +++ b/browser/w3m/w3m.go @@ -2,14 +2,15 @@ package w3m import ( "bufio" + "context" "errors" - "net/http" "strconv" "strings" "time" "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/iterx" ) type w3mCookieStore struct { @@ -18,89 +19,70 @@ type w3mCookieStore struct { var _ cookies.CookieStore = (*w3mCookieStore)(nil) -func ReadCookies(filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { - s, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer s.Close() +func ReadCookies(ctx context.Context, filename string, filters ...kooky.Filter) ([]*kooky.Cookie, error) { + return cookies.SingleRead(cookieStore, filename, filters...).ReadAllCookies(ctx) +} - return s.ReadCookies(filters...) +func TraverseCookies(filename string, filters ...kooky.Filter) kooky.CookieSeq { + return cookies.SingleRead(cookieStore, filename, filters...) } -func (s *w3mCookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cookie, error) { +func (s *w3mCookieStore) TraverseCookies(filters ...kooky.Filter) kooky.CookieSeq { // cookie.c: void save_cookies(void){} // https://github.com/tats/w3m/blob/169789b1480710712d587d5859fab9d93eb952a2/cookie.c#L429 if s == nil { - return nil, errors.New(`cookie store is nil`) + return iterx.ErrCookieSeq(errors.New(`cookie store is nil`)) } if err := s.Open(); err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } else if s.File == nil { - return nil, errors.New(`file is nil`) + return iterx.ErrCookieSeq(errors.New(`file is nil`)) } - var ret []*kooky.Cookie - - scanner := bufio.NewScanner(s.File) - for scanner.Scan() { - // split line into fields - sp := strings.Split(scanner.Text(), "\t") - if len(sp) != 11 { - continue - } - exp, err := strconv.ParseInt(sp[3], 10, 64) - if err != nil { - continue - } - bitFlag, err := strconv.Atoi(sp[6]) - if err != nil { - continue - } - - // #defined in "fm.h" - const ( - // cooUse int = 1 // COO_USE - cooSecure int = 2 // COO_SECURE - // cooDomain int = 4 // COO_DOMAIN - // cooPath int = 8 // COO_PATH - // cooDiscard int = 16 // COO_DISCARD - // cooOverride int = 32 // COO_OVERRIDE - user override of security checks - ) - - cookie := &kooky.Cookie{} - cookie.Name = sp[1] - cookie.Value = sp[2] - cookie.Path = sp[5] - cookie.Domain = sp[4] - cookie.Expires = time.Unix(exp, 0) - cookie.Secure = bitFlag&cooSecure != 0 - // sp[6] // state management specification version - // sp[7] // port list - - if !kooky.FilterCookie(cookie, filters...) { - continue + return func(yield func(*kooky.Cookie, error) bool) { + scanner := bufio.NewScanner(s.File) + for scanner.Scan() { + // split line into fields + sp := strings.Split(scanner.Text(), "\t") + if len(sp) != 11 { + continue + } + exp, err := strconv.ParseInt(sp[3], 10, 64) + if err != nil { + continue + } + bitFlag, err := strconv.Atoi(sp[6]) + if err != nil { + continue + } + + // #defined in "fm.h" + const ( + // cooUse int = 1 // COO_USE + cooSecure int = 2 // COO_SECURE + // cooDomain int = 4 // COO_DOMAIN + // cooPath int = 8 // COO_PATH + // cooDiscard int = 16 // COO_DISCARD + // cooOverride int = 32 // COO_OVERRIDE - user override of security checks + ) + + cookie := &kooky.Cookie{} + cookie.Name = sp[1] + cookie.Value = sp[2] + cookie.Path = sp[5] + cookie.Domain = sp[4] + cookie.Expires = time.Unix(exp, 0) + cookie.Secure = bitFlag&cooSecure != 0 + // sp[6] // state management specification version + // sp[7] // port list + cookie.Browser = s + + if !iterx.CookieFilterYield(context.Background(), cookie, nil, yield, filters...) { + return + } } - - ret = append(ret, cookie) - } - return ret, nil -} - -// CookieJar returns an initiated http.CookieJar based on the cookies stored by -// the w3m browser. Set cookies are memory stored and do not modify any -// browser files. -func CookieJar(filename string, filters ...kooky.Filter) (http.CookieJar, error) { - j, err := cookieStore(filename, filters...) - if err != nil { - return nil, err - } - defer j.Close() - if err := j.InitJar(); err != nil { - return nil, err } - return j, nil } // CookieStore has to be closed with CookieStore.Close() after use. @@ -113,7 +95,7 @@ func cookieStore(filename string, filters ...kooky.Filter) (*cookies.CookieJar, s.FileNameStr = filename s.BrowserStr = `w3m` - return &cookies.CookieJar{CookieStore: s}, nil + return cookies.NewCookieJar(s, filters...), nil } // TODO: diff --git a/browser/w3m/w3m_test.go b/browser/w3m/w3m_test.go index 34e9d02..1d6abd9 100644 --- a/browser/w3m/w3m_test.go +++ b/browser/w3m/w3m_test.go @@ -1,6 +1,7 @@ package w3m import ( + "context" "testing" "time" @@ -13,7 +14,7 @@ func TestReadCookies(t *testing.T) { t.Fatalf("Failed to load test data file") } - cookies, err := ReadCookies(testCookiesPath) + cookies, err := TraverseCookies(testCookiesPath).ReadAllCookies(context.Background()) if err != nil { t.Fatal(err) } diff --git a/chrome_example_test.go b/chrome_example_test.go index de01ddc..4615e47 100644 --- a/chrome_example_test.go +++ b/chrome_example_test.go @@ -17,13 +17,7 @@ func Example_chromeSimpleMacOS() { // read the cookies from the file // decryption is handled automatically - cookies, err := chrome.ReadCookies(cookieStoreFile) - if err != nil { - // TODO: handle the error - return - } - - for _, cookie := range cookies { + for cookie := range chrome.TraverseCookies(cookieStoreFile).OnlyCookies() { fmt.Println(cookie) } } diff --git a/cmd/kooky/kooky.go b/cmd/kooky/kooky.go index 93b211b..dfb041f 100644 --- a/cmd/kooky/kooky.go +++ b/cmd/kooky/kooky.go @@ -1,15 +1,18 @@ package main import ( + "context" + "encoding/json" "fmt" "io" "log" "os" + "os/signal" "strings" "text/tabwriter" - "github.com/xiazemin/kooky" - _ "github.com/xiazemin/kooky/browser/all" + "github.com/browserutils/kooky" + _ "github.com/browserutils/kooky/browser/all" "github.com/spf13/pflag" ) @@ -22,15 +25,30 @@ func main() { domain := pflag.StringP(`domain`, `d`, ``, `cookie domain filter (partial)`) name := pflag.StringP(`name`, `n`, ``, `cookie name filter (exact)`) export := pflag.StringP(`export`, `o`, ``, `export cookies in netscape format`) + jsonFormat := pflag.BoolP(`jsonl`, `j`, false, `JSON Lines output format`) pflag.Parse() - cookieStores := kooky.FindAllCookieStores() + // cookie filters + filters := []kooky.Filter{storeFilter(browser, profile, defaultProfile)} + if showExpired == nil || !*showExpired { + filters = append(filters, kooky.Valid) + } + if domain != nil && len(*domain) > 0 { + filters = append(filters, kooky.DomainContains(*domain)) + } + if name != nil && len(*name) > 0 { + filters = append(filters, kooky.Name(*name)) + } - var cookiesExport []*kooky.Cookie // for netscape export + ctx, cancel := context.WithCancel(context.Background()) + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { <-c; cancel() }() + + seq := kooky.TraverseCookies(ctx, filters...) - var f io.Writer // for netscape export - var w *tabwriter.Writer // for printing if export != nil && len(*export) > 0 { + var f io.Writer // for netscape export if *export == `-` { f = os.Stdout } else { @@ -41,73 +59,88 @@ func main() { defer fl.Close() f = fl } - } else { - w = tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) + seq.Export(ctx, f) + return } - trimLen := 45 - for _, store := range cookieStores { - defer store.Close() - // cookie store filters - if browser != nil && len(*browser) > 0 && store.Browser() != *browser { - continue - } - if profile != nil && len(*profile) > 0 && store.Profile() != *profile { - continue - } - if defaultProfile != nil && *defaultProfile && !store.IsDefaultProfile() { - continue - } + w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) // for printing - // cookie filters - var filters []kooky.Filter - if showExpired == nil || !*showExpired { - filters = append(filters, kooky.Valid) - } - if domain != nil && len(*domain) > 0 { - filters = append(filters, kooky.DomainContains(*domain)) - } - if name != nil && len(*name) > 0 { - filters = append(filters, kooky.Name(*name)) + trimLen := 45 + // use channel so that tabwriter won't panic + for cookie := range seq.Chan(ctx) { + if jsonFormat != nil && *jsonFormat { + b, err := json.Marshal(cookie) + if err != nil { + log.Fatalln(err) + } + fmt.Fprintf(w, "%s\n", b) + } else { + prCookieLine(w, cookie, trimLen) } + } +} - cookies, _ := store.ReadCookies(filters...) - /*fmt.Println(store.FilePath()) // TODO rm - cookies, err := store.ReadCookies(filters...) - if err != nil { - fmt.Println(err) - }*/ - // continue // TODO rm +func prCookieLine(w io.Writer, cookie *kooky.Cookie, trimLen int) { + if cookie == nil { + return + } + container := cookie.Container + if len(container) > 0 { + container = ` [` + container + `]` + } + fmt.Fprintf( + w, + "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + prBrowser(cookie), + prProfile(cookie), + container, + trimStr(prFilePath(cookie), trimLen), + trimStr(cookie.Domain, trimLen), + trimStr(cookie.Name, trimLen), + // be careful about raw bytes + trimStr(strings.Trim(fmt.Sprintf(`%q`, cookie.Value), `"`), trimLen), + cookie.Expires.Format(`2006.01.02 15:04:05`), + ) +} - if export != nil && len(*export) > 0 { - cookiesExport = append(cookiesExport, cookies...) - } else { - for _, cookie := range cookies { - container := cookie.Container - if len(container) > 0 { - container = ` [` + container + `]` - } - fmt.Fprintf( - w, - "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", - store.Browser(), - store.Profile(), - container, - trimStr(store.FilePath(), trimLen), - trimStr(cookie.Domain, trimLen), - trimStr(cookie.Name, trimLen), - // be careful about raw bytes - trimStr(strings.Trim(fmt.Sprintf(`%q`, cookie.Value), `"`), trimLen), - cookie.Expires.Format(`2006.01.02 15:04:05`), - ) - } - } +func prBrowser(c *kooky.Cookie) string { + if c == nil || c.Browser == nil { + return `` } - if export != nil && len(*export) > 0 { - kooky.ExportCookies(f, cookiesExport) - } else { - w.Flush() + return c.Browser.Browser() +} + +func prProfile(c *kooky.Cookie) string { + if c == nil || c.Browser == nil { + return `` + } + return c.Browser.Profile() +} + +func prFilePath(c *kooky.Cookie) string { + if c == nil || c.Browser == nil { + return `` } + return c.Browser.FilePath() +} + +func storeFilter(browser, profile *string, defaultProfile *bool) kooky.Filter { + return kooky.FilterFunc(func(cookie *kooky.Cookie) bool { + if cookie == nil || cookie.Browser == nil { + return false + } + // cookie store filters + if browser != nil && len(*browser) > 0 && cookie.Browser.Browser() != *browser { + return false + } + if profile != nil && len(*profile) > 0 && cookie.Browser.Profile() != *profile { + return false + } + if defaultProfile != nil && *defaultProfile && !cookie.Browser.IsDefaultProfile() { + return false + } + return true + }) } func trimStr(str string, length int) string { diff --git a/cookiejar_example_test.go b/cookiejar_example_test.go index 2260f8a..8f3ff9d 100644 --- a/cookiejar_example_test.go +++ b/cookiejar_example_test.go @@ -1,6 +1,7 @@ package kooky_test import ( + "context" "fmt" "io" "log" @@ -13,7 +14,8 @@ import ( ) func Example_cookieJar() { - stores := kooky.FindAllCookieStores() + ctx := context.TODO() + stores := kooky.FindAllCookieStores(ctx) var s kooky.CookieStore for _, store := range stores { if store.Browser() != `firefox` || !store.IsDefaultProfile() { @@ -24,15 +26,14 @@ func Example_cookieJar() { } // jar := s // only store cookies relevant for the target website in the cookie jar - jar, _ := s.SubJar(kooky.Domain(`github.com`)) + jar, _ := s.SubJar(ctx, kooky.FilterFunc(func(c *kooky.Cookie) bool { + return kooky.Domain(`github.com`).Filter(c) || kooky.Domain(`.github.com`).Filter(c) + })) u, _ := url.Parse(`https://github.com/settings/profile`) - var loggedIn bool - cookies := kooky.FilterCookies(jar.Cookies(u), kooky.Name(`logged_in`)) - if len(cookies) > 0 { - loggedIn = true - } - if !loggedIn { + + cookies := kooky.FilterCookies(ctx, jar.Cookies(u), kooky.Name(`logged_in`)).Collect(ctx) + if len(cookies) == 0 { log.Fatal(`not logged in`) } diff --git a/cookiestores_example_test.go b/cookiestores_example_test.go index 61560b2..4c7dd8e 100644 --- a/cookiestores_example_test.go +++ b/cookiestores_example_test.go @@ -1,6 +1,7 @@ package kooky_test import ( + "context" "fmt" "github.com/xiazemin/kooky" @@ -8,7 +9,8 @@ import ( ) func ExampleFindAllCookieStores() { - cookieStores := kooky.FindAllCookieStores() + ctx := context.TODO() + cookieStores := kooky.FindAllCookieStores(ctx) for _, store := range cookieStores { // CookieStore keeps files/databases open for repeated reads @@ -19,8 +21,7 @@ func ExampleFindAllCookieStores() { kooky.Valid, // remove expired cookies } - cookies, _ := store.ReadCookies(filters...) - for _, cookie := range cookies { + for cookie := range store.TraverseCookies(filters...).OnlyCookies() { fmt.Printf( "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", store.Browser(), diff --git a/export.go b/export.go index a799c36..6269b1d 100644 --- a/export.go +++ b/export.go @@ -1,70 +1,109 @@ package kooky import ( + "context" "fmt" "io" "net/http" "strings" ) -const httpOnlyPrefix = `#HttpOnly_` +const ( + httpOnlyPrefix = `#HttpOnly_` + netscapeHeader = "# HTTP Cookie File\n\n" +) // ExportCookies() export "cookies" in the Netscape format. // // curl, wget, ... use this format. -func ExportCookies[T Cookie | http.Cookie](w io.Writer, cookies []*T) { +func ExportCookies[S CookieSeq | []*Cookie | []*http.Cookie](ctx context.Context, w io.Writer, cookies S) { + switch cookiesTyped := any(cookies).(type) { + case CookieSeq: + exportCookieSeq(ctx, w, cookiesTyped) + case []*Cookie: + exportCookieSlice(ctx, w, cookiesTyped) + case []*http.Cookie: + exportCookieSlice(ctx, w, cookiesTyped) + } +} + +func exportCookie(w io.Writer, cookie *http.Cookie) { + var domain string + if cookie.HttpOnly { + domain = httpOnlyPrefix + } + domain += cookie.Domain + + fmt.Fprintf( + w, + "%s\t%s\t%s\t%s\t%d\t%s\t%s\n", + domain, + netscapeBool(strings.HasPrefix(cookie.Domain, `.`)), + cookie.Path, + netscapeBool(cookie.Secure), + cookie.Expires.Unix(), + cookie.Name, + cookie.Value, + ) +} + +func exportCookieSlice[S []*T, T Cookie | http.Cookie](ctx context.Context, w io.Writer, cookies S) { if len(cookies) < 1 { return } var j int for i, cookie := range cookies { + select { + case <-ctx.Done(): + return + default: + } if cookie == nil { continue } - fmt.Fprint(w, "# HTTP Cookie File\n\n") + fmt.Fprint(w, netscapeHeader) j = i break } - writeCookie := func(w io.Writer, cookie *http.Cookie) { - var domain string - if cookie.HttpOnly { - domain = httpOnlyPrefix - } - domain += cookie.Domain - - fmt.Fprintf( - w, - "%s\t%s\t%s\t%s\t%d\t%s\t%s\n", - domain, - netscapeBool(strings.HasPrefix(cookie.Domain, `.`)), - cookie.Path, - netscapeBool(cookie.Secure), - cookie.Expires.Unix(), - cookie.Name, - cookie.Value, - ) - } - // https://github.com/golang/go/issues/45380#issuecomment-1014950980 switch cookiesTyp := any(cookies).(type) { + case CookieSeq: case []*http.Cookie: for i := j; i < len(cookiesTyp); i++ { if cookiesTyp[i] == nil { continue } - writeCookie(w, cookiesTyp[i]) + exportCookie(w, cookiesTyp[i]) } case []*Cookie: for i := j; i < len(cookiesTyp); i++ { if cookiesTyp[i] == nil { continue } - writeCookie(w, &cookiesTyp[i].Cookie) + exportCookie(w, &cookiesTyp[i].Cookie) + } + } +} + +func exportCookieSeq(ctx context.Context, w io.Writer, seq CookieSeq) { + var init bool + for cookie, _ := range seq.OnlyCookies() { + select { + case <-ctx.Done(): + return + default: } + if !init { + fmt.Fprint(w, netscapeHeader) + init = true + } + exportCookie(w, &cookie.Cookie) } } +func (s CookieSeq) Export(ctx context.Context, w io.Writer) { exportCookieSeq(ctx, w, s) } + type netscapeBool bool func (b netscapeBool) String() string { diff --git a/export_example_test.go b/export_example_test.go index 5237af6..50908ad 100644 --- a/export_example_test.go +++ b/export_example_test.go @@ -1,6 +1,7 @@ package kooky_test import ( + "context" "net/http" "os" @@ -19,5 +20,5 @@ func ExampleExportCookies() { } defer file.Close() - kooky.ExportCookies(file, cookies) + kooky.ExportCookies(context.TODO(), file, cookies) } diff --git a/filter.go b/filter.go index 3aa57ce..ddddf2b 100644 --- a/filter.go +++ b/filter.go @@ -1,13 +1,18 @@ package kooky import ( + "context" + "errors" "fmt" "net/http" + "reflect" "strings" "time" ) -// Filter is used for filtering cokies in ReadCookies() functions. +// Filter is used for filtering cookies in ReadCookies() functions. +// Filter order might be changed for performance reasons +// (omission of value decryption of filtered out cookies, etc). // // A cookie passes the Filter if Filter.Filter returns true. type Filter interface{ Filter(*Cookie) bool } @@ -21,45 +26,125 @@ func (f FilterFunc) Filter(c *Cookie) bool { return f(c) } -// FilterCookies() applies "filters" in order to the "cookies". -func FilterCookies[T Cookie | http.Cookie](cookies []*T, filters ...Filter) []*T { - var ret = make([]*T, 0, len(cookies)) +type ValueFilterFunc func(*Cookie) bool +func (f ValueFilterFunc) Filter(c *Cookie) bool { + if f == nil { + return false + } + return f(c) +} + +func FilterCookies[S CookieSeq | ~[]*Cookie | ~[]*http.Cookie](ctx context.Context, cookies S, filters ...Filter) CookieSeq { + var ret CookieSeq // https://github.com/golang/go/issues/45380#issuecomment-1014950980 - switch cookiesTyp := any(cookies).(type) { + switch cookiesTyped := any(cookies).(type) { + case CookieSeq: + ret = filterCookieSeq(ctx, cookiesTyped, filters...) + case Cookies: + ret = filterCookieSlice(ctx, []*Cookie(cookiesTyped), filters...) + case []*Cookie: + ret = filterCookieSlice(ctx, cookiesTyped, filters...) case []*http.Cookie: - cookieLoopHTTP: - for i, cookie := range cookiesTyp { + ret = filterCookieSlice(ctx, cookiesTyped, filters...) + default: + rv := reflect.ValueOf(cookies) + rtc := reflect.TypeFor[[]*Cookie]() + rthc := reflect.TypeFor[[]*http.Cookie]() + if rv.CanConvert(rtc) { + cookiesTyped := rv.Convert(rtc).Interface().([]*Cookie) + ret = filterCookieSlice(ctx, cookiesTyped, filters...) + } else if rv.CanConvert(rthc) { + cookiesTyped := rv.Convert(rthc).Interface().([]*http.Cookie) + ret = filterCookieSlice(ctx, cookiesTyped, filters...) + } else { + ret = func(yield func(*Cookie, error) bool) { yield(nil, errors.New(`unknown type`)) } + } + } + return ret +} + +func filterCookieSeq(ctx context.Context, cookies CookieSeq, filters ...Filter) CookieSeq { + return func(yield func(*Cookie, error) bool) { + if cookies == nil { + yield(nil, errors.New(`nil receiver`)) + return + } + for cookie, errCookie := range cookies { + if errCookie != nil { + if !yield(nil, errCookie) { + return + } + continue + } if cookie == nil { continue } - for _, filter := range filters { - if !filter.Filter(&Cookie{Cookie: *cookie}) { - continue cookieLoopHTTP - } + select { + case <-ctx.Done(): + return + default: } - ret = append(ret, cookies[i]) - } - case []*Cookie: - cookieLoopKooky: - for i, cookie := range cookiesTyp { - if cookie == nil { + if !FilterCookie(ctx, cookie, filters...) { continue } - for _, filter := range filters { - if !filter.Filter(cookie) { - continue cookieLoopKooky - } + if !yield(cookie, nil) { + return } - ret = append(ret, cookies[i]) } } +} - return ret +func filterCookieSlice[S ~[]*T, T Cookie | http.Cookie](ctx context.Context, cookies S, filters ...Filter) CookieSeq { + return func(yield func(*Cookie, error) bool) { + if len(cookies) < 1 { + _ = yield(nil, errors.New(`cookie slice of lenght 0`)) + return + } + switch cookiesTyped := any(cookies).(type) { + case []*http.Cookie: + cookieLoopHTTP: + for _, cookie := range cookiesTyped { + if cookie == nil { + continue + } + select { + case <-ctx.Done(): + break cookieLoopHTTP + default: + } + kooky := &Cookie{Cookie: *cookie} + if !FilterCookie(ctx, kooky, filters...) { + continue + } + if !yield(kooky, nil) { + return + } + } + case []*Cookie: + cookieLoopKooky: + for _, cookie := range cookiesTyped { + if cookie == nil { + continue + } + select { + case <-ctx.Done(): + break cookieLoopKooky + default: + } + if !FilterCookie(ctx, cookie, filters...) { + continue + } + if !yield(cookie, nil) { + return + } + } + } + } } // FilterCookie() tells if a "cookie" passes all "filters". -func FilterCookie[T Cookie | http.Cookie](cookie *T, filters ...Filter) bool { +func FilterCookie[T Cookie | http.Cookie](ctx context.Context, cookie *T, filters ...Filter) bool { if cookie == nil { return false } @@ -74,6 +159,14 @@ func FilterCookie[T Cookie | http.Cookie](cookie *T, filters ...Filter) bool { } for _, filter := range filters { + if filter == nil { + continue + } + select { + case <-ctx.Done(): + return false + default: + } if !filter.Filter(c) { return false } @@ -87,6 +180,7 @@ func FilterCookie[T Cookie | http.Cookie](cookie *T, filters ...Filter) bool { // // Position Debug after the filter you want to test. var Debug Filter = FilterFunc(func(cookie *Cookie) bool { + // TODO(srlehn): where should the Debug filter be positioned when the filter rearrangement happens? fmt.Printf("%+#v\n", cookie) return true }) @@ -94,7 +188,7 @@ var Debug Filter = FilterFunc(func(cookie *Cookie) bool { // domain filters type domainFilter struct { - filterFunc func(*Cookie) bool + filterFunc FilterFunc typ string domain string } @@ -191,27 +285,27 @@ func PathDepth(depth int) Filter { // value filters func Value(value string) Filter { - return FilterFunc(func(cookie *Cookie) bool { + return ValueFilterFunc(func(cookie *Cookie) bool { return cookie != nil && cookie.Value == value }) } func ValueContains(substr string) Filter { - return FilterFunc(func(cookie *Cookie) bool { + return ValueFilterFunc(func(cookie *Cookie) bool { return cookie != nil && strings.Contains(cookie.Value, substr) }) } func ValueHasPrefix(prefix string) Filter { - return FilterFunc(func(cookie *Cookie) bool { + return ValueFilterFunc(func(cookie *Cookie) bool { return cookie != nil && strings.HasPrefix(cookie.Value, prefix) }) } func ValueHasSuffix(suffix string) Filter { - return FilterFunc(func(cookie *Cookie) bool { + return ValueFilterFunc(func(cookie *Cookie) bool { return cookie != nil && strings.HasSuffix(cookie.Value, suffix) }) } func ValueLen(length int) Filter { - return FilterFunc(func(cookie *Cookie) bool { + return ValueFilterFunc(func(cookie *Cookie) bool { return cookie != nil && len(cookie.Value) == length }) } @@ -230,7 +324,7 @@ var HTTPOnly Filter = FilterFunc(func(cookie *Cookie) bool { // expires filters -var Valid Filter = FilterFunc(func(cookie *Cookie) bool { +var Valid Filter = ValueFilterFunc(func(cookie *Cookie) bool { return cookie != nil && cookie.Expires.After(time.Now()) && cookie.Cookie.Valid() == nil }) diff --git a/filter_example_test.go b/filter_example_test.go index 2124ba1..d4f2b8b 100644 --- a/filter_example_test.go +++ b/filter_example_test.go @@ -1,6 +1,7 @@ package kooky_test import ( + "context" "fmt" "net/http" "regexp" @@ -14,11 +15,13 @@ var reBase64 = regexp.MustCompile(`^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[ func ExampleFilter_regex() { var cookies = []*kooky.Cookie{{Cookie: http.Cookie{Name: `test`, Value: `dGVzdA==`}}} + ctx := context.Background() cookies = kooky.FilterCookies( + ctx, cookies, ValueRegexMatch(reBase64), // filter cookies with the regex filter // kooky.Debug, // print cookies after applying the regex filter - ) + ).Collect(ctx) for _, cookie := range cookies { fmt.Println(cookie.Value) diff --git a/filtercookies_example_test.go b/filtercookies_example_test.go index bf762c4..b8d969a 100644 --- a/filtercookies_example_test.go +++ b/filtercookies_example_test.go @@ -1,6 +1,8 @@ package kooky_test import ( + "context" + "github.com/xiazemin/kooky" _ "github.com/xiazemin/kooky/browser/all" // register cookiestore finders ) @@ -8,13 +10,18 @@ import ( var cookieName = `NID` func ExampleFilterCookies() { - cookies := kooky.ReadCookies() // automatic read + ctx := context.TODO() + + cookies := kooky.AllCookies(). // automatic read + Seq(). + Filter( + ctx, + kooky.Valid, // remove expired cookies + kooky.DomainContains(`google`), // cookie domain has to contain "google" + kooky.Name(cookieName), // cookie name is "NID" + kooky.Debug, // print cookies after applying previous filter + ). + Collect(ctx) // iterate and collect in a slice - cookies = kooky.FilterCookies( - cookies, - kooky.Valid, // remove expired cookies - kooky.DomainContains(`google`), // cookie domain has to contain "google" - kooky.Name(cookieName), // cookie name is "NID" - kooky.Debug, // print cookies after applying previous filter - ) + _ = cookies // do something } diff --git a/find.go b/find.go index 49a9185..ffeccb7 100644 --- a/find.go +++ b/find.go @@ -1,6 +1,9 @@ package kooky import ( + "context" + "fmt" + "iter" "net/http" "sync" ) @@ -10,18 +13,22 @@ import ( // Call CookieStore.Close() after using any of its methods. type CookieStore interface { http.CookieJar - SubJar(filters ...Filter) (http.CookieJar, error) - ReadCookies(...Filter) ([]*Cookie, error) + BrowserInfo + SubJar(context.Context, ...Filter) (http.CookieJar, error) + TraverseCookies(...Filter) CookieSeq + Close() error +} + +type BrowserInfo interface { Browser() string Profile() string IsDefaultProfile() bool FilePath() string - Close() error } // CookieStoreFinder tries to find cookie stores at default locations. type CookieStoreFinder interface { - FindCookieStores() ([]CookieStore, error) + FindCookieStores() CookieStoreSeq } var ( @@ -52,105 +59,181 @@ func RegisterFinder(browser string, finder CookieStoreFinder) { // Or only a specific browser: // // import _ "github.com/xiazemin/kooky/browser/chrome" -func FindAllCookieStores() []CookieStore { - var ret []CookieStore - - var wg sync.WaitGroup - wg.Add(len(finders)) +func FindAllCookieStores(ctx context.Context) []CookieStore { + return TraverseCookieStores(ctx).AllCookieStores(ctx) +} - c := make(chan []CookieStore) - done := make(chan struct{}) +type CookieStoreSeq iter.Seq2[CookieStore, error] - go func() { - for cookieStores := range c { - ret = append(ret, cookieStores...) +// sequence of non-nil cookie stores and nil errors +func (s CookieStoreSeq) OnlyCookieStores() CookieStoreSeq { + return func(yield func(CookieStore, error) bool) { + if s == nil { + return } - close(done) - }() - - muFinder.RLock() - defer muFinder.RUnlock() - for _, finder := range finders { - go func(finder CookieStoreFinder) { - defer wg.Done() - cookieStores, err := finder.FindCookieStores() - if err == nil && cookieStores != nil { - c <- cookieStores + for cookieStore, err := range s { + if err != nil || cookieStore == nil { + continue } - }(finder) + if !yield(cookieStore, nil) { + return + } + } } +} - wg.Wait() - close(c) - - <-done - +func (s CookieStoreSeq) AllCookieStores(ctx context.Context) []CookieStore { + var ret []CookieStore + if s == nil { + return nil + } +Outer: + for cookieStore, _ := range s { + select { + case <-ctx.Done(): + break Outer + default: + } + if cookieStore == nil { + continue + } + ret = append(ret, cookieStore) + } return ret } -// ReadCookies() uses registered cookiestore finders to read cookies. -// Erronous reads are skipped. -// -// Register cookie store finders for all browsers like this: -// -// import _ "github.com/xiazemin/kooky/browser/all" -// -// Or only a specific browser: -// -// import _ "github.com/xiazemin/kooky/browser/chrome" -func ReadCookies(filters ...Filter) []*Cookie { - var ret []*Cookie +func (s CookieStoreSeq) TraverseCookies(ctx context.Context, filters ...Filter) CookieSeq { + if s == nil { + return func(yield func(*Cookie, error) bool) {} + } - cs := make(chan []CookieStore) - c := make(chan []*Cookie) - done := make(chan struct{}) + ctx, cancel := context.WithCancel(ctx) + type ce struct { + c *Cookie + e error + } + startChan := make(chan struct{}, 1) + cookieChan := make(chan ce, 1) - // append cookies go func() { - for cookies := range c { - ret = append(ret, cookies...) + select { + case <-ctx.Done(): + return + case <-startChan: // wait for iteration start + } + var wg sync.WaitGroup + defer func() { + wg.Wait() + cancel() + close(cookieChan) + }() + for cookieStore, err := range s { + if err != nil { + select { + case <-ctx.Done(): + return + case cookieChan <- ce{e: fmt.Errorf(`cookie store: %w`, err)}: + } + continue + } + if cookieStore == nil { + continue + } + wg.Add(1) + go func(cookieStore CookieStore) { + defer wg.Done() + for cookie, err := range cookieStore.TraverseCookies(filters...) { + select { + case <-ctx.Done(): + return + case cookieChan <- ce{c: cookie, e: err}: + } + } + }(cookieStore) } - close(done) }() - // read cookies - go func() { - var wgcs sync.WaitGroup - for cookieStores := range cs { - for _, store := range cookieStores { - wgcs.Add(1) - go func(store CookieStore) { - defer wgcs.Done() - cookies, err := store.ReadCookies(filters...) - if err == nil && cookies != nil { - c <- cookies - } - }(store) + return func(yield func(*Cookie, error) bool) { + startChan <- struct{}{} + for { + select { + case <-ctx.Done(): + return + case c, ok := <-cookieChan: + if !ok { + cancel() + return + } + if !yield(c.c, c.e) { + cancel() + return + } } + } + } +} + +func TraverseCookieStores(ctx context.Context) CookieStoreSeq { + ctx, cancel := context.WithCancel(ctx) + type se struct { + s CookieStore + e error + } + startChan := make(chan struct{}, 1) + storeChan := make(chan se, 1) + + go func() { + select { + case <-ctx.Done(): + return + case <-startChan: + } + var wg sync.WaitGroup + wg.Add(len(finders)) + defer func() { + wg.Wait() + cancel() + close(storeChan) + }() + + muFinder.RLock() + defer muFinder.RUnlock() + + for _, finder := range finders { + if finder == nil { + wg.Done() + continue + } + go func(finder CookieStoreFinder) { + defer wg.Done() + for cookieStore, err := range finder.FindCookieStores() { + select { + case <-ctx.Done(): + return + case storeChan <- se{s: cookieStore, e: err}: + } + } + }(finder) } - wgcs.Wait() - close(c) }() - // find cookie store - var wgcsf sync.WaitGroup - muFinder.RLock() - defer muFinder.RUnlock() - wgcsf.Add(len(finders)) - for _, finder := range finders { - go func(finder CookieStoreFinder) { - defer wgcsf.Done() - cookieStores, err := finder.FindCookieStores() - if err == nil && cookieStores != nil { - cs <- cookieStores + return func(yield func(CookieStore, error) bool) { + startChan <- struct{}{} + for { + select { + case <-ctx.Done(): + return + case s, ok := <-storeChan: + if !ok { + cancel() + return + } + if !yield(s.s, s.e) { + cancel() + return + } } - }(finder) + } } - wgcsf.Wait() - close(cs) - - <-done - - return ret } diff --git a/firstmatch_nid_test.go b/firstmatch_nid_test.go new file mode 100644 index 0000000..6512ede --- /dev/null +++ b/firstmatch_nid_test.go @@ -0,0 +1,27 @@ +package kooky + +import ( + "context" + "testing" +) + +var nidFilters = []Filter{ + Domain(`.google.com`), + Name(`NID`), +} + +func BenchmarkFirstMatch(b *testing.B) { + ctx := context.Background() + for i := 0; i < b.N; i++ { + cookie := TraverseCookies(ctx).FirstMatch(ctx, nidFilters...) + _ = cookie + } +} + +func BenchmarkFirstMatchSlice(b *testing.B) { + for i := 0; i < b.N; i++ { + // cookies := ReadCookies(nidFilters...) // old name + cookies := AllCookies(nidFilters...) + _ = cookies + } +} diff --git a/go.mod b/go.mod index df22025..dec7f38 100644 --- a/go.mod +++ b/go.mod @@ -1,20 +1,18 @@ module github.com/xiazemin/kooky -go 1.18 +go 1.23.2 require ( github.com/Velocidex/ordereddict v0.0.0-20230909174157-2aa49cc5d11d - github.com/bobesa/go-domain-util v0.0.0-20190911083921-4033b5f7dd89 github.com/go-ini/ini v1.67.0 github.com/go-sqlite/sqlite3 v0.0.0-20180313105335-53dd8e640ee7 - github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100 + github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 github.com/spf13/pflag v1.0.5 - github.com/zalando/go-keyring v0.2.3 - github.com/xiazemin/kooky v0.0.0-20230814063115-d4b42194bf0b - golang.org/x/crypto v0.13.0 - golang.org/x/net v0.15.0 - golang.org/x/sys v0.12.0 - golang.org/x/text v0.13.0 + github.com/zalando/go-keyring v0.2.5 + golang.org/x/crypto v0.28.0 + golang.org/x/net v0.30.0 + golang.org/x/sys v0.26.0 + golang.org/x/text v0.19.0 www.velocidex.com/golang/go-ese v0.2.0 ) diff --git a/go.sum b/go.sum index 1e65efc..d557c8b 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,6 @@ github.com/alecthomas/repr v0.1.1 h1:87P60cSmareLAxMc4Hro0r2RBY4ROm0dYwkJNpS4pPs github.com/alecthomas/repr v0.1.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= -github.com/bobesa/go-domain-util v0.0.0-20190911083921-4033b5f7dd89 h1:2pkAuIM8OF1fy4ToFpMnI4oE+VeUNRbGrpSLKshK0oQ= -github.com/bobesa/go-domain-util v0.0.0-20190911083921-4033b5f7dd89/go.mod h1:/09nEjna1UMoasyyQDhOrIn8hi2v2kiJglPWed1idck= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -25,8 +23,8 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gonuts/binary v0.2.0 h1:caITwMWAoQWlL0RNvv2lTU/AHqAJlVuu6nZmNgfbKW4= github.com/gonuts/binary v0.2.0/go.mod h1:kM+CtBrCGDSKdv8WXTuCUsw+loiy8f/QEI8YCCC0M/E= -github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100 h1:rG3VnJUnAWyiv7qYmmdOdSapzz6HM+zb9/uRFr0T5EM= -github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100/go.mod h1:qDHUvIjGZJUtdPtuP4WMu5/U4aVWbFw1MhlkJqCGmCQ= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -48,24 +46,21 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= -github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= -github.com/xiazemin/kooky v0.0.0-20230814063115-d4b42194bf0b h1:PC1NpdF3TGAXnj+aIBMowu+J82GFKldtsQ8zA3j7RN8= -github.com/xiazemin/kooky v0.0.0-20230814063115-d4b42194bf0b/go.mod h1:6JjIozsIKDJ2H0S0ePLIoVbhhBvI15a/oFlMYsfIsSA= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8= +github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= diff --git a/internal/chrome/chrome.go b/internal/chrome/chrome.go index 02bf577..f2774c5 100644 --- a/internal/chrome/chrome.go +++ b/internal/chrome/chrome.go @@ -2,6 +2,7 @@ package chrome import ( "bytes" + "context" "crypto/aes" "crypto/cipher" "crypto/sha1" @@ -13,96 +14,118 @@ import ( "golang.org/x/crypto/pbkdf2" "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/iterx" "github.com/xiazemin/kooky/internal/timex" "github.com/xiazemin/kooky/internal/utils" ) // Thanks to https://gist.github.com/dacort/bd6a5116224c594b14db -func (s *CookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cookie, error) { +func (s *CookieStore) TraverseCookies(filters ...kooky.Filter) kooky.CookieSeq { if s == nil { - return nil, errors.New(`cookie store is nil`) + return iterx.ErrCookieSeq(errors.New(`cookie store is nil`)) } if err := s.Open(); err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } else if s.Database == nil { - return nil, errors.New(`database is nil`) + return iterx.ErrCookieSeq(errors.New(`database is nil`)) } - var cookies []*kooky.Cookie - headerMappings := map[string]string{ "secure": "is_secure", "httponly": "is_httponly", } - err := utils.VisitTableRows(s.Database, `cookies`, headerMappings, func(rowID *int64, row utils.TableRow) error { - cookie := &kooky.Cookie{ - Creation: timex.FromFILETIME(*rowID * 10), - } - var err error - - cookie.Domain, err = row.String(`host_key`) - if err != nil { - return err - } + splitFilters := true + valRetr := func(row utils.TableRow) func(c *kooky.Cookie) error { + return func(c *kooky.Cookie) error { return s.saveCookieValue(c, row) } + } + yldr := iterx.NewCookieFilterYielder(splitFilters, filters...) - cookie.Name, err = row.String(`name`) - if err != nil { - return err - } + ctx := context.Background() + visitor := func(ctx context.Context, yield func(*kooky.Cookie, error) bool) func(rowID *int64, row utils.TableRow) error { + return func(rowID *int64, row utils.TableRow) error { + cookie := &kooky.Cookie{ + Creation: timex.FromFILETIME(*rowID * 10), + } - cookie.Path, err = row.String(`path`) - if err != nil { - return err - } + var err error - if expires_utc, err := row.Int64(`expires_utc`); err == nil { - // https://cs.chromium.org/chromium/src/base/time/time.h?l=452&rcl=fceb9a030c182e939a436a540e6dacc70f161cb1 - if expires_utc != 0 { - cookie.Expires = timex.FromFILETIME(expires_utc * 10) + cookie.Domain, err = row.String(`host_key`) + if err != nil { + return err } - } else { - return err - } - cookie.Secure, err = row.Bool(`is_secure`) - if err != nil { - return err - } + cookie.Name, err = row.String(`name`) + if err != nil { + return err + } - cookie.HttpOnly, err = row.Bool(`is_httponly`) - if err != nil { - return err - } + cookie.Path, err = row.String(`path`) + if err != nil { + return err + } - encrypted_value, err := row.BytesStringOrFallback(`encrypted_value`, nil) - if err != nil { - return err - } else if len(encrypted_value) > 0 { - if decrypted, err := s.decrypt(encrypted_value); err == nil { - cookie.Value = string(decrypted) + if expiresUTC, err := row.Int64(`expires_utc`); err == nil { + // https://cs.chromium.org/chromium/src/base/time/time.h?l=452&rcl=fceb9a030c182e939a436a540e6dacc70f161cb1 + if expiresUTC != 0 { + cookie.Expires = timex.FromFILETIME(expiresUTC * 10) + } } else { - return fmt.Errorf("decrypting cookie %v: %w", cookie, err) + return err } - } else { - cookie.Value, err = row.String(`value`) + + cookie.Secure, err = row.Bool(`is_secure`) if err != nil { return err } + + cookie.HttpOnly, err = row.Bool(`is_httponly`) + if err != nil { + return err + } + cookie.Browser = s + + if !yldr(ctx, yield, cookie, nil, valRetr(row)) { + return iterx.ErrYieldEnd + } + + return nil } + } - if kooky.FilterCookie(cookie, filters...) { - cookies = append(cookies, cookie) + seq := func(yield func(*kooky.Cookie, error) bool) { + err := utils.VisitTableRows(s.Database, `cookies`, headerMappings, visitor(ctx, yield)) + if !errors.Is(err, iterx.ErrYieldEnd) { + yield(nil, err) } + } + return seq +} + +// query, decrypt and store cookie value +func (s *CookieStore) saveCookieValue(cookie *kooky.Cookie, row utils.TableRow) error { + if cookie.Value != "" { return nil - }) + } + encryptedValue, err := row.BytesStringOrFallback(`encrypted_value`, nil) if err != nil { - return nil, err + return err } - - return cookies, nil + if len(encryptedValue) > 0 { + if decrypted, err := s.decrypt(encryptedValue); err == nil { + cookie.Value = string(decrypted) + } else { + return fmt.Errorf("decrypting cookie %v: %w", cookie, err) + } + } else { + cookie.Value, err = row.String(`value`) + if err != nil { + return err + } + } + return nil } // "mock_password" from https://github.com/chromium/chromium/blob/34f6b421d6d255b27e01d82c3c19f49a455caa06/crypto/mock_apple_keychain.cc#L75 diff --git a/internal/chrome/chrome_darwin.go b/internal/chrome/chrome_darwin.go index 70d2445..392f890 100644 --- a/internal/chrome/chrome_darwin.go +++ b/internal/chrome/chrome_darwin.go @@ -25,8 +25,11 @@ func (s *CookieStore) getKeyringPassword(useSaved bool) ([]byte, error) { } } - // TODO: use s.browser - out, err := exec.Command(`/usr/bin/security`, `find-generic-password`, `-s`, `Chrome Safe Storage`, `-wa`, `Chrome`).Output() + out, err := exec.Command( + `/usr/bin/security`, `find-generic-password`, + `-s`, s.safeStorageName(), + `-wa`, s.safeStorageAccount(), + ).Output() if err != nil { return nil, err } diff --git a/internal/chrome/chrome_darwin_cgo.go b/internal/chrome/chrome_darwin_cgo.go index 7bc66bc..0c0ed50 100644 --- a/internal/chrome/chrome_darwin_cgo.go +++ b/internal/chrome/chrome_darwin_cgo.go @@ -27,9 +27,9 @@ func (s *CookieStore) getKeyringPassword(useSaved bool) ([]byte, error) { } } - password, err := keychain.GetGenericPassword("Chrome Safe Storage", "Chrome", "", "") + password, err := keychain.GetGenericPassword(s.safeStorageName(), s.safeStorageAccount(), "", "") if err != nil { - return nil, fmt.Errorf("error reading 'Chrome Safe Storage' keychain password: %w", err) + return nil, fmt.Errorf(`error reading '%s' keychain password: %w`, s.safeStorageName(), err) } s.KeyringPasswordBytes = password keyringPasswordMap.set(kpmKey, password) diff --git a/internal/chrome/chrome_notwindows.go b/internal/chrome/chrome_notwindows.go index 094f71b..77601b6 100644 --- a/internal/chrome/chrome_notwindows.go +++ b/internal/chrome/chrome_notwindows.go @@ -1,4 +1,5 @@ -//+build !windows +//go:build !windows +// +build !windows package chrome diff --git a/internal/chrome/chrome_windows.go b/internal/chrome/chrome_windows.go index d10cb5f..6963fb1 100644 --- a/internal/chrome/chrome_windows.go +++ b/internal/chrome/chrome_windows.go @@ -9,7 +9,7 @@ import ( "encoding/base64" "encoding/json" "errors" - "io/ioutil" + "os" "path/filepath" "unsafe" @@ -88,7 +88,7 @@ func (s *CookieStore) getKeyringPassword(useSaved bool) ([]byte, error) { } } - stateBytes, err := ioutil.ReadFile(stateFile) + stateBytes, err := os.ReadFile(stateFile) if err != nil { return nil, err } diff --git a/internal/chrome/cookiestore.go b/internal/chrome/cookiestore.go index 83a7055..cbc2def 100644 --- a/internal/chrome/cookiestore.go +++ b/internal/chrome/cookiestore.go @@ -6,6 +6,7 @@ import ( "github.com/go-sqlite/sqlite3" "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/utils" ) type CookieStore struct { @@ -14,6 +15,7 @@ type CookieStore struct { KeyringPasswordBytes []byte PasswordBytes []byte DecryptionMethod func(data, password []byte) ([]byte, error) + storage safeStorage } func (s *CookieStore) Open() error { @@ -24,7 +26,11 @@ func (s *CookieStore) Open() error { return nil } - db, err := sqlite3.Open(s.FileNameStr) + f, err := utils.OpenFile(s.FileNameStr) + if err != nil { + return err + } + db, err := sqlite3.OpenFrom(f) if err != nil { return err } diff --git a/internal/chrome/derivatives.go b/internal/chrome/derivatives.go new file mode 100644 index 0000000..ca4c7e5 --- /dev/null +++ b/internal/chrome/derivatives.go @@ -0,0 +1,53 @@ +package chrome + +import ( + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type safeStorage struct { + account string + name string +} + +func (s *CookieStore) SetSafeStorage(account, name string) { + if s == nil { + return + } + if len(account) == 0 && len(s.BrowserStr) > 0 { + account = cases.Title(language.English, cases.Compact).String(s.BrowserStr) + } + if len(name) == 0 { + name = account + ` Safe Storage` + } + s.storage.account = account + s.storage.name = name +} + +func (s *CookieStore) safeStorageName() string { + if s == nil { + return `` + } + if len(s.storage.name) == 0 { + s.SetSafeStorage(``, ``) + } + return s.storage.name +} + +func (s *CookieStore) safeStorageAccount() string { + if s == nil { + return `` + } + if len(s.storage.name) == 0 { + s.SetSafeStorage(``, ``) + } + return s.storage.account +} + +/* +known "Safe Storage" string combinations: +account | name +---------------------------- +Chrome | Chrome Safe Storage +Microsoft Edge | Microsoft Edge Safe Storage +*/ diff --git a/internal/chrome/find/find.go b/internal/chrome/find/find.go index 0ea130a..5fd49bf 100644 --- a/internal/chrome/find/find.go +++ b/internal/chrome/find/find.go @@ -3,7 +3,8 @@ package find import ( "encoding/json" "errors" - "io/ioutil" + "iter" + "os" "path/filepath" "runtime" ) @@ -18,79 +19,91 @@ type chromeCookieStoreFile struct { // chromeRoots and chromiumRoots could be put into the github.com/kooky/browser/{chrome,chromium} packages. // It might be better though to keep those 2 together here as they are based on the same source. -func FindChromeCookieStoreFiles() ([]*chromeCookieStoreFile, error) { +func FindChromeCookieStoreFiles() iter.Seq2[*chromeCookieStoreFile, error] { return FindCookieStoreFiles(chromeRoots, `chrome`) } -func FindChromiumCookieStoreFiles() ([]*chromeCookieStoreFile, error) { +func FindChromiumCookieStoreFiles() iter.Seq2[*chromeCookieStoreFile, error] { return FindCookieStoreFiles(chromiumRoots, `chromium`) } -func FindCookieStoreFiles(rootsFunc func() ([]string, error), browserName string) ([]*chromeCookieStoreFile, error) { - if rootsFunc == nil { - return nil, errors.New(`passed roots function is nil`) - } - var files []*chromeCookieStoreFile - roots, err := rootsFunc() - if err != nil { - return nil, err - } - for _, root := range roots { - localStateBytes, err := ioutil.ReadFile(filepath.Join(root, `Local State`)) - if err != nil { - continue +func FindCookieStoreFiles(rootsFunc iter.Seq2[string, error], browserName string) iter.Seq2[*chromeCookieStoreFile, error] { + return func(yield func(*chromeCookieStoreFile, error) bool) { + if rootsFunc == nil { + _ = yield(nil, errors.New(`passed roots function is nil`)) + return } - var localState struct { - Profile struct { - InfoCache map[string]struct { - IsUsingDefaultName bool `json:"is_using_default_name"` - Name string - } `json:"info_cache"` + for root, err := range rootsFunc { + if err != nil { + if !yield(nil, err) { + return + } + continue } - } - if err := json.Unmarshal(localStateBytes, &localState); err != nil { - // fallback - json file exists, json structure unknown - files = append( - files, - []*chromeCookieStoreFile{ - { - Browser: browserName, - Profile: `Profile 1`, - IsDefaultProfile: true, - Path: filepath.Join(root, `Default`, `Network`, `Cookies`), // Chrome 96 - OS: runtime.GOOS, - }, - { - Browser: browserName, - Profile: `Profile 1`, - IsDefaultProfile: true, - Path: filepath.Join(root, `Default`, `Cookies`), - OS: runtime.GOOS, - }, - }..., - ) - continue + localStateBytes, err := os.ReadFile(filepath.Join(root, `Local State`)) + if err != nil { + if !yield(nil, err) { + return + } + continue + } + var localState struct { + Profile struct { + InfoCache map[string]struct { + IsUsingDefaultName bool `json:"is_using_default_name"` + Name string + } `json:"info_cache"` + } + } + if err := json.Unmarshal(localStateBytes, &localState); err != nil { + // fallback - json file exists, json structure unknown + if !yield(nil, err) { + return + } + st := &chromeCookieStoreFile{ + Browser: browserName, + Profile: `Profile 1`, + IsDefaultProfile: true, + Path: filepath.Join(root, `Default`, `Network`, `Cookies`), // Chrome 96 + OS: runtime.GOOS, + } + if !yield(st, nil) { + return + } + st = &chromeCookieStoreFile{ + Browser: browserName, + Profile: `Profile 1`, + IsDefaultProfile: true, + Path: filepath.Join(root, `Default`, `Cookies`), + OS: runtime.GOOS, + } + if !yield(st, nil) { + return + } + continue + } + for profDir, profStr := range localState.Profile.InfoCache { + st := &chromeCookieStoreFile{ - } - for profDir, profStr := range localState.Profile.InfoCache { - files = append( - files, - []*chromeCookieStoreFile{ - { - Browser: browserName, - Profile: profStr.Name, - IsDefaultProfile: profStr.IsUsingDefaultName, - Path: filepath.Join(root, profDir, `Network`, `Cookies`), // Chrome 96 - OS: runtime.GOOS, - }, { - Browser: browserName, - Profile: profStr.Name, - IsDefaultProfile: profStr.IsUsingDefaultName, - Path: filepath.Join(root, profDir, `Cookies`), - OS: runtime.GOOS, - }, - }..., - ) + Browser: browserName, + Profile: profStr.Name, + IsDefaultProfile: profStr.IsUsingDefaultName, + Path: filepath.Join(root, profDir, `Network`, `Cookies`), // Chrome 96 + OS: runtime.GOOS, + } + if !yield(st, nil) { + return + } + st = &chromeCookieStoreFile{ + Browser: browserName, + Profile: profStr.Name, + IsDefaultProfile: profStr.IsUsingDefaultName, + Path: filepath.Join(root, profDir, `Cookies`), + OS: runtime.GOOS, + } + if !yield(st, nil) { + return + } + } } } - return files, nil } diff --git a/internal/chrome/find/find_android.go b/internal/chrome/find/find_android.go index dbae725..e9b64bb 100644 --- a/internal/chrome/find/find_android.go +++ b/internal/chrome/find/find_android.go @@ -1,20 +1,17 @@ -//+build android +//go:build android +// +build android package find -import ( - "os" - "path/filepath" -) +import "errors" -func chromeRoots() ([]string, error) { +var errNotImplemented = errors.New(`not implemented`) + +func chromeRoots(yield func(string, error) bool) { // https://chromium.googlesource.com/chromium/src.git/+/62.0.3202.58/docs/user_data_dir.md#android - var ret = []string{ - `/data/user/0/com.android.chrome/app_chrome` // TODO check + if !yield(`/data/user/0/com.android.chrome/app_chrome`, nil) { // TODO check + return } - return ret, nil } -func chromiumRoots() ([]string, error) { - return ret, errors.New(`not implemented`) -} +func chromiumRoots(yield func(string, error) bool) { _ = yield(``, errNotImplemented) } diff --git a/internal/chrome/find/find_darwin.go b/internal/chrome/find/find_darwin.go index c0ecb46..4525465 100644 --- a/internal/chrome/find/find_darwin.go +++ b/internal/chrome/find/find_darwin.go @@ -7,29 +7,31 @@ import ( "path/filepath" ) -func chromeRoots() ([]string, error) { +func chromeRoots(yield func(string, error) bool) { // https://chromium.googlesource.com/chromium/src.git/+/62.0.3202.58/docs/user_data_dir.md#mac-os-x // The canary channel suffix is determined using the CrProductDirName key in the browser app's Info.plist // "$HOME/Library/Application Support" cfgDir, err := os.UserConfigDir() if err != nil { - return nil, err + _ = yield(``, err) + return } - var ret = []string{ - filepath.Join(cfgDir, `Google`, `Chrome`), - filepath.Join(cfgDir, `Google`, `Chrome Canary`), + if !yield(filepath.Join(cfgDir, `Google`, `Chrome`), nil) { + return + } + if !yield(filepath.Join(cfgDir, `Google`, `Chrome Canary`), nil) { + return } - return ret, nil } -func chromiumRoots() ([]string, error) { +func chromiumRoots(yield func(string, error) bool) { // "$HOME/Library/Application Support" cfgDir, err := os.UserConfigDir() if err != nil { - return nil, err + _ = yield(``, err) + return } - var ret = []string{ - filepath.Join(cfgDir, `Chromium`), + if !yield(filepath.Join(cfgDir, `Chromium`), nil) { + return } - return ret, nil } diff --git a/internal/chrome/find/find_others.go b/internal/chrome/find/find_others.go index e2d731b..e4ad8c6 100644 --- a/internal/chrome/find/find_others.go +++ b/internal/chrome/find/find_others.go @@ -1,4 +1,4 @@ -//go:build plan9 || android || ios || js || aix +//go:build plan9 || ios || js || aix package find @@ -6,10 +6,6 @@ import "errors" var errNotImplemented = errors.New(`not implemented`) -func chromeRoots() ([]string, error) { - return nil, errNotImplemented -} +func chromeRoots(yield func(string, error) bool) { _ = yield(``, errNotImplemented) } -func chromiumRoots() ([]string, error) { - return nil, errNotImplemented -} +func chromiumRoots(yield func(string, error) bool) { _ = yield(``, errNotImplemented) } diff --git a/internal/chrome/find/find_unix.go b/internal/chrome/find/find_unix.go index 842d671..778c3a9 100644 --- a/internal/chrome/find/find_unix.go +++ b/internal/chrome/find/find_unix.go @@ -7,12 +7,16 @@ import ( "path/filepath" ) -func chromeRoots() ([]string, error) { +func chromeRoots(yield func(string, error) bool) { // "${CHROME_VERSION_EXTRA:-${XDG_CONFIG_HOME:-$HOME/.config}}" // https://chromium.googlesource.com/chromium/src.git/+/62.0.3202.58/docs/user_data_dir.md#linux var dotConfigs, ret []string // fallback - if home, err := os.UserHomeDir(); err == nil { + if home, err := os.UserHomeDir(); err != nil { + if !yield(``, err) { + return + } + } else { dotConfigs = append(dotConfigs, filepath.Join(home, `.config`)) } for _, v := range []string{`XDG_CONFIG_HOME`, `CHROME_CONFIG_HOME`} { @@ -35,24 +39,28 @@ func chromeRoots() ([]string, error) { ) } } - return ret, nil + for _, r := range ret { + if !yield(r, nil) { + return + } + } } -func chromiumRoots() ([]string, error) { +func chromiumRoots(yield func(string, error) bool) { // "${XDG_CONFIG_HOME:-$HOME/.config}" - var dotConfigs, ret []string + var dotConfigs []string // fallback - if home, err := os.UserHomeDir(); err == nil { + if home, err := os.UserHomeDir(); err != nil { + _ = yield(``, err) + } else { dotConfigs = append(dotConfigs, filepath.Join(home, `.config`)) } if dir, ok := os.LookupEnv(`XDG_CONFIG_HOME`); ok { dotConfigs = append(dotConfigs, dir) } for _, dotConfig := range dotConfigs { - ret = append( - ret, - filepath.Join(dotConfig, `chromium`), - ) + if !yield(filepath.Join(dotConfig, `chromium`), nil) { + return + } } - return ret, nil } diff --git a/internal/chrome/find/find_windows.go b/internal/chrome/find/find_windows.go index 4d4049d..5e1490f 100644 --- a/internal/chrome/find/find_windows.go +++ b/internal/chrome/find/find_windows.go @@ -1,4 +1,5 @@ -//+build windows +//go:build windows +// +build windows package find @@ -8,25 +9,30 @@ import ( "path/filepath" ) -func chromeRoots() ([]string, error) { +func chromeRoots(yield func(string, error) bool) { // https://chromium.googlesource.com/chromium/src.git/+/62.0.3202.58/docs/user_data_dir.md#windows cfgDir := os.Getenv(`LocalAppData`) if len(cfgDir) == 0 { - return nil, errors.New(`%LocalAppData% is empty`) + _ = yield(``, errors.New(`%LocalAppData% is empty`)) + return } - var ret = []string{ - filepath.Join(cfgDir, `Google`, `Chrome`, `User Data`), - // Canary uses InstallConstants::install_suffix - // https://cs.chromium.org/chromium/src/chrome/install_static/install_constants.h?q=install_suffix - filepath.Join(cfgDir, `Google`, `Chrome SxS`, `User Data`), + if !yield(filepath.Join(cfgDir, `Google`, `Chrome`, `User Data`), nil) { + return + } + // Canary uses InstallConstants::install_suffix + // https://cs.chromium.org/chromium/src/chrome/install_static/install_constants.h?q=install_suffix + if !yield(filepath.Join(cfgDir, `Google`, `Chrome SxS`, `User Data`), nil) { + return } - return ret, nil } -func chromiumRoots() ([]string, error) { +func chromiumRoots(yield func(string, error) bool) { cfgDir := os.Getenv(`LocalAppData`) if len(cfgDir) == 0 { - return nil, errors.New(`%LocalAppData% is empty`) + _ = yield(``, errors.New(`%LocalAppData% is empty`)) + return + } + if !yield(filepath.Join(cfgDir, `Chromium`, `User Data`), nil) { + return } - return []string{filepath.Join(cfgDir, `Chromium`, `User Data`)}, nil } diff --git a/internal/cookies/cookiejar.go b/internal/cookies/cookiejar.go index d1fc4b6..4803352 100644 --- a/internal/cookies/cookiejar.go +++ b/internal/cookies/cookiejar.go @@ -1,6 +1,7 @@ package cookies import ( + "context" "errors" "net/http" "net/http/cookiejar" @@ -21,18 +22,30 @@ type CookieJar struct { *cookiejar.Jar init sync.Once initErr error + filters []kooky.Filter + cookies kooky.Cookies // duplicate storage required for SubJar() CookieStore } +func NewCookieJar(st CookieStore, filters ...kooky.Filter) *CookieJar { + // TODO set struct fields of the CookieStore: + // FileNameStr, BrowserStr, string; CookieStore CookieStore (inner), File *os.File + return &CookieJar{ + filters: filters, + CookieStore: st, + } +} + func (s *CookieJar) Cookies(u *url.URL) []*http.Cookie { + var ret []*http.Cookie if s == nil || s.CookieStore == nil || s.initErr != nil { - return nil + return ret } if err := s.InitJar(); err != nil { - return nil + return ret } - if s.Jar == nil { - return nil + if s.Jar == nil || u == nil { + return ret } return s.Jar.Cookies(u) } @@ -45,11 +58,18 @@ func (s *CookieJar) InitJar() error { return errors.New(`no cookie store set`) } s.init.Do(func() { - kookies, err := s.CookieStore.ReadCookies() - defer s.Close() - if err != nil { - s.initErr = err - return + ctx := context.Background() + var kookies []*kooky.Cookie + if s.CookieStore != nil && len(s.cookies) == 0 { + var err error + kookies, err = s.CookieStore.TraverseCookies(s.filters...).ReadAllCookies(ctx) + defer s.Close() + if err != nil { + s.initErr = err + return + } + } else { + kookies = kooky.FilterCookies(ctx, s.cookies, s.filters...).Collect(ctx) } jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) if err != nil { @@ -57,38 +77,44 @@ func (s *CookieJar) InitJar() error { return } s.Jar = jar - cookies := kookies2cookies(kookies) + s.cookies = kookies + cookies := kookies2cookies(ctx, kookies) setAllCookies(s, cookies) }) return s.initErr } -func (s *CookieJar) SubJar(filters ...kooky.Filter) (http.CookieJar, error) { +func (s *CookieJar) SubJar(ctx context.Context, filters ...kooky.Filter) (http.CookieJar, error) { if s == nil { return nil, errors.New(`nil receiver`) } if s.CookieStore == nil { return nil, errors.New(`no cookie store set`) } - kookies, err := s.CookieStore.ReadCookies(filters...) - defer s.Close() - if err != nil { + if err := s.InitJar(); err != nil { return nil, err } + kookies := kooky.FilterCookies(ctx, s.cookies, filters...).Collect(ctx) jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) if err != nil { return nil, err } - cookies := kookies2cookies(kookies) + j := &CookieJar{ + filters: append(s.filters, filters...), + cookies: kookies, + Jar: jar, + CookieStore: s.CookieStore, + } + cookies := kookies2cookies(ctx, kookies) setAllCookies(jar, cookies) - return jar, nil + return j, nil } -func kookies2cookies(kookies []*kooky.Cookie, filters ...kooky.Filter) []*http.Cookie { - filteredKookies := kooky.FilterCookies(kookies, filters...) - cookies := make([]*http.Cookie, len(filteredKookies)) +func kookies2cookies(ctx context.Context, kookies []*kooky.Cookie, filters ...kooky.Filter) []*http.Cookie { + filteredKookies := kooky.FilterCookies(ctx, kookies, filters...).Collect(ctx) + cookies := make([]*http.Cookie, 0, len(filteredKookies)) for _, k := range filteredKookies { cookies = append(cookies, &k.Cookie) } diff --git a/internal/cookies/cookiestore.go b/internal/cookies/cookiestore.go index c71c8e5..91107b0 100644 --- a/internal/cookies/cookiestore.go +++ b/internal/cookies/cookiestore.go @@ -1,16 +1,19 @@ package cookies import ( + "context" "errors" "io" "os" "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/iterx" + "github.com/xiazemin/kooky/internal/utils" ) // kooky.CookieStore without http.CookieJar and SubJar() type CookieStore interface { - ReadCookies(...kooky.Filter) ([]*kooky.Cookie, error) + TraverseCookies(...kooky.Filter) kooky.CookieSeq Browser() string Profile() string IsDefaultProfile() bool @@ -19,8 +22,8 @@ type CookieStore interface { } /* -DefaultCookieStore implements most of the kooky.CookieStore interface except for the ReadCookies method -func (s *DefaultCookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cookie, error) +DefaultCookieStore implements most of the kooky.CookieStore interface except for the TraverseCookies method +func (s *DefaultCookieStore) TraverseCookies(filters ...kooky.Filter) kooky.CookieSeq DefaultCookieStore also provides an Open() method */ @@ -67,7 +70,7 @@ func (s *DefaultCookieStore) Open() error { return nil } - f, err := os.Open(s.FileNameStr) + f, err := utils.OpenFile(s.FileNameStr) if err != nil { return err } @@ -90,3 +93,33 @@ func (s *DefaultCookieStore) Close() error { return err } + +type JarCreator func(filename string, filters ...kooky.Filter) (*CookieJar, error) + +func SingleRead(jarCr JarCreator, filename string, filters ...kooky.Filter) kooky.CookieSeq { + st, err := jarCr(filename, filters...) + if err != nil { + return iterx.ErrCookieSeq(err) + } + return ReadCookiesClose(st, filters...) +} + +func ReadCookiesClose(store CookieStore, filters ...kooky.Filter) kooky.CookieSeq { + if store == nil { + return iterx.ErrCookieSeq(errors.New(`nil cookie store`)) + } + seq := func(yield func(*kooky.Cookie, error) bool) { + defer func() { + if err := store.Close(); err != nil { + yield(nil, err) + } + }() + for cookie, err := range store.TraverseCookies(filters...) { + if !iterx.CookieFilterYield(context.Background(), cookie, err, yield, filters...) { + return + } + } + + } + return seq +} diff --git a/internal/firefox/cookiestore.go b/internal/firefox/cookiestore.go index 1a228fc..ad75688 100644 --- a/internal/firefox/cookiestore.go +++ b/internal/firefox/cookiestore.go @@ -7,6 +7,7 @@ import ( "github.com/go-sqlite/sqlite3" "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/utils" ) type CookieStore struct { @@ -26,14 +27,18 @@ func (s *CookieStore) Open() error { return nil } - db, err := sqlite3.Open(s.FileNameStr) + f, err := utils.OpenFile(s.FileNameStr) + if err != nil { + return err + } + db, err := sqlite3.OpenFrom(f) if err != nil { return err } s.Database = db contFileName := filepath.Join(filepath.Dir(s.FileNameStr), `containers.json`) - s.contFile, _ = os.Open(contFileName) + s.contFile, _ = utils.OpenFile(contFileName) return nil } diff --git a/internal/firefox/find/find.go b/internal/firefox/find/find.go index e7e24a9..4da0e63 100644 --- a/internal/firefox/find/find.go +++ b/internal/firefox/find/find.go @@ -2,6 +2,7 @@ package find import ( "errors" + "iter" "path/filepath" "strings" @@ -15,63 +16,62 @@ type firefoxCookieStoreFile struct { IsDefaultProfile bool } -func FindFirefoxCookieStoreFiles() ([]*firefoxCookieStoreFile, error) { +func FindFirefoxCookieStoreFiles() iter.Seq2[*firefoxCookieStoreFile, error] { return FindCookieStoreFiles(firefoxRoots, `firefox`, `cookies.sqlite`) } -func FindCookieStoreFiles(rootsFunc func() ([]string, error), browserName, fileName string) ([]*firefoxCookieStoreFile, error) { - if rootsFunc == nil { - return nil, errors.New(`provided roots function is nil`) - } - roots, err := rootsFunc() - if err != nil { - return nil, err - } - if len(roots) == 0 { - return nil, errors.New(`no firefox root directories`) - } - var files []*firefoxCookieStoreFile - for _, root := range roots { - iniFile := filepath.Join(root, `profiles.ini`) - profIni, err := ini.Load(iniFile) - if err != nil { - continue - } - var defaultProfileFolder string - for _, sec := range profIni.SectionStrings() { - cfgSec := profIni.Section(sec) - if cfgSec.Key(`Locked`).String() == `1` { - defaultProfileFolder = cfgSec.Key(`Default`).String() - } +func FindCookieStoreFiles(rootsFunc iter.Seq2[string, error], browserName, fileName string) iter.Seq2[*firefoxCookieStoreFile, error] { + return func(yield func(*firefoxCookieStoreFile, error) bool) { + if rootsFunc == nil { + _ = yield(nil, errors.New(`provided roots function is nil`)) + return } - for _, sec := range profIni.SectionStrings() { - // dedicated profiles (firefox 67+) start with Install instead of Profile followed by upper case hex - // https://support.mozilla.org/en-US/kb/dedicated-profiles-firefox-installation - if !strings.HasPrefix(sec, `Profile`) { + for root, err := range rootsFunc { + if err != nil { + if !yield(nil, err) { + return + } continue } - cfgSec := profIni.Section(sec) - profileFolder := cfgSec.Key(`Path`).String() - var defaultBrowser bool - if profileFolder == defaultProfileFolder /* || cfgSec.Key(`Default`).String() == `1` */ { - defaultBrowser = true + iniFile := filepath.Join(root, `profiles.ini`) + profIni, err := ini.Load(iniFile) + if err != nil { + continue } - profileFolder = filepath.FromSlash(profileFolder) - if cfgSec.Key(`IsRelative`).String() == `1` { - // relative profile path - profileFolder = filepath.Join(root, profileFolder) + var defaultProfileFolder string + for _, sec := range profIni.SectionStrings() { + cfgSec := profIni.Section(sec) + if cfgSec.Key(`Locked`).String() == `1` { + defaultProfileFolder = cfgSec.Key(`Default`).String() + } } - files = append( - files, - &firefoxCookieStoreFile{ + for _, sec := range profIni.SectionStrings() { + // dedicated profiles (firefox 67+) start with Install instead of Profile followed by upper case hex + // https://support.mozilla.org/en-US/kb/dedicated-profiles-firefox-installation + if !strings.HasPrefix(sec, `Profile`) { + continue + } + cfgSec := profIni.Section(sec) + profileFolder := cfgSec.Key(`Path`).String() + var defaultBrowser bool + if profileFolder == defaultProfileFolder /* || cfgSec.Key(`Default`).String() == `1` */ { + defaultBrowser = true + } + profileFolder = filepath.FromSlash(profileFolder) + if cfgSec.Key(`IsRelative`).String() == `1` { + // relative profile path + profileFolder = filepath.Join(root, profileFolder) + } + st := &firefoxCookieStoreFile{ Browser: browserName, Profile: cfgSec.Key(`Name`).String(), IsDefaultProfile: defaultBrowser, Path: filepath.Join(profileFolder, fileName), - }, - ) + } + if !yield(st, nil) { + return + } + } } } - - return files, nil } diff --git a/internal/firefox/find/find_darwin.go b/internal/firefox/find/find_darwin.go index e922fdd..5953ae1 100644 --- a/internal/firefox/find/find_darwin.go +++ b/internal/firefox/find/find_darwin.go @@ -7,11 +7,14 @@ import ( "path/filepath" ) -func firefoxRoots() ([]string, error) { +func firefoxRoots(yield func(string, error) bool) { // "$HOME/Library/Application Support" cfgDir, err := os.UserConfigDir() if err != nil { - return nil, err + _ = yield(``, err) + return + } + if !yield(filepath.Join(cfgDir, `Firefox`), nil) { + return } - return []string{filepath.Join(cfgDir, `Firefox`)}, nil } diff --git a/internal/firefox/find/find_others.go b/internal/firefox/find/find_others.go index 1c385e2..a803315 100644 --- a/internal/firefox/find/find_others.go +++ b/internal/firefox/find/find_others.go @@ -4,6 +4,4 @@ package find import "errors" -func firefoxRoots() ([]string, error) { - return nil, errors.New(`not implemented`) -} +func firefoxRoots(yield func(string, error) bool) { yield(``, errors.New(`not implemented`)) } diff --git a/internal/firefox/find/find_unix.go b/internal/firefox/find/find_unix.go index 5bd9878..de932fa 100644 --- a/internal/firefox/find/find_unix.go +++ b/internal/firefox/find/find_unix.go @@ -7,13 +7,17 @@ import ( "path/filepath" ) -func firefoxRoots() ([]string, error) { +func firefoxRoots(yield func(string, error) bool) { home, err := os.UserHomeDir() if err != nil { - return nil, err + _ = yield(``, err) + return + } + // Ubuntu 21.10 (snap) + if !yield(filepath.Join(home, `snap`, `firefox`, `common`, `.mozilla`, `firefox`), nil) { + return + } + if !yield(filepath.Join(home, `.mozilla`, `firefox`), nil) { + return } - return []string{ - filepath.Join(home, `snap`, `firefox`, `common`, `.mozilla`, `firefox`), // Ubuntu 21.10 (snap) - filepath.Join(home, `.mozilla`, `firefox`), - }, nil } diff --git a/internal/firefox/find/find_windows.go b/internal/firefox/find/find_windows.go index e089609..9a67455 100644 --- a/internal/firefox/find/find_windows.go +++ b/internal/firefox/find/find_windows.go @@ -1,4 +1,5 @@ -//+build windows +//go:build windows +// +build windows package find @@ -7,11 +8,14 @@ import ( "path/filepath" ) -func firefoxRoots() ([]string, error) { +func firefoxRoots(yield func(string, error) bool) { // "%AppData%" cfgDir, err := os.UserConfigDir() if err != nil { - return nil, err + _ = yield(``, err) + return + } + if !yield(filepath.Join(cfgDir, `Mozilla`, `Firefox`), nil) { + return } - return []string{filepath.Join(cfgDir, `Mozilla`, `Firefox`)}, nil } diff --git a/internal/firefox/firefox.go b/internal/firefox/firefox.go index 6762ec6..2058dd8 100644 --- a/internal/firefox/firefox.go +++ b/internal/firefox/firefox.go @@ -1,6 +1,7 @@ package firefox import ( + "context" "errors" "fmt" "strconv" @@ -8,115 +9,118 @@ import ( "time" "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/iterx" "github.com/xiazemin/kooky/internal/utils" - - "github.com/bobesa/go-domain-util/domainutil" ) -func (s *CookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cookie, error) { +func (s *CookieStore) TraverseCookies(filters ...kooky.Filter) kooky.CookieSeq { if s == nil { - return nil, errors.New(`cookie store is nil`) + return iterx.ErrCookieSeq(errors.New(`cookie store is nil`)) } if err := s.Open(); err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } else if s.Database == nil { - return nil, errors.New(`database is nil`) + return iterx.ErrCookieSeq(errors.New(`database is nil`)) } _ = s.initContainersMap() - var cookies []*kooky.Cookie - - err := utils.VisitTableRows(s.Database, `moz_cookies`, map[string]string{}, func(rowId *int64, row utils.TableRow) error { - cookie := kooky.Cookie{} - var err error + visitor := func(yield func(*kooky.Cookie, error) bool) func(rowId *int64, row utils.TableRow) error { + return func(rowId *int64, row utils.TableRow) error { + cookie := kooky.Cookie{} + var err error - // Name - cookie.Name, err = row.String(`name`) - if err != nil { - return err - } - - // Value - cookie.Value, err = row.String(`value`) - if err != nil { - return err - } + // Name + cookie.Name, err = row.String(`name`) + if err != nil { + return err + } - // Domain - if baseDomain := row.ValueOrFallback(`baseDomain`, nil); baseDomain == nil { - if host, err := row.String(`host`); err != nil { + // Value + cookie.Value, err = row.String(`value`) + if err != nil { return err - } else { - cookie.Domain = domainutil.Domain(host) } - } else { - // handle databases prior v78 ESR - var ok bool - cookie.Domain, ok = baseDomain.(string) - if !ok { - return fmt.Errorf("got unexpected value for baseDomain %v (type %[1]T)", baseDomain) + + // Domain + if baseDomain := row.ValueOrFallback(`baseDomain`, nil); baseDomain == nil { + if host, err := row.String(`host`); err != nil { + return err + } else { + cookie.Domain = host + } + } else { + // handle databases prior v78 ESR + var ok bool + cookie.Domain, ok = baseDomain.(string) + if !ok { + return fmt.Errorf("got unexpected value for baseDomain %v (type %[1]T)", baseDomain) + } } - } - // Path - cookie.Path, err = row.String(`path`) - if err != nil { - return err - } + // Path + cookie.Path, err = row.String(`path`) + if err != nil { + return err + } - // Expires - if expiry, err := row.Int64(`expiry`); err == nil { - cookie.Expires = time.Unix(expiry, 0) - } else { - return err - } + // Expires + if expiry, err := row.Int64(`expiry`); err == nil { + cookie.Expires = time.Unix(expiry, 0) + } else { + return err + } - // Creation - if creationTime, err := row.Int64(`creationTime`); err == nil { - cookie.Creation = time.UnixMicro(creationTime) - } else { - return err - } + // Creation + if creationTime, err := row.Int64(`creationTime`); err == nil { + cookie.Creation = time.UnixMicro(creationTime) + } else { + return err + } - // Secure - cookie.Secure, err = row.Bool(`isSecure`) - if err != nil { - return err - } + // Secure + cookie.Secure, err = row.Bool(`isSecure`) + if err != nil { + return err + } - // HttpOnly - cookie.HttpOnly, err = row.Bool(`isHttpOnly`) - if err != nil { - return err - } + // HttpOnly + cookie.HttpOnly, err = row.Bool(`isHttpOnly`) + if err != nil { + return err + } - // Container - if s.Containers != nil { - ucidStr, _ := row.String(`originAttributes`) - prefixContextID := `^userContextId=` - if len(ucidStr) > 0 && strings.HasPrefix(ucidStr, prefixContextID) { - ucidStr = strings.TrimPrefix(ucidStr, prefixContextID) - cookie.Container = ucidStr - ucid, err := strconv.Atoi(ucidStr) - if err == nil { - contName, okContName := s.Containers[ucid] - if okContName && len(contName) > 0 { - cookie.Container += `|` + contName + // Container + if s.Containers != nil { + ucidStr, _ := row.String(`originAttributes`) + prefixContextID := `^userContextId=` + if len(ucidStr) > 0 && strings.HasPrefix(ucidStr, prefixContextID) { + ucidStr = strings.TrimPrefix(ucidStr, prefixContextID) + cookie.Container = ucidStr + ucid, err := strconv.Atoi(ucidStr) + if err == nil { + contName, okContName := s.Containers[ucid] + if okContName && len(contName) > 0 { + cookie.Container += `|` + contName + } } } } - } - if kooky.FilterCookie(&cookie, filters...) { - cookies = append(cookies, &cookie) - } + cookie.Browser = s - return nil - }) - if err != nil { - return nil, err + if !iterx.CookieFilterYield(context.Background(), &cookie, nil, yield, filters...) { + return iterx.ErrYieldEnd + } + return nil + } + } + seq := func(yield func(*kooky.Cookie, error) bool) { + err := utils.VisitTableRows(s.Database, `moz_cookies`, map[string]string{}, visitor(yield)) + if !errors.Is(err, iterx.ErrYieldEnd) { + yield(nil, err) + } } - return cookies, nil + return seq } diff --git a/internal/ie/cookiestore.go b/internal/ie/cookiestore.go index 44586ef..c494f87 100644 --- a/internal/ie/cookiestore.go +++ b/internal/ie/cookiestore.go @@ -62,7 +62,7 @@ func (s *ESECookieStore) Open() error { } // TODO: create temporary copy of the file on Windows - a service on Windows has a permanent lock on it // TODO: remove temporary copy in Close() - if f, err := os.Open(s.FileNameStr); err != nil { + if f, err := utils.OpenFile(s.FileNameStr); err != nil { return err } else { s.File = f @@ -102,18 +102,16 @@ func (s *ESECookieStore) Close() error { } func GetCookieStore(filename, browser string, m map[string]func(f *os.File, s *CookieStore, browser string), filters ...kooky.Filter) (*cookies.CookieJar, error) { - var s CookieStore + s := &CookieStore{} f, typ, err := utils.DetectFileType(filename) if err != nil { return nil, err } - if m != nil { - for name, fn := range m { - if f != nil && typ == name { - fn(f, &s, browser) - goto end - } + for name, fn := range m { + if f != nil && typ == name { + fn(f, s, browser) + goto end } } switch typ { @@ -135,5 +133,5 @@ func GetCookieStore(filename, browser string, m map[string]func(f *os.File, s *C } end: - return &cookies.CookieJar{CookieStore: &s}, nil + return cookies.NewCookieJar(s, filters...), nil } diff --git a/internal/ie/ese.go b/internal/ie/ese.go index 180486c..9661c76 100644 --- a/internal/ie/ese.go +++ b/internal/ie/ese.go @@ -1,129 +1,148 @@ package ie import ( + "context" "encoding/binary" "encoding/hex" "errors" "strings" "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/iterx" "github.com/xiazemin/kooky/internal/timex" "github.com/Velocidex/ordereddict" "www.velocidex.com/golang/go-ese/parser" ) -func (s *ESECookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cookie, error) { +func (s *ESECookieStore) TraverseCookies(filters ...kooky.Filter) kooky.CookieSeq { if s == nil { - return nil, errors.New(`cookie store is nil`) + return iterx.ErrCookieSeq(errors.New(`cookie store is nil`)) } if err := s.Open(); err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } else if s.File == nil { - return nil, errors.New(`file is nil`) + return iterx.ErrCookieSeq(errors.New(`file is nil`)) } if s.ESECatalog == nil { - return nil, errors.New(`ESE catalog is nil`) + return iterx.ErrCookieSeq(errors.New(`ESE catalog is nil`)) } tables := s.ESECatalog.Tables if tables == nil { - return nil, errors.New(`catalog.Tables is nil`) + return iterx.ErrCookieSeq(errors.New(`catalog.Tables is nil`)) } - var cookies []*kooky.Cookie - var errs []error - - cbCookieEntries := func(row *ordereddict.Dict) error { - if row == nil { - errs = append(errs, errors.New(`row is nil`)) - return nil - } + cbCookieEntries := func(yield func(*kooky.Cookie, error) bool) func(row *ordereddict.Dict) error { + return func(row *ordereddict.Dict) error { + if row == nil { + if !yield(nil, errors.New(`row is nil`)) { + return iterx.ErrYieldEnd + } + return nil + } + + var cookieEntry webCacheCookieEntry + if e, ok := row.GetInt64(`EntryId`); ok { + cookieEntry.entryID = uint64(e) + } else { + if !yield(nil, errors.New(`no int64 EntryId`)) { + return iterx.ErrYieldEnd + } + return nil + } + if e, ok := row.GetInt64(`MinimizedRDomainLength`); ok { + cookieEntry.minimizedRDomainLength = uint64(e) + } else { + if !yield(nil, errors.New(`no int64 MinimizedRDomainLength`)) { + return iterx.ErrYieldEnd + } + return nil + } + if e, ok := row.GetInt64(`Flags`); ok { + cookieEntry.flags = uint32(e) + } else { + if !yield(nil, errors.New(`no int64 Flags`)) { + return iterx.ErrYieldEnd + } + return nil + } + if e, ok := row.GetInt64(`Expires`); ok { + cookieEntry.expires = e + } else { + if !yield(nil, errors.New(`no int64 Expires`)) { + return iterx.ErrYieldEnd + } + return nil + } + if e, ok := row.GetInt64(`LastModified`); ok { + cookieEntry.lastModified = e + } else { + if !yield(nil, errors.New(`no int64 LastModified`)) { + return iterx.ErrYieldEnd + } + return nil + } + var ok bool + cookieEntry.cookieHash, ok = row.GetString(`CookieHash`) + if !ok { + if !yield(nil, errors.New(`no string CookieHash`)) { + return iterx.ErrYieldEnd + } + return nil + } + cookieEntry.rDomain, ok = row.GetString(`RDomain`) + if !ok { + if !yield(nil, errors.New(`no string RDomain`)) { + return iterx.ErrYieldEnd + } + return nil + } + cookieEntry.path, ok = row.GetString(`Path`) + if !ok { + if !yield(nil, errors.New(`no string Path`)) { + return iterx.ErrYieldEnd + } + return nil + } + cookieEntry.name, ok = row.GetString(`Name`) + if !ok { + if !yield(nil, errors.New(`no string Name`)) { + return iterx.ErrYieldEnd + } + return nil + } + cookieEntry.value, ok = row.GetString(`Value`) + if !ok { + if !yield(nil, errors.New(`no string Value`)) { + return iterx.ErrYieldEnd + } + return nil + } + + cookie, errCookie := convertCookieEntry(&cookieEntry, s) + if !iterx.CookieFilterYield(context.Background(), cookie, errCookie, yield, filters...) { + return nil + } - var cookieEntry webCacheCookieEntry - if e, ok := row.GetInt64(`EntryId`); ok { - cookieEntry.entryID = uint64(e) - } else { - errs = append(errs, errors.New(`no int64 EntryId`)) - return nil - } - if e, ok := row.GetInt64(`MinimizedRDomainLength`); ok { - cookieEntry.minimizedRDomainLength = uint64(e) - } else { - errs = append(errs, errors.New(`no int64 MinimizedRDomainLength`)) - return nil - } - if e, ok := row.GetInt64(`Flags`); ok { - cookieEntry.flags = uint32(e) - } else { - errs = append(errs, errors.New(`no int64 Flags`)) return nil } - if e, ok := row.GetInt64(`Expires`); ok { - cookieEntry.expires = e - } else { - errs = append(errs, errors.New(`no int64 Expires`)) - return nil - } - if e, ok := row.GetInt64(`LastModified`); ok { - cookieEntry.lastModified = e - } else { - errs = append(errs, errors.New(`no int64 LastModified`)) - return nil - } - var ok bool - cookieEntry.cookieHash, ok = row.GetString(`CookieHash`) - if !ok { - errs = append(errs, errors.New(`no string CookieHash`)) - return nil - } - cookieEntry.rDomain, ok = row.GetString(`RDomain`) - if !ok { - errs = append(errs, errors.New(`no string RDomain`)) - return nil - } - cookieEntry.path, ok = row.GetString(`Path`) - if !ok { - errs = append(errs, errors.New(`no string Path`)) - return nil - } - cookieEntry.name, ok = row.GetString(`Name`) - if !ok { - errs = append(errs, errors.New(`no string Name`)) - return nil - } - cookieEntry.value, ok = row.GetString(`Value`) - if !ok { - errs = append(errs, errors.New(`no string Value`)) - return nil - } - - cookie, err := convertCookieEntry(&cookieEntry) - if err != nil { - errs = append(errs, err) - return nil - } - - if kooky.FilterCookie(cookie, filters...) { - cookies = append(cookies, cookie) - } - - return nil } - for _, tableName := range tables.Keys() { - if !strings.HasPrefix(tableName, `CookieEntryEx_`) { - continue - } - if err := s.ESECatalog.DumpTable(tableName, cbCookieEntries); err != nil { - errs = append(errs, err) - err = errorList{Errors: errs} - return nil, err + seq := func(yield func(*kooky.Cookie, error) bool) { + for _, tableName := range tables.Keys() { + if !strings.HasPrefix(tableName, `CookieEntryEx_`) { + continue + } + if err := s.ESECatalog.DumpTable(tableName, cbCookieEntries(yield)); err != nil { + if !yield(nil, err) { + return + } + } } } - - return cookies, nil + return seq } type webCacheCookieEntry struct { @@ -143,6 +162,8 @@ type errorList struct { Errors []error } +var _ error = (*errorList)(nil) + func (l errorList) Error() string { if len(l.Errors) > 0 { return l.Errors[0].Error() + `, additional errors...` @@ -158,7 +179,7 @@ func eseHexDecodeString(raw string) (string, error) { return strings.Split(string(b), "\x00")[0], nil } -func convertCookieEntry(entry *webCacheCookieEntry) (*kooky.Cookie, error) { +func convertCookieEntry(entry *webCacheCookieEntry, bi kooky.BrowserInfo) (*kooky.Cookie, error) { if entry == nil { return nil, errors.New(`cookie entry is nil`) } @@ -195,6 +216,8 @@ func convertCookieEntry(entry *webCacheCookieEntry) (*kooky.Cookie, error) { // TODO: use "CookieEntryEx_##.LastModified" field as "Cookie.Creation" time? + cookie.Browser = bi + return cookie, nil } diff --git a/internal/ie/find/find_windows.go b/internal/ie/find/find_windows.go index cec7a38..c8ba561 100644 --- a/internal/ie/find/find_windows.go +++ b/internal/ie/find/find_windows.go @@ -12,11 +12,11 @@ import ( "github.com/xiazemin/kooky/internal/ie" ) -type finder struct { - browser string +type IEFinder struct { + Browser string } -var _ kooky.CookieStoreFinder = (*finder)(nil) +var _ kooky.CookieStoreFinder = (*IEFinder)(nil) var registerOnce sync.Once @@ -24,83 +24,88 @@ func init() { browser := `ie+edge` // don't register multiple times for files shared between ie and edge registerOnce.Do(func() { - kooky.RegisterFinder(browser, &finder{browser: browser}) + kooky.RegisterFinder(browser, &IEFinder{Browser: browser}) }) } -func (f *finder) FindCookieStores() ([]kooky.CookieStore, error) { - locApp := os.Getenv(`LOCALAPPDATA`) - home := os.Getenv(`USERPROFILE`) - windows := os.Getenv(`windir`) - appData, _ := os.UserConfigDir() +func (f *IEFinder) FindCookieStores() kooky.CookieStoreSeq { + return func(yield func(kooky.CookieStore, error) bool) { + locApp, err := os.UserCacheDir() + if !yield(nil, err) { + return + } + home := os.Getenv(`USERPROFILE`) + windows := os.Getenv(`windir`) + appData, err := os.UserConfigDir() + if !yield(nil, err) { + return + } - type pathStruct struct { - dir string - paths [][]string - } + type pathStruct struct { + dir string + paths [][]string + } - // https://tzworks.com/prototypes/index_dat/id.users.guide.pdf - paths := []pathStruct{ - { - dir: windows, - paths: [][]string{ - []string{`Cookies`}, // IE 4.0 + // https://tzworks.com/prototypes/index_dat/id.users.guide.pdf + paths := []pathStruct{ + { + dir: windows, + paths: [][]string{ + {`Cookies`}, // IE 4.0 + }, }, - }, - { - dir: home, - paths: [][]string{ - []string{`Cookies`}, // XP, Vista + { + dir: home, + paths: [][]string{ + {`Cookies`}, // XP, Vista + }, }, - }, - { - dir: appData, - paths: [][]string{ - []string{`Microsoft`, `Windows`, `Cookies`}, - []string{`Microsoft`, `Windows`, `Cookies`, `Low`}, - []string{`Microsoft`, `Windows`, `Cookies`, `Low`}, - []string{`Microsoft`, `Windows`, `Internet Explorer`, `UserData`, `Low`}, + { + dir: appData, + paths: [][]string{ + {`Microsoft`, `Windows`, `Cookies`}, + {`Microsoft`, `Windows`, `Cookies`, `Low`}, + {`Microsoft`, `Windows`, `Cookies`, `Low`}, + {`Microsoft`, `Windows`, `Internet Explorer`, `UserData`, `Low`}, + }, }, - }, - } - - var cookiesFiles []kooky.CookieStore - for _, p := range paths { - if len(p.dir) == 0 { - continue } - for _, path := range p.paths { - cookiesFiles = append( - cookiesFiles, - &cookies.CookieJar{ + + for _, p := range paths { + if len(p.dir) == 0 { + continue + } + for _, path := range p.paths { + st := &cookies.CookieJar{ CookieStore: &ie.CookieStore{ CookieStore: &ie.IECacheCookieStore{ DefaultCookieStore: cookies.DefaultCookieStore{ - BrowserStr: f.browser, + BrowserStr: f.Browser, IsDefaultProfileBool: true, FileNameStr: filepath.Join(append(append([]string{p.dir}, path...), `index.dat`)...), }, }, }, - }, - ) + } + if !yield(st, nil) { + return + } + } } - } - cookiesFiles = append( - cookiesFiles, - &cookies.CookieJar{ + st := &cookies.CookieJar{ CookieStore: &ie.CookieStore{ CookieStore: &ie.ESECookieStore{ DefaultCookieStore: cookies.DefaultCookieStore{ - BrowserStr: f.browser, + BrowserStr: f.Browser, IsDefaultProfileBool: true, FileNameStr: filepath.Join(locApp, `Microsoft`, `Windows`, `WebCache`, `WebCacheV01.dat`), }, }, }, - }, - ) - - return cookiesFiles, nil + } + if !yield(st, nil) { + return + } + } } diff --git a/internal/ie/ie.go b/internal/ie/ie.go index 6894c5d..60ad6ea 100644 --- a/internal/ie/ie.go +++ b/internal/ie/ie.go @@ -4,13 +4,14 @@ import ( "errors" "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/iterx" ) -func (s *CookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cookie, error) { - if s == nil { - return nil, errors.New(`cookie store is nil`) +func (s *CookieStore) TraverseCookies(filters ...kooky.Filter) kooky.CookieSeq { + if s == nil || s.CookieStore == nil { + return iterx.ErrCookieSeq(errors.New(`cookie store is nil`)) } - return s.CookieStore.ReadCookies(filters...) + return s.CookieStore.TraverseCookies(filters...) } /* diff --git a/internal/ie/iecache.go b/internal/ie/iecache.go index a1a68b8..8006302 100644 --- a/internal/ie/iecache.go +++ b/internal/ie/iecache.go @@ -12,39 +12,40 @@ import ( "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/bytesx" "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/iterx" "github.com/xiazemin/kooky/internal/timex" ) // index.dat parser -func (s *IECacheCookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cookie, error) { +func (s *IECacheCookieStore) TraverseCookies(filters ...kooky.Filter) kooky.CookieSeq { if s == nil { - return nil, errors.New(`cookie store is nil`) + return iterx.ErrCookieSeq(errors.New(`cookie store is nil`)) } if err := s.Open(); err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } else if s.File == nil { - return nil, errors.New(`file is nil`) + return iterx.ErrCookieSeq(errors.New(`file is nil`)) } ieCacheVersion, err := bytesx.ReadString(s.File, "file format version", 0x00, 0x18) if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } if ieCacheVersion != `5.2` { - return nil, errors.New(`unsupported IE url cache version`) + return iterx.ErrCookieSeq(errors.New(`unsupported IE url cache version`)) } offsetHashStart, err := bytesx.ReadOffSetInt64LE(s.File, "hash table offset", 0x00, 0x20) if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } hashSig, err := bytesx.ReadBytesN(s.File, "hash table offset", offsetHashStart, 0x00, 4) if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } if string(hashSig) != `HASH` { - return nil, errors.New(`wrong offset for hash table`) + return iterx.ErrCookieSeq(errors.New(`wrong offset for hash table`)) } // TODO: use hash entries for domain search @@ -52,7 +53,7 @@ func (s *IECacheCookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cook var textCookieStores []*TextCookieStore // TODO: do url records always start at 0x5000? urlSig := []byte(`URL `) - s.File.Seek(0, io.SeekStart) + _, _ = s.File.Seek(0, io.SeekStart) for { offsetURLEntry, err := scanRest(s.File, urlSig) if err != nil { @@ -62,24 +63,24 @@ func (s *IECacheCookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cook blockCount, err := bytesx.ReadOffSetInt64LE(s.File, "block count", offsetURLEntry, 4) if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } dateModification, err := getFILETIME(s.File, "modification date", offsetURLEntry, 8) if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } dateLastAccessed, err := getFILETIME(s.File, "last access date", offsetURLEntry, 16) if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } // probably less accurate copy of DateLastAccessed dateLastChecked, err := getFATTIME(s.File, "last check date", offsetURLEntry, 80) if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } dateExpiry, err := getFATTIME(s.File, "expiry date", offsetURLEntry, 24) if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } entry.BlockCount = blockCount @@ -92,11 +93,11 @@ func (s *IECacheCookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cook // Cookie:@ offsetURLRecordLocation, err := bytesx.ReadOffSetInt64LE(s.File, "location offset", offsetURLEntry, 52) // always 104? if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } location, err := bytesx.ReadString(s.File, "location", offsetURLEntry, offsetURLRecordLocation) if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } if !strings.HasPrefix(location, `Cookie:`) { _, _ = s.File.Seek(offsetURLEntry+int64(len(urlSig)), io.SeekStart) @@ -110,7 +111,7 @@ func (s *IECacheCookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cook entry.Domain = strings.SplitN(locAtParts[1], `/`, 2)[0] directoryIndex, err := bytesx.ReadBytesN(s.File, "directory index", offsetURLEntry, 56, 1) if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } entry.DirectoryIndex = directoryIndex isCookieEntry := string(entry.DirectoryIndex) == string([]byte{0xFE}) @@ -120,40 +121,43 @@ func (s *IECacheCookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cook } formatVersion, err := bytesx.ReadBytesN(s.File, "entry format version", offsetURLEntry, 58, 1) // 0x00 ⇒ IE5_URL_FILEMAP_ENTRY, 0x10 ⇒ IE6_URL_FILEMAP_ENTRY if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } entry.FormatVersion = formatVersion offsetURLRecordFileName, err := bytesx.ReadOffSetInt64LE(s.File, "url record filename offset", offsetURLEntry, 60) + if err != nil { + return iterx.ErrCookieSeq(err) + } fileName, err := bytesx.ReadString(s.File, "file name", offsetURLEntry, offsetURLRecordFileName) if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } entry.FileName = filepath.Join(filepath.Dir(s.FileNameStr), fileName) // https://github.com/libyal/libmsiecf/blob/main/documentation/MSIE%20Cache%20File%20(index.dat)%20format.asciidoc#43-cache-entry-flags flags, err := bytesx.ReadBytesN(s.File, "flags", offsetURLEntry, 64, 4) if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } entry.Flags = binary.LittleEndian.Uint32(flags) // probably no Data in Cookie Entries offsetURLRecordData, err := bytesx.ReadOffSetInt64LE(s.File, "url record data offset", offsetURLEntry, 68) if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } if offsetURLRecordData != 0 { urlRecordDataSize, err := bytesx.ReadBytesN(s.File, "url record data size", offsetURLEntry, 72, 4) if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } data, err := bytesx.ReadBytesN(s.File, "url record data", offsetURLEntry, offsetURLRecordData, binary.LittleEndian.Uint32(urlRecordDataSize)) if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } entry.Data = data } hitsCount, err := bytesx.ReadBytesN(s.File, "hits count", offsetURLEntry, 84, 4) if err != nil { - return nil, err + return iterx.ErrCookieSeq(err) } entry.HitsCount = binary.LittleEndian.Uint32(hitsCount) @@ -168,24 +172,22 @@ func (s *IECacheCookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cook }, }, ) + _ = entries // TODO // TODO: the file name of these nested text cookie stores are not visible to the caller, the index.dat appears as the file source _, _ = s.File.Seek(offsetURLEntry+int64(len(urlSig)), io.SeekStart) } - var ret []*kooky.Cookie - for _, textCookieStore := range textCookieStores { - // TODO: parallelize (internalize kooky/find.go?) - cs, err := textCookieStore.ReadCookies(filters...) - if err == nil { - ret = append( - ret, - cs..., - ) + return func(yield func(*kooky.Cookie, error) bool) { + for _, textCookieStore := range textCookieStores { + // TODO: parallelize (internalize kooky/find.go?) + for cookie, err := range textCookieStore.TraverseCookies(filters...) { + if !yield(cookie, err) { + return + } + } } } - - return ret, nil } type CacheCookieEntry struct { diff --git a/internal/ie/textcookies.go b/internal/ie/textcookies.go index 4718222..bfd9452 100644 --- a/internal/ie/textcookies.go +++ b/internal/ie/textcookies.go @@ -2,12 +2,14 @@ package ie import ( "bufio" + "context" "errors" "strconv" "strings" "github.com/xiazemin/kooky" "github.com/xiazemin/kooky/internal/cookies" + "github.com/xiazemin/kooky/internal/iterx" "github.com/xiazemin/kooky/internal/timex" ) @@ -17,88 +19,106 @@ type TextCookieStore struct { var _ cookies.CookieStore = (*TextCookieStore)(nil) -func (s *TextCookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cookie, error) { +func (s *TextCookieStore) TraverseCookies(filters ...kooky.Filter) kooky.CookieSeq { if s == nil { - return nil, errors.New(`cookie store is nil`) + return iterx.ErrCookieSeq(errors.New(`cookie store is nil`)) } - if err := s.Open(); err != nil { - return nil, err - } else if s.File == nil { - return nil, errors.New(`file is nil`) - } - fi, err := s.File.Stat() - if err != nil { - return nil, err - } - if fi.IsDir() { - // TODO read directory content - *.txt - } - - var ( - lineNr int - expLeast, crtLeast uint64 - cookie *kooky.Cookie - cookies []*kooky.Cookie - ) - scanner := bufio.NewScanner(s.File) - for scanner.Scan() { - lineNr = lineNr%9 + 1 - line := scanner.Text() - switch lineNr { - case 1: - cookie = &kooky.Cookie{} - cookie.Name = line - case 2: - cookie.Value = line - case 3: - sp := strings.SplitN(line, `/`, 2) - cookie.Domain = sp[0] - if len(sp) == 2 { - cookie.Path = `/` + sp[1] - } else { - cookie.Path = `/` - } - case 4: - flags, err := strconv.ParseUint(line, 10, 32) - if err != nil { - return nil, err - } - // TODO: is "Secure" encoded in flags? - cookie.HttpOnly = flags&(1<<13) != 0 - case 5: - var err error - expLeast, err = strconv.ParseUint(line, 10, 32) - if err != nil { - return nil, err - } - case 6: - expMost, err := strconv.ParseUint(line, 10, 32) - if err != nil { - return nil, err - } - cookie.Expires = timex.FromFILETIMESplit(expLeast, expMost) - case 7: - var err error - crtLeast, err = strconv.ParseUint(line, 10, 32) - if err != nil { - return nil, err - } - case 8: - crtMost, err := strconv.ParseUint(line, 10, 32) - if err != nil { - return nil, err - } - cookie.Creation = timex.FromFILETIMESplit(crtLeast, crtMost) - case 9: - // Secure (?) - if line != `*` { - return nil, errors.New(`cookie record delimiter not "*"`) + return func(yield func(*kooky.Cookie, error) bool) { + if err := s.Open(); err != nil { + yield(nil, err) + return + } else if s.File == nil { + yield(nil, errors.New(`file is nil`)) + return + } + fi, err := s.File.Stat() + if err != nil { + yield(nil, err) + return + } + if fi.IsDir() { + // TODO read directory content - *.txt + yield(nil, errors.New(`file is a directory`)) + return + } + var ( + lineNr int + expLeast, crtLeast uint64 + cookie *kooky.Cookie + errCookie error + ) + scanner := bufio.NewScanner(s.File) + for scanner.Scan() { + lineNr = lineNr%9 + 1 + line := scanner.Text() + if errCookie != nil && lineNr != 1 && lineNr != 9 { + continue } - if kooky.FilterCookie(cookie, filters...) { - cookies = append(cookies, cookie) + switch lineNr { + case 1: + cookie = &kooky.Cookie{} + cookie.Name = line + case 2: + cookie.Value = line + case 3: + sp := strings.SplitN(line, `/`, 2) + cookie.Domain = sp[0] + if len(sp) == 2 { + cookie.Path = `/` + sp[1] + } else { + cookie.Path = `/` + } + case 4: + flags, err := strconv.ParseUint(line, 10, 32) + if err != nil { + errCookie = err + continue + } + // TODO: is "Secure" encoded in flags? + cookie.HttpOnly = flags&(1<<13) != 0 + case 5: + var err error + expLeast, err = strconv.ParseUint(line, 10, 32) + if err != nil { + errCookie = err + continue + } + case 6: + expMost, err := strconv.ParseUint(line, 10, 32) + if err != nil { + errCookie = err + continue + } + cookie.Expires = timex.FromFILETIMESplit(expLeast, expMost) + case 7: + var err error + crtLeast, err = strconv.ParseUint(line, 10, 32) + if err != nil { + errCookie = err + continue + } + case 8: + crtMost, err := strconv.ParseUint(line, 10, 32) + if err != nil { + errCookie = err + continue + } + cookie.Creation = timex.FromFILETIMESplit(crtLeast, crtMost) + case 9: + // Secure (?) + if line != `*` { + errCookie = errors.New(`cookie record delimiter not "*"`) + } + if errCookie == nil { + cookie.Browser = s + } else { + cookie = nil + } + if !iterx.CookieFilterYield(context.Background(), cookie, errCookie, yield, filters...) { + return + } + errCookie = nil } } } - - return cookies, nil } diff --git a/internal/iterx/iter.go b/internal/iterx/iter.go new file mode 100644 index 0000000..849a928 --- /dev/null +++ b/internal/iterx/iter.go @@ -0,0 +1,78 @@ +package iterx + +import ( + "cmp" + "context" + "errors" + + "github.com/xiazemin/kooky" +) + +func CookieFilterYield(ctx context.Context, cookie *kooky.Cookie, errCookie error, yield func(*kooky.Cookie, error) bool, filters ...kooky.Filter) bool { + ret := true + if errCookie != nil { + if errors.Is(errCookie, ErrYieldEnd) { + return false + } + ret = yield(nil, errCookie) + } + if kooky.FilterCookie(ctx, cookie, filters...) { + ret = yield(cookie, nil) + } + return ret +} + +func NewCookieFilterYielder(splitFilters bool, filters ...kooky.Filter) func(_ context.Context, yield func(*kooky.Cookie, error) bool, _ *kooky.Cookie, errCookie error, valRetriever func(*kooky.Cookie) error) bool { + var valueFilters, nonValueFilters []kooky.Filter + if splitFilters { + for _, filter := range filters { + if _, ok := filter.(kooky.ValueFilterFunc); ok { + valueFilters = append(valueFilters, filter) + } else { + // these non-value filters can be used for prefiltering before value decryption + nonValueFilters = append(nonValueFilters, filter) + } + } + } + return func(ctx context.Context, yield func(*kooky.Cookie, error) bool, cookie *kooky.Cookie, errCookie error, valRetriever func(*kooky.Cookie) error) bool { + if errCookie != nil { + return yield(nil, errCookie) + } + if cookie == nil { + return true + } + retr := func(cookie *kooky.Cookie) bool { + err := valRetriever(cookie) + return err == nil || (yield(nil, err) && false) + } + done := make(chan struct{}) + var ret, ctxDone bool + go func() { + defer func() { done <- struct{}{} }() + if valRetriever != nil { + ret = !cmp.Or( + !kooky.FilterCookie(ctx, cookie, nonValueFilters...), + !retr(cookie), + !kooky.FilterCookie(ctx, cookie, valueFilters...), + ) + } else { + ret = kooky.FilterCookie(ctx, cookie, filters...) + } + ret = ret && yield(cookie, nil) + }() + select { + case <-ctx.Done(): + ctxDone = true + <-done // wait for current yield to finish + case <-done: + } + return ret && !ctxDone + } +} + +func ErrCookieSeq(e error) kooky.CookieSeq { + return func(yield func(*kooky.Cookie, error) bool) { yield(nil, e) } +} + +// this error should not surface to the library user +var ErrYieldEnd = errors.New(`yield end`) diff --git a/internal/netscape/cookiestore.go b/internal/netscape/cookiestore.go index a653eeb..64d89bf 100644 --- a/internal/netscape/cookiestore.go +++ b/internal/netscape/cookiestore.go @@ -6,12 +6,18 @@ import ( type CookieStore struct { cookies.DefaultCookieStore - IsStrictBool bool + isStrict func() bool } // strict netscape cookies.txt format func (s *CookieStore) IsStrict() bool { - return s != nil && s.IsStrictBool + if s == nil { + return false + } + if s.isStrict == nil { + _, s.isStrict = TraverseCookies(s.File, s) + } + return s.isStrict() } var _ cookies.CookieStore = (*CookieStore)(nil) diff --git a/internal/netscape/netscape.go b/internal/netscape/netscape.go index 1cb05b4..f961c70 100644 --- a/internal/netscape/netscape.go +++ b/internal/netscape/netscape.go @@ -2,67 +2,69 @@ package netscape import ( "bufio" + "context" "errors" + "fmt" "io" "strconv" "strings" "time" "github.com/xiazemin/kooky" + "github.com/xiazemin/kooky/internal/iterx" ) const httpOnlyPrefix = `#HttpOnly_` -func (s *CookieStore) ReadCookies(filters ...kooky.Filter) ([]*kooky.Cookie, error) { +func (s *CookieStore) TraverseCookies(filters ...kooky.Filter) kooky.CookieSeq { if s == nil { - return nil, errors.New(`cookie store is nil`) + return iterx.ErrCookieSeq(errors.New(`cookie store is nil`)) } if err := s.Open(); err != nil { - return nil, err - } else if s.File == nil { - return nil, errors.New(`file is nil`) + return iterx.ErrCookieSeq(err) + } + if s.File == nil { + return iterx.ErrCookieSeq(errors.New(`file is nil`)) } - cookies, isStrict, err := ReadCookies(s.File, filters...) - s.IsStrictBool = isStrict + seq, str := TraverseCookies(s.File, s, filters...) + s.isStrict = str - return cookies, err + return seq } -func ReadCookies(file io.Reader, filters ...kooky.Filter) (c []*kooky.Cookie, strict bool, e error) { +func TraverseCookies(file io.Reader, bi kooky.BrowserInfo, filters ...kooky.Filter) (_ kooky.CookieSeq, isStrict func() bool) { // http://web.archive.org/web/20080520061150/wp.netscape.com/newsref/std/cookie_spec.html // https://github.com/Rob--W/cookie-manager/blob/83c04b74b79cb7768a33c4a93fbdfd04b90fa931/cookie-manager.js#L975 // https://hg.python.org/cpython/file/5470dc81caf9/Lib/http/cookiejar.py#l1981 - if file == nil { - return nil, false, errors.New(`file is nil`) - } - - var ret []*kooky.Cookie - - var lineNr uint - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - if lineNr == 0 && (line == `# HTTP Cookie File` || line == `# Netscape HTTP Cookie File`) { - strict = true - } + parseLine := func(line string, lineNr int, strPtr *bool, yield func(*kooky.Cookie, error) bool) bool { // split line into fields sp := strings.Split(line, "\t") - if len(sp) != 7 { - continue + colCnt := 7 + if l := len(sp); l != colCnt { + if len(line) == 0 || strings.HasPrefix(line, `#`) { + // comment + return true // continue + } + return yield(nil, fmt.Errorf(`row %d: has %d fields; expected are %d: %q`, lineNr+1, l, colCnt, line)) } var exp int64 if len(sp[4]) > 0 { e, err := strconv.ParseInt(sp[4], 10, 64) if err != nil { - continue + return yield(nil, fmt.Errorf(`row %d: Expires field is not an integer: %w`, lineNr+1, err)) } else { exp = e } } else { // allow empty expiry field for uzbl's "session-cookies.txt" file - strict = false + if strPtr != nil && *strPtr { + *strPtr = false + if !yield(nil, ErrNotStrict) { + return false + } + } } cookie := &kooky.Cookie{} @@ -71,7 +73,7 @@ func ReadCookies(file io.Reader, filters ...kooky.Filter) (c []*kooky.Cookie, st cookie.Secure = true case `FALSE`: default: - continue + return yield(nil, fmt.Errorf(`row %d: Secure field is not a bool`, lineNr+1)) } // https://github.com/curl/curl/blob/curl-7_39_0/lib/cookie.c#L644 @@ -88,13 +90,54 @@ func ReadCookies(file io.Reader, filters ...kooky.Filter) (c []*kooky.Cookie, st cookie.Name = sp[5] cookie.Value = strings.TrimSpace(sp[6]) cookie.Expires = time.Unix(exp, 0) + cookie.Browser = bi + + return iterx.CookieFilterYield(context.Background(), cookie, nil, yield, filters...) + } + + var strict bool + done := make(chan struct{}, 1) + isStrict = func() bool { + <-done + return strict + } + + seq := func(yield func(*kooky.Cookie, error) bool) { + defer func() { done <- struct{}{} }() - if !kooky.FilterCookie(cookie, filters...) { - continue + if file == nil { + yield(nil, errors.New(`file is nil`)) + return } - ret = append(ret, cookie) + var lineNr int + scanner := bufio.NewScanner(file) + if !scanner.Scan() { + yield(nil, errors.New(`file has no lines`)) + return + } + line := scanner.Text() + if line == `# HTTP Cookie File` || line == `# Netscape HTTP Cookie File` { + strict = true + } else { + if !yield(nil, ErrNotStrict) { + return + } + } + lineNr++ + if !parseLine(line, lineNr, &strict, yield) { + return + } + for scanner.Scan() { + line := scanner.Text() + lineNr++ + if !parseLine(line, lineNr, &strict, yield) { + return + } + } } - return ret, strict, nil + return seq, isStrict } + +var ErrNotStrict = errors.New(`netscape cookie file: file format not strictly followed`) diff --git a/internal/utils/magiclite.go b/internal/utils/magiclite.go index f67edad..7bfc765 100644 --- a/internal/utils/magiclite.go +++ b/internal/utils/magiclite.go @@ -27,7 +27,7 @@ var signatures = map[string][]signature{ } func DetectFileType(filename string) (f *os.File, typ string, e error) { - f, err := os.Open(filename) + f, err := OpenFile(filename) if err != nil { return nil, ``, err } diff --git a/internal/utils/open.go b/internal/utils/open.go new file mode 100644 index 0000000..c65f305 --- /dev/null +++ b/internal/utils/open.go @@ -0,0 +1,5 @@ +package utils + +// OpenFile is like os.Open but might be able to open some locked files. +// The windows implementation sets FILE_SHARE_DELETE in addition. +var OpenFile = openFile diff --git a/internal/utils/open_others.go b/internal/utils/open_others.go new file mode 100644 index 0000000..85d3cea --- /dev/null +++ b/internal/utils/open_others.go @@ -0,0 +1,7 @@ +//go:build !windows + +package utils + +import "os" + +var openFile = os.Open diff --git a/internal/utils/open_windows.go b/internal/utils/open_windows.go new file mode 100644 index 0000000..e50ac22 --- /dev/null +++ b/internal/utils/open_windows.go @@ -0,0 +1,112 @@ +//go:build windows + +package utils + +import ( + "os" + "syscall" + "unsafe" +) + +const ( + _ERROR_BAD_NETPATH = syscall.Errno(53) +) + +func makeInheritSa() *syscall.SecurityAttributes { + var sa syscall.SecurityAttributes + sa.Length = uint32(unsafe.Sizeof(sa)) + sa.InheritHandle = 1 + return &sa +} + +// copied from the Go standard library which underlies a BSD-3 License. +// We set FILE_SHARE_DELETE in addition + +// https://cs.opensource.google/go/go/+/refs/tags/go1.22.3:src/syscall/syscall_windows.go;drc=d33ad2d8f357d83dfdc14c3358e3956aac76a9b0;l=342 + +func sysOpen(path string, mode int, perm uint32) (fd syscall.Handle, err error) { + if len(path) == 0 { + return syscall.InvalidHandle, syscall.ERROR_FILE_NOT_FOUND + } + pathp, err := syscall.UTF16PtrFromString(path) + if err != nil { + return syscall.InvalidHandle, err + } + var access uint32 + switch mode & (syscall.O_RDONLY | syscall.O_WRONLY | syscall.O_RDWR) { + case syscall.O_RDONLY: + access = syscall.GENERIC_READ + case syscall.O_WRONLY: + access = syscall.GENERIC_WRITE + case syscall.O_RDWR: + access = syscall.GENERIC_READ | syscall.GENERIC_WRITE + } + if mode&syscall.O_CREAT != 0 { + access |= syscall.GENERIC_WRITE + } + if mode&syscall.O_APPEND != 0 { + access &^= syscall.GENERIC_WRITE + access |= syscall.FILE_APPEND_DATA + } + // mod with FILE_SHARE_DELETE + sharemode := uint32(syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE | syscall.FILE_SHARE_DELETE) + var sa *syscall.SecurityAttributes + if mode&syscall.O_CLOEXEC == 0 { + sa = makeInheritSa() + } + var createmode uint32 + switch { + case mode&(syscall.O_CREAT|syscall.O_EXCL) == (syscall.O_CREAT | syscall.O_EXCL): + createmode = syscall.CREATE_NEW + case mode&(syscall.O_CREAT|syscall.O_TRUNC) == (syscall.O_CREAT | syscall.O_TRUNC): + createmode = syscall.CREATE_ALWAYS + case mode&syscall.O_CREAT == syscall.O_CREAT: + createmode = syscall.OPEN_ALWAYS + case mode&syscall.O_TRUNC == syscall.O_TRUNC: + createmode = syscall.TRUNCATE_EXISTING + default: + createmode = syscall.OPEN_EXISTING + } + var attrs uint32 = syscall.FILE_ATTRIBUTE_NORMAL + if perm&syscall.S_IWRITE == 0 { + attrs = syscall.FILE_ATTRIBUTE_READONLY + if createmode == syscall.CREATE_ALWAYS { + // We have been asked to create a read-only file. + // If the file already exists, the semantics of + // the Unix open system call is to preserve the + // existing permissions. If we pass CREATE_ALWAYS + // and FILE_ATTRIBUTE_READONLY to CreateFile, + // and the file already exists, CreateFile will + // change the file permissions. + // Avoid that to preserve the Unix semantics. + h, e := syscall.CreateFile(pathp, access, sharemode, sa, syscall.TRUNCATE_EXISTING, syscall.FILE_ATTRIBUTE_NORMAL, 0) + switch e { + case syscall.ERROR_FILE_NOT_FOUND, _ERROR_BAD_NETPATH, syscall.ERROR_PATH_NOT_FOUND: + // File does not exist. These are the same + // errors as Errno.Is checks for ErrNotExist. + // Carry on to create the file. + default: + // Success or some different error. + return h, e + } + } + } + if createmode == syscall.OPEN_EXISTING && access == syscall.GENERIC_READ { + // Necessary for opening directory handles. + attrs |= syscall.FILE_FLAG_BACKUP_SEMANTICS + } + if mode&syscall.O_SYNC != 0 { + const _FILE_FLAG_WRITE_THROUGH = 0x80000000 + attrs |= _FILE_FLAG_WRITE_THROUGH + } + return syscall.CreateFile(pathp, access, sharemode, sa, createmode, attrs, 0) +} + +func openFile(name string) (*os.File, error) { + fd, err := sysOpen(name, os.O_RDONLY, 0) + if err != nil { + return nil, err + } + f := os.NewFile(uintptr(fd), name) + return f, nil +} diff --git a/internal/utils/tablerow.go b/internal/utils/tablerow.go index f4c6e8b..122df09 100644 --- a/internal/utils/tablerow.go +++ b/internal/utils/tablerow.go @@ -2,6 +2,7 @@ package utils import ( "fmt" + "reflect" "github.com/go-sqlite/sqlite3" ) @@ -11,25 +12,12 @@ type TableRow struct { record *sqlite3.Record } +func (row TableRow) String(columnName string) (string, error) { return Value[string](row, columnName) } func (row TableRow) BytesOrFallback(columnName string, fallback []byte) ([]byte, error) { - rawValue := row.ValueOrFallback(columnName, nil) - if value, ok := rawValue.([]byte); ok { - return value, nil - } - return nil, fmt.Errorf("expected column [%s] to be []byte; got %T with value %[2]v", columnName, rawValue) + return ValueOrFallback(row, columnName, fallback, false) } func (row TableRow) BytesStringOrFallback(columnName string, fallback []byte) ([]byte, error) { - rawValue := row.ValueOrFallback(columnName, nil) - if value, ok := rawValue.([]byte); ok { - return value, nil - } - switch value := rawValue.(type) { - case []byte: - return value, nil - case string: - return []byte(value), nil - } - return nil, fmt.Errorf("expected column [%s] to be []byte or string; got %T with value %[2]v", columnName, rawValue) + return ValueOrFallback(row, columnName, fallback, true) } func (row TableRow) Bool(columnName string) (bool, error) { @@ -62,18 +50,7 @@ func (row TableRow) Int64(columnName string) (int64, error) { } } -func (row TableRow) String(columnName string) (string, error) { - rawValue, err := row.Value(columnName) - if err != nil { - return "", err - } - if value, ok := rawValue.(string); ok { - return value, nil - } - return "", fmt.Errorf("expected column [%s] to be string; got %T with value %[2]v", columnName, rawValue) -} - -func (row TableRow) Value(columnName string) (interface{}, error) { +func (row TableRow) Value(columnName string) (any, error) { if index, ok := row.columns[columnName]; !ok { return nil, fmt.Errorf("table doesn't have a column named [%s]", columnName) } else if count := len(row.columns); count <= index { @@ -83,9 +60,45 @@ func (row TableRow) Value(columnName string) (interface{}, error) { } } -func (row TableRow) ValueOrFallback(columnName string, fallback interface{}) interface{} { +func (row TableRow) ValueOrFallback(columnName string, fallback any) any { if index, ok := row.columns[columnName]; ok && index < len(row.columns) { return row.record.Values[index] } return fallback } + +func Value[T any](row TableRow, columnName string) (T, error) { + var zero T + if index, ok := row.columns[columnName]; !ok { + return zero, fmt.Errorf("table doesn't have a column named [%s]", columnName) + } else if count := len(row.columns); count <= index { + return zero, fmt.Errorf("column named [%s] has index %d but row only has %d values", columnName, index, count) + } else if v, ok := row.record.Values[index].(T); !ok { + return zero, fmt.Errorf("expected column [%s] to be type %T; got type %[3]T with value %[3]v", columnName, zero, row.record.Values[index]) + } else { + return v, nil + } +} + +func ValueOrFallback[T any](row TableRow, columnName string, fallback T, tryConvert bool) (T, error) { + index, ok := row.columns[columnName] + if !ok || index >= len(row.columns) || index < 0 { + return fallback, fmt.Errorf("expected column [%s] does not exist", columnName) + } + v := row.record.Values[index] + vt, ok := v.(T) + if ok { + return vt, nil + } + var zero T + if !tryConvert { + var zero T + return fallback, fmt.Errorf("expected column [%s] to be type %T; got type %[3]T with value %[3]v", columnName, zero, v) + } + rv := reflect.ValueOf(v) + rt := reflect.TypeFor[T]() + if !rv.CanConvert(rt) { + return fallback, fmt.Errorf("expected column [%s] to be type %T; got type %[3]T with value %[3]v; using fallback: %v", columnName, zero, v, fallback) + } + return rv.Convert(rt).Interface().(T), nil +} diff --git a/kooky.go b/kooky.go index 63c14f7..19f6812 100644 --- a/kooky.go +++ b/kooky.go @@ -1,16 +1,294 @@ package kooky import ( + "context" + "encoding/json" + "errors" + "iter" "net/http" + "sync" "time" ) -// TODO(zellyn): figure out what to do with quoted values, like the "bcookie" cookie -// from slideshare.net - -// Cookie is the struct returned by functions in this package. Similar to http.Cookie. +// Cookie is an http.Cookie augmented with information obtained through the scraping process. type Cookie struct { http.Cookie Creation time.Time Container string + Browser BrowserInfo +} + +// Cookie retrieving functions in this package like TraverseCookies(), ReadCookies(), AllCookies() +// use registered cookiestore finders to read cookies. +// Erronous reads are skipped. +// +// Register cookie store finders for all browsers like this: +// +// import _ "github.com/xiazemin/kooky/browser/all" +// +// Or only a specific browser: +// +// import _ "github.com/xiazemin/kooky/browser/chrome" +func ReadCookies(ctx context.Context, filters ...Filter) (Cookies, error) { + return TraverseCookies(ctx).ReadAllCookies(ctx) +} + +func AllCookies(filters ...Filter) Cookies { + // for convenience... + ctx := context.Background() + return TraverseCookies(ctx).Collect(ctx) +} + +// adjustments to the json marshaling to allow dates with more than 4 year digits +// https://github.com/golang/go/issues/4556 +// https://github.com/golang/go/issues/54580 +// encoding/json/v2 "format"(?) might make this unnecessary + +func (c *Cookie) MarshalJSON() ([]byte, error) { + if c == nil { + return []byte(`null`), nil + } + c2 := &struct { + // net/http.Cookie + Name string `json:"name"` + Value string `json:"value"` + Quoted bool `json:"quoted"` + Path string `json:"path"` + Domain string `json:"domain"` + Expires jsonTime `json:"expires"` + RawExpires string `json:"raw_expires,omitempty"` + MaxAge int `json:"max_age"` + Secure bool `json:"secure"` + HttpOnly bool `json:"http_only"` + SameSite http.SameSite `json:"same_site"` + Partitioned bool `json:"partitioned"` + Raw string `json:"raw,omitempty"` + Unparsed []string `json:"unparsed,omitempty"` + // extra fields + Creation jsonTime `json:"creation"` + Browser string `json:"browser,omitempty"` + Profile string `json:"profile,omitempty"` + IsDefaultProfile bool `json:"is_default_profile"` + Container string `json:"container,omitempty"` + FilePath string `json:"file_path,omitempty"` + }{ + Name: c.Cookie.Name, + Value: c.Cookie.Value, + Quoted: c.Cookie.Quoted, + Path: c.Cookie.Path, + Domain: c.Cookie.Domain, + Expires: jsonTime{c.Cookie.Expires}, + RawExpires: c.Cookie.RawExpires, + MaxAge: c.Cookie.MaxAge, + Secure: c.Cookie.Secure, + HttpOnly: c.Cookie.HttpOnly, + SameSite: c.Cookie.SameSite, + Partitioned: c.Cookie.Partitioned, + Raw: c.Cookie.Raw, + Unparsed: c.Cookie.Unparsed, + Creation: jsonTime{c.Creation}, + Container: c.Container, + } + if c.Browser != nil { + c2.Browser = c.Browser.Browser() + c2.Profile = c.Browser.Profile() + c2.IsDefaultProfile = c.Browser.IsDefaultProfile() + c2.FilePath = c.Browser.FilePath() + } + return json.Marshal(c2) +} + +type jsonTime struct{ time.Time } + +// MarshalJSON implements the [json.Marshaler] interface. +// The time is a quoted string in the RFC 3339 format with sub-second precision. +// the timestamp might be represented as invalid RFC 3339 if necessary (year with more than 4 digits). +func (t jsonTime) MarshalJSON() ([]byte, error) { + if b, err := t.Time.MarshalJSON(); err == nil { + return b, nil + } + return []byte(t.Time.Format(`"` + time.RFC3339 + `"`)), nil +} + +// for-rangeable cookie retriever +type CookieSeq iter.Seq2[*Cookie, error] + +func TraverseCookies(ctx context.Context, filters ...Filter) CookieSeq { + return TraverseCookieStores(ctx).TraverseCookies(ctx, filters...) +} + +// Collect() is the same as ReadAllCookies but ignores the error +func (s CookieSeq) Collect(ctx context.Context) Cookies { + cookies, _ := s.ReadAllCookies(ctx) + return cookies +} + +func (s CookieSeq) ReadAllCookies(ctx context.Context) (Cookies, error) { + if s == nil { + return nil, errors.New(`nil receiver`) + } + var ( + errs []error + cookies []*Cookie + ) +Outer: + for cookie, err := range s { + if err != nil { + errs = append(errs, err) + continue + } + if cookie != nil { + cookies = append(cookies, cookie) + } + select { + case <-ctx.Done(): + errs = append(errs, errors.New(`context cancel`)) + break Outer + default: + } + } + return cookies, errors.Join(errs...) +} + +// sequence of non-nil cookies and nil errors +func (s CookieSeq) OnlyCookies() CookieSeq { + return func(yield func(*Cookie, error) bool) { + if s == nil { + return + } + for cookie, err := range s { + if err != nil || cookie == nil { + continue + } + if !yield(cookie, nil) { + return + } + } + } +} + +func (s CookieSeq) Filter(ctx context.Context, filters ...Filter) CookieSeq { + return func(yield func(*Cookie, error) bool) { + if s == nil { + yield(nil, errors.New(`nil receiver`)) + return + } + for cookie, errCookie := range s { + if errCookie != nil { + if !yield(nil, errCookie) { + return + } + continue + } + if cookie == nil { + continue + } + select { + case <-ctx.Done(): + return + default: + } + if !FilterCookie(ctx, cookie, filters...) { + continue + } + if !yield(cookie, nil) { + return + } + } + } +} + +func (s CookieSeq) FirstMatch(ctx context.Context, filters ...Filter) *Cookie { + if s == nil { + return nil + } + for cookie, _ := range s.OnlyCookies() { + select { + case <-ctx.Done(): + return nil + default: + } + if FilterCookie(ctx, cookie, filters...) { + return cookie + } + } + return nil +} + +func (s CookieSeq) Merge(seqs ...CookieSeq) CookieSeq { return MergeCookieSeqs(append(seqs, s)...) } + +func MergeCookieSeqs(seqs ...CookieSeq) CookieSeq { + var sq []iter.Seq2[*Cookie, error] + for _, s := range seqs { + sq = append(sq, iter.Seq2[*Cookie, error](s)) + } + return CookieSeq(mergeSeqs(sq...)) +} + +func mergeSeqs[S iter.Seq2[T, error], T any](seqs ...S) S { + seqs0 := func(yield func(T, error) bool) {} + seqs2 := func(yield func(T, error) bool) { + var wg sync.WaitGroup + defer wg.Wait() + wg.Add(len(seqs) + 1) + runner := func(seq S) { + defer wg.Done() + if seq == nil { + return + } + for v, error := range seq { + if !yield(v, error) { + return + } + } + } + for _, seq := range seqs { + go runner(seq) + } + } + switch len(seqs) { + case 0: + return seqs0 + case 1: + return seqs[0] + default: + return seqs2 + } +} + +func (s CookieSeq) Chan(ctx context.Context) <-chan *Cookie { + cookieChan := make(chan *Cookie) + go func() { + defer close(cookieChan) + for cookie, err := range s { + select { + case <-ctx.Done(): + return + default: + } + if err != nil || cookie == nil { + continue + } + cookieChan <- cookie + } + }() + return cookieChan +} + +type Cookies []*Cookie + +func (c Cookies) Seq() CookieSeq { + return func(yield func(*Cookie, error) bool) { + if c == nil { + return + } + for _, cookie := range c { + if cookie == nil { + continue + } + if !yield(cookie, nil) { + return + } + } + } }