kooky

package module
v0.0.0-...-f9d77c8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 11, 2024 License: MIT Imports: 6 Imported by: 0

README

kooky

PkgGoDev Go Report Card Lines of code No Maintenance Intended PRs Welcome MIT license

Reaching into browser-specific, vaguely documented, possibly concurrently modified cookie stores to pilfer cookies is a bad idea. Since you've arrived here, you're almost certainly going to do it anyway. Me too. And if we're going to do the Wrong Thing, at least let's try to Do it Right.

Package kooky contains routines to reach into cookie stores for Chrome, Firefox, Safari, ... and retrieve the cookies.

It aspires to be pure Go (I spent quite a while making go-sqlite/sqlite3 work for it).

It also aspires to work for all major browsers, on all three major platforms.

Status

Basic functionality works on Windows, MacOS and Linux. Some functions might not yet be implemented on some platforms. The API is currently not expected to be at all stable.

PRs more than welcome.

TODOs

  • Set up CI
  • Make it work on Windows. (Look at this and this to learn how to decrypt.)
  • Handle rows in Chrome's cookie DB with other than 14 columns (?)

Example usage

package main

import (
	"fmt"

	"github.com/etng/kooky"
	_ "github.com/etng/kooky/browser/all" // register cookie store finders!
)

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`))

	for _, cookie := range cookies {
		fmt.Println(cookie.Domain, cookie.Name, cookie.Value)
	}
 }
Chrome on macOS
package main

import (
	"fmt"
	"log"
	"os"

	"github.com/etng/kooky/browser/chrome"
)

func main() {
	dir, _ := os.UserConfigDir() // "/<USER>/Library/Application Support/"
	cookiesFile := dir + "/Google/Chrome/Default/Cookies"
	cookies, err := chrome.ReadCookies(cookiesFile)
	if err != nil {
		log.Fatal(err)
	}
	for _, cookie := range cookies {
		fmt.Println(cookie)
	}
}
Safari
package main

import (
	"fmt"
	"log"
	"os"

	"github.com/etng/kooky/browser/safari"
)

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 {
		fmt.Println(cookie)
	}
}

Thanks/references

Documentation

Overview

Package kooky contains routines to reach into cookie stores for various browsers and retrieve the cookies.

Example (ChromeSimpleMacOS)
package main

import (
	"fmt"
	"os"

	"github.com/etng/kooky/browser/chrome"
)

// on macOS:
var cookieStorePath = "/Google/Chrome/Default/Cookies"

func main() {
	// construct file path for the sqlite database containing the cookies
	dir, _ := os.UserConfigDir() // on macOS: "/<USER>/Library/Application Support/"
	cookieStoreFile := dir + cookieStorePath

	// 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 {
		fmt.Println(cookie)
	}
}
Output:

Example (CookieJar)
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"strings"

	"github.com/etng/kooky"
	_ "github.com/etng/kooky/browser/firefox"
)

func main() {
	stores := kooky.FindAllCookieStores()
	var s kooky.CookieStore
	for _, store := range stores {
		if store.Browser() != `firefox` || !store.IsDefaultProfile() {
			continue
		}
		s = store
		break
	}
	// jar := s
	// only store cookies relevant for the target website in the cookie jar
	jar, _ := s.SubJar(kooky.Domain(`github.com`))

	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 {
		log.Fatal(`not logged in`)
	}

	client := http.Client{Jar: jar}
	resp, _ := client.Get(u.String())
	body, _ := io.ReadAll(resp.Body)
	if !strings.Contains(string(body), `id="user_profile_name"`) {
		fmt.Print("not ")
	}
	fmt.Println("logged in")

}
Output:

logged in

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ExportCookies

func ExportCookies[T Cookie | http.Cookie](w io.Writer, cookies []*T)

ExportCookies() export "cookies" in the Netscape format.

curl, wget, ... use this format.

Example
package main

import (
	"net/http"
	"os"

	"github.com/etng/kooky"
)

var cookieFile = `cookies.txt`

func main() {
	var cookies = []*kooky.Cookie{{Cookie: http.Cookie{Domain: `.test.com`, Name: `test`, Value: `dGVzdA==`}}}

	file, err := os.OpenFile(cookieFile, os.O_RDWR|os.O_CREATE, 0644)
	if err != nil {
		// TODO: handle error
		return
	}
	defer file.Close()

	kooky.ExportCookies(file, cookies)
}
Output:

func FilterCookie

func FilterCookie[T Cookie | http.Cookie](cookie *T, filters ...Filter) bool

FilterCookie() tells if a "cookie" passes all "filters".

func FilterCookies

func FilterCookies[T Cookie | http.Cookie](cookies []*T, filters ...Filter) []*T

FilterCookies() applies "filters" in order to the "cookies".

Example
package main

import (
	"github.com/etng/kooky"
	_ "github.com/etng/kooky/browser/all" // register cookiestore finders
)

var cookieName = `NID`

func main() {
	cookies := kooky.ReadCookies() // automatic read

	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
	)
}
Output:

func RegisterFinder

func RegisterFinder(browser string, finder CookieStoreFinder)

RegisterFinder() registers CookieStoreFinder enabling automatic finding of cookie stores with FindAllCookieStores() and ReadCookies().

RegisterFinder() is called by init() in the browser subdirectories.

Types

type Cookie struct {
	http.Cookie
	Creation  time.Time
	Container string
}

Cookie is the struct returned by functions in this package. Similar to http.Cookie.

func ReadCookies

func ReadCookies(filters ...Filter) []*Cookie

ReadCookies() uses registered cookiestore finders to read cookies. Erronous reads are skipped.

Register cookie store finders for all browsers like this:

import _ "github.com/etng/kooky/browser/all"

Or only a specific browser:

import _ "github.com/etng/kooky/browser/chrome"
Example (All)
package main

import (
	"fmt"

	"github.com/etng/kooky"
	_ "github.com/etng/kooky/browser/all" // This registers all cookiestore finders!
	// _ "github.com/etng/kooky/browser/chrome" // load only the chrome cookiestore finder
)

func main() {
	// try to find cookie stores in default locations and
	// read the cookies from them.
	// decryption is handled automatically.
	cookies := kooky.ReadCookies()

	for _, cookie := range cookies {
		fmt.Println(cookie)
	}
}

var _ struct{} // ignore this - for correct working of the documentation tool
Output:

type CookieStore

type CookieStore interface {
	http.CookieJar
	SubJar(filters ...Filter) (http.CookieJar, error)
	ReadCookies(...Filter) ([]*Cookie, error)
	Browser() string
	Profile() string
	IsDefaultProfile() bool
	FilePath() string
	Close() error
}

CookieStore represents a file, directory, etc containing cookies.

Call CookieStore.Close() after using any of its methods.

func FindAllCookieStores

func FindAllCookieStores() []CookieStore

FindAllCookieStores() tries to find cookie stores at default locations.

FindAllCookieStores() requires registered CookieStoreFinders.

Register cookie store finders for all browsers like this:

import _ "github.com/etng/kooky/browser/all"

Or only a specific browser:

import _ "github.com/etng/kooky/browser/chrome"
Example
package main

import (
	"fmt"

	"github.com/etng/kooky"
	_ "github.com/etng/kooky/browser/all"
)

func main() {
	cookieStores := kooky.FindAllCookieStores()

	for _, store := range cookieStores {
		// CookieStore keeps files/databases open for repeated reads
		// close those when no longer needed
		defer store.Close()

		var filters = []kooky.Filter{
			kooky.Valid, // remove expired cookies
		}

		cookies, _ := store.ReadCookies(filters...)
		for _, cookie := range cookies {
			fmt.Printf(
				"%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
				store.Browser(),
				store.Profile(),
				store.FilePath(),
				cookie.Domain,
				cookie.Name,
				cookie.Value,
				cookie.Expires.Format(`2006.01.02 15:04:05`),
			)
		}
	}
}
Output:

type CookieStoreFinder

type CookieStoreFinder interface {
	FindCookieStores() ([]CookieStore, error)
}

CookieStoreFinder tries to find cookie stores at default locations.

type Filter

type Filter interface{ Filter(*Cookie) bool }

Filter is used for filtering cokies in ReadCookies() functions.

A cookie passes the Filter if Filter.Filter returns true.

Example (Regex)
package main

import (
	"fmt"
	"net/http"
	"regexp"

	"github.com/etng/kooky"
)

// example regex matching base64 strings
var reBase64 = regexp.MustCompile(`^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$`)

func main() {
	var cookies = []*kooky.Cookie{{Cookie: http.Cookie{Name: `test`, Value: `dGVzdA==`}}}

	cookies = kooky.FilterCookies(
		cookies,
		ValueRegexMatch(reBase64), // filter cookies with the regex filter
		// kooky.Debug,            // print cookies after applying the regex filter
	)

	for _, cookie := range cookies {
		fmt.Println(cookie.Value)
		break // only first element
	}

}

func ValueRegexMatch(re *regexp.Regexp) kooky.Filter {
	return kooky.FilterFunc(func(cookie *kooky.Cookie) bool {
		return cookie != nil && re != nil && re.Match([]byte(cookie.Value))
	})
}
Output:

dGVzdA==
var Debug Filter = FilterFunc(func(cookie *Cookie) bool {
	fmt.Printf("%+#v\n", cookie)
	return true
})

Debug prints the cookie.

Position Debug after the filter you want to test.

var Expired Filter = FilterFunc(func(cookie *Cookie) bool {
	return cookie != nil && cookie.Expires.Before(time.Now())
})
var HTTPOnly Filter = FilterFunc(func(cookie *Cookie) bool {
	return cookie != nil && cookie.HttpOnly
})
var Secure Filter = FilterFunc(func(cookie *Cookie) bool {
	return cookie != nil && cookie.Secure
})
var Valid Filter = FilterFunc(func(cookie *Cookie) bool {
	return cookie != nil && cookie.Expires.After(time.Now()) && cookie.Cookie.Valid() == nil
})

func CreationAfter

func CreationAfter(u time.Time) Filter

func CreationBefore

func CreationBefore(u time.Time) Filter

func Domain

func Domain(domain string) Filter

func DomainContains

func DomainContains(substr string) Filter

func DomainHasPrefix

func DomainHasPrefix(prefix string) Filter

func DomainHasSuffix

func DomainHasSuffix(suffix string) Filter

func ExpiresAfter

func ExpiresAfter(u time.Time) Filter

func ExpiresBefore

func ExpiresBefore(u time.Time) Filter

func Name

func Name(name string) Filter

func NameContains

func NameContains(substr string) Filter

func NameHasPrefix

func NameHasPrefix(prefix string) Filter

func NameHasSuffix

func NameHasSuffix(suffix string) Filter

func Path

func Path(path string) Filter

func PathContains

func PathContains(substr string) Filter

func PathDepth

func PathDepth(depth int) Filter

func PathHasPrefix

func PathHasPrefix(prefix string) Filter

func PathHasSuffix

func PathHasSuffix(suffix string) Filter

func Value

func Value(value string) Filter

func ValueContains

func ValueContains(substr string) Filter

func ValueHasPrefix

func ValueHasPrefix(prefix string) Filter

func ValueHasSuffix

func ValueHasSuffix(suffix string) Filter

func ValueLen

func ValueLen(length int) Filter

type FilterFunc

type FilterFunc func(*Cookie) bool

func (FilterFunc) Filter

func (f FilterFunc) Filter(c *Cookie) bool

Directories

Path Synopsis
browser
all
browsh
Browsh Browser
Browsh Browser
ie
w3m
cmd
internal
ie

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL