├── README.md
├── cmd
└── chromedb
│ └── main.go
├── cookies.go
├── go.mod
├── go.sum
├── localstorage.go
└── sessionstorage.go
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 
Because chromiumdb
is a bit of a mouthful for a Go package name.
3 |
4 |
5 | Read Chromium data (namely, cookies and local storage) straight from disk—_without_ spinning up the browser. I primarily use this to extract tokens from authenticated browser sessions for use in automation, scraping, etc.
6 |
7 | ## Description
8 |
9 | Chromium-based browsers store cookies and local storage in the following respective databases within the profile directory:
10 |
11 | Path | Format | Encrypted
12 | --- | --- | ---
13 | `Cookies` | SQLite | Yes
14 | `Local Storage/leveldb/` | LevelDB | No
15 |
16 | This tool reads from those databases, decrypts where necessary, and outputs the data in JSON format for easy parsing on CLI.
17 |
18 | ## Getting started
19 |
20 | ### Prerequisites
21 |
22 | This has only been tested with Arc browser on macOS, but should work with any Chromium-based browser. I'd accept a PR to support decrypting cookies on other operating systems.
23 |
24 | ### Install
25 |
26 | ```bash
27 | 𝄢 go install -v github.com/noperator/chromedb/cmd/chromedb@latest
28 | ```
29 |
30 | ### Usage
31 |
32 | ```bash
33 | 𝄢 chromedb -h
34 | Usage of chromedb:
35 | -c cookies
36 | -ls
37 | local storage
38 | -p string
39 | path to browser profile directory
40 |
41 | ```
42 |
43 | To decrypt cookies for Chromium-based Arc browser, we need to first get its password from the keychain.
44 |
45 | ```bash
46 | 𝄢 export BROWSER_PASSWORD=$(security find-generic-password -wga Arc)
47 | 𝄢 chromedb -c -p ~/Library/Application\ Support/Arc/User\ Data/Profile\ 1/ |
48 | shuf -n 2 |
49 | jq '.encrypted_value = ""'
50 |
51 | {
52 | "domain": ".geeksforgeeks.org",
53 | "name": "gfg_theme",
54 | "encrypted_value": "",
55 | "value": "gfgThemeDark"
56 | }
57 | {
58 | "domain": "www.citrix.com",
59 | "name": "renderid",
60 | "encrypted_value": "",
61 | "value": "rend01"
62 | }
63 | ```
64 |
65 | Local storage is unencrypted and doesn't require a password.
66 |
67 | ```bash
68 | 𝄢 chromedb -ls -p ~/Library/Application\ Support/Arc/User\ Data/Profile\ 1/ |
69 | shuf -n 2 |
70 | jq
71 |
72 | {
73 | "storage_key": "https://docs.paloaltonetworks.com",
74 | "script_key": "ClientSidePersistence",
75 | "charset": "ISO-8859-1",
76 | "mime": "text/plain",
77 | "conversions": [
78 | "strconv.Quote"
79 | ],
80 | "value": "ClientContext/CLIENTCONTEXT:=visitorId%3D"
81 | }
82 | {
83 | "storage_key": "https://github.com",
84 | "script_key": "ref-selector:greasysock/railscookie:branch",
85 | "charset": "ISO-8859-1",
86 | "mime": "application/json",
87 | "conversions": [],
88 | "value": {
89 | "refs": [
90 | "master"
91 | ],
92 | "cacheKey": "v0:1554731422.0"
93 | }
94 | }
95 | ```
96 |
97 | ## Back matter
98 |
99 | ### See also
100 |
101 | Three outstanding posts from CCL Solutions Group that helped me understand Chromium data storage:
102 | - [LevelDB](https://www.cclsolutionsgroup.com/post/hang-on-thats-not-sqlite-chrome-electron-and-leveldb)
103 | - [IndexedDB](https://www.cclsolutionsgroup.com/post/indexeddb-on-chromium)
104 | - [Local storage](https://www.cclsolutionsgroup.com/post/chromium-session-storage-and-local-storage)
105 |
106 | I almost didn't write this tool as there are many others that do this kind of thing already. The most widely used ones are written in Python ([`pycookiecheat`](https://github.com/n8henrie/pycookiecheat/blob/dev/src/pycookiecheat/chrome.py) for cookies, [`ccl_chrome_indexeddb`](https://github.com/cclgroupltd/ccl_chrome_indexeddb/blob/master/ccl_chromium_localstorage.py) for local storage)—but I avoid using Python if possible due [nightmarish environment management](https://xkcd.com/1987/). There are a few Go-based cookie-dumping utilities, but they:
107 |
108 | - don't read from disk, and instead abuse the [remote debugging port](https://blog.chromium.org/2011/05/remote-debugging-with-chrome-developer.html) to launch a browser and dump unencrypted cookies ([`WhiteChocolateMacademiaNut`](https://github.com/slyd0g/WhiteChocolateMacademiaNut), [`chromecookiestealer`](https://github.com/magisterquis/chromecookiestealer), [`chrome-dump`](https://github.com/lesnuages/chrome-dump), [`go-chrome-stealer`](https://github.com/omaidf/go-chrome-stealer))
109 | - only work on Windows ([`gookies`](https://github.com/CCob/gookies))
110 | - are "obsolete" ([`gostealer`](https://github.com/4kord/gostealer)) or "demo-only" ([`go-stealer`](https://github.com/idfp/go-stealer))
111 | - do read from disk, but either used too many hardcoded values or were too complex for my needs ([`go-chrome-cookies`](https://github.com/teocci/go-chrome-cookies), [`chrome-cookie`](https://github.com/muyids/chrome-cookie), [`ChromeDecryptor`](https://github.com/wat4r/ChromeDecryptor), [`chrome-cookie-cutter`](https://github.com/saranrapjs/chrome-cookie-cutter), [`chrome-cookie-decrypt`](https://github.com/kinghrothgar/chrome-cookie-decrypt), [`chrome-cookies`](https://github.com/igara/chrome-cookies), [`cookies`](https://github.com/creachadair/cookies))
112 |
113 | I wasn't able to find _any_ Go-based tools that specifically parse local storage (though [`leveldb-cli`](https://github.com/cions/leveldb-cli) does read LevelDB, which is a good start).
114 |
115 | (I really did try.)
116 |
117 |
118 | ```bash
119 | # Search for tools written in Go that process Chrom(e|ium)'s cookies or local storage.
120 | for QUERY in \
121 | 'chrome cookie' \
122 | 'chromium cookie' \
123 | 'chrome leveldb' \
124 | 'chromium leveldb' \
125 | 'chrome local storage' \
126 | 'chromium local storage' \
127 | ; do
128 | echo "query: $QUERY" >&2
129 | curl -sv "https://github.com/search?type=repositories" \
130 | -G -X GET --data-urlencode "q=$QUERY language:Go" \
131 | -H 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' |
132 | tee >/dev/null \
133 | >(htmlq -t .search-title | nl -nln | sed -E 's/(^[0-9]+)/\1.1/') \
134 | >(htmlq 'li > a' -a 'aria-label' | nl -nln | sed -E 's/(^[0-9]+)/\1.2/') |
135 | sort -V |
136 | sed -E 's/^[0-9\.]+\s+//' |
137 | paste -d @ - - |
138 | tee /dev/stderr
139 | sleep 2
140 | done |
141 | sed -E 's/ stars$//' |
142 | sort -u |
143 | tee repos.lst
144 |
145 | # Download all of those tools' repos.
146 | mkdir -p repos
147 | cd repos
148 | cat ../repos.lst | parallel --colsep @ --bar 'git clone --depth 1 https://github.com/{1} $(echo {1} | sed -E "s|/|--|")--{2}'
149 | rm -rf ./*/.git
150 |
151 | # Look for various topics that may or may not indicate that the tool uses the approach I care about.
152 | echo "
153 | sql sql|db|database|query|row
154 | remote ws://|remote|debugg(ing|er)|9222|cdp|dev.?tools|debug.port
155 | keychain log.?in|keychain
156 | crypto key.?length|iteration|aes|cbc|sha1?|pbkdf|cipher|crypt
157 | localStorage ldb|leveldb|local.?storage
158 | " | grep -v '^$' | while read TOPIC REGEX; do
159 | rg -g '!.git*' -g '!topics.jsonl' -c "$REGEX" . | while read RESULT; do
160 | FILE=$(echo "$RESULT" | cut -d : -f 1 | sed -E 's|\./||')
161 | REPO="https://github.com/"$(echo "$FILE" | grep -oE '[^/]+' | head -n 1 | sed -E 's|--|/|; s/--.*//')
162 | STARS=$(echo "$FILE" | grep -oE '\--[0-9]+/' | head -n 1 | grep -oE '[0-9]+')
163 | COUNT=$(echo "$RESULT" | cut -d : -f 2)
164 | jo topic="$TOPIC" repo="$REPO" file="$FILE" count="$COUNT" stars="$STARS"
165 | done
166 | done | tee topics.jsonl
167 |
168 | # Sort relevant repos by popularity.
169 | cat topics.jsonl | jq -sc 'group_by(.repo) | map({
170 | repo: .[0].repo,
171 | stars: .[0].stars,
172 | topics: map({topic, count}) |
173 | group_by(.topic) |
174 | map({topic: .[0].topic, count: map(.count) | add})
175 | }) | sort_by(.stars) | reverse[]'
176 |
177 | {"repo":"https://github.com/slyd0g/WhiteChocolateMacademiaNut","stars":141,"topics":[{"topic":"crypto","count":5},{"topic":"remote","count":17},{"topic":"sql","count":6}]}
178 | {"repo":"https://github.com/magisterquis/chromecookiestealer","stars":92,"topics":[{"topic":"crypto","count":6},{"topic":"keychain","count":3},{"topic":"remote","count":20},{"topic":"sql","count":15}]}
179 | {"repo":"https://github.com/CCob/gookies","stars":48,"topics":[{"topic":"crypto","count":36},{"topic":"sql","count":14}]}
180 | {"repo":"https://github.com/cions/leveldb-cli","stars":27,"topics":[{"topic":"crypto","count":14},{"topic":"localStorage","count":60},{"topic":"sql","count":290}]}
181 | {"repo":"https://github.com/lesnuages/chrome-dump","stars":18,"topics":[{"topic":"crypto","count":11},{"topic":"keychain","count":1},{"topic":"remote","count":13}]}
182 | {"repo":"https://github.com/teocci/go-chrome-cookies","stars":9,"topics":[{"topic":"crypto","count":140},{"topic":"keychain","count":42},{"topic":"sql","count":283}]}
183 | {"repo":"https://github.com/idfp/go-stealer","stars":9,"topics":[{"topic":"crypto","count":73},{"topic":"keychain","count":32},{"topic":"sql","count":66}]}
184 | {"repo":"https://github.com/muyids/chrome-cookie","stars":8,"topics":[{"topic":"crypto","count":22},{"topic":"keychain","count":3},{"topic":"sql","count":25}]}
185 | {"repo":"https://github.com/wat4r/ChromeDecryptor","stars":5,"topics":[{"topic":"crypto","count":107},{"topic":"keychain","count":9},{"topic":"sql","count":35}]}
186 | {"repo":"https://github.com/saranrapjs/chrome-cookie-cutter","stars":5,"topics":[{"topic":"crypto","count":29},{"topic":"sql","count":10}]}
187 | {"repo":"https://github.com/omaidf/go-chrome-stealer","stars":5,"topics":[{"topic":"remote","count":3}]}
188 | {"repo":"https://github.com/kawakatz/macCookies","stars":3,"topics":[{"topic":"crypto","count":68},{"topic":"keychain","count":4},{"topic":"sql","count":30}]}
189 | {"repo":"https://github.com/4kord/gostealer","stars":3,"topics":[{"topic":"crypto","count":52},{"topic":"keychain","count":13},{"topic":"sql","count":242}]}
190 | {"repo":"https://github.com/kinghrothgar/chrome-cookie-decrypt","stars":1,"topics":[{"topic":"crypto","count":45},{"topic":"keychain","count":12},{"topic":"sql","count":55}]}
191 | {"repo":"https://github.com/kalelc/go-rails-cook","stars":1,"topics":[{"topic":"crypto","count":25},{"topic":"sql","count":4}]}
192 | {"repo":"https://github.com/hybridtheory/samesite-cookie-support","stars":1,"topics":[{"topic":"crypto","count":2},{"topic":"sql","count":24}]}
193 | {"repo":"https://github.com/m4tt72/rails-cookie-decrypt-go","stars":0,"topics":[{"topic":"crypto","count":31},{"topic":"sql","count":1}]}
194 | {"repo":"https://github.com/igara/chrome-cookies","stars":0,"topics":[{"topic":"crypto","count":31},{"topic":"sql","count":12}]}
195 | {"repo":"https://github.com/greasysock/railscookie","stars":0,"topics":[{"topic":"crypto","count":13},{"topic":"sql","count":1}]}
196 | {"repo":"https://github.com/corenting/cookies","stars":0,"topics":[{"topic":"crypto","count":24},{"topic":"remote","count":1},{"topic":"sql","count":10}]}
197 | ```
198 |
199 |
200 |
201 | ### To-do
202 |
203 | - [ ] decrypt cookies on Linux, Windows
204 | - [ ] specify a domain to filter on
205 | - [ ] clean up error handling, logging
206 | - [x] support session storage
207 |
--------------------------------------------------------------------------------
/cmd/chromedb/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "flag"
6 | "fmt"
7 | "os"
8 | "path/filepath"
9 |
10 | "github.com/noperator/chromedb"
11 | )
12 |
13 | func main() {
14 |
15 | browserPath := flag.String("p", "", "path to browser profile directory (required)")
16 | cookies := flag.Bool("c", false, "cookies")
17 | localStorage := flag.Bool("ls", false, "local storage")
18 | sessionStorage := flag.Bool("ss", false, "session storage")
19 |
20 | flag.Parse()
21 |
22 | if *browserPath == "" {
23 | fmt.Println("Error: -p flag (path to browser profile directory) is required")
24 | flag.Usage()
25 | os.Exit(1)
26 | }
27 |
28 | // Check for mutually exclusive flags.
29 | flagCount := 0
30 | if *cookies {
31 | flagCount++
32 | }
33 | if *localStorage {
34 | flagCount++
35 | }
36 | if *sessionStorage {
37 | flagCount++
38 | }
39 |
40 | if flagCount != 1 {
41 | fmt.Println("Error: Please specify exactly one of -c, -ls, or -ss")
42 | flag.Usage()
43 | os.Exit(1)
44 | }
45 |
46 | if *cookies {
47 |
48 | cookiesPath := filepath.Join(*browserPath, "Cookies")
49 | cookies, err := chromedb.GetCookies(cookiesPath)
50 | if err != nil {
51 | fmt.Println("Error opening Cookies database:", err)
52 | os.Exit(1)
53 | }
54 |
55 | key, err := chromedb.GetKey()
56 | if err != nil {
57 | fmt.Println("Error getting key:", err)
58 | os.Exit(1)
59 | }
60 |
61 | for _, c := range cookies {
62 | if len(c.EncryptedValue) > 0 {
63 | value, err := chromedb.DecryptValue(c.EncryptedValue, key, c.Domain)
64 | if err != nil {
65 | fmt.Printf("Failed to decrypt cookie %s: %v\n", c.Name, err)
66 | }
67 | c.Value = value
68 | }
69 |
70 | j, err := json.Marshal(c)
71 | if err != nil {
72 | fmt.Println("Error converting cookie to JSON:", err)
73 | os.Exit(1)
74 | }
75 |
76 | fmt.Println(string(j))
77 | }
78 | }
79 |
80 | if *localStorage {
81 | localStoragePath := filepath.Join(*browserPath, "Local Storage/leveldb")
82 |
83 | lsd, err := chromedb.LoadLocalStorage(localStoragePath)
84 | if err != nil {
85 | fmt.Println("Error opening LevelDB:", err)
86 | os.Exit(1)
87 | }
88 | defer lsd.Close()
89 |
90 | for _, r := range lsd.Records {
91 | j, err := chromedb.LocalStorageRecordToJson(r)
92 | if err != nil {
93 | fmt.Println("Error converting record to JSON:", err)
94 | os.Exit(1)
95 | }
96 |
97 | fmt.Println(j)
98 | }
99 | }
100 |
101 | if *sessionStorage {
102 | sessionStoragePath := filepath.Join(*browserPath, "Session Storage")
103 |
104 | ssd, err := chromedb.LoadSessionStorage(sessionStoragePath)
105 | if err != nil {
106 | fmt.Println("Error opening LevelDB:", err)
107 | os.Exit(1)
108 | }
109 | defer ssd.Close()
110 |
111 | for _, r := range ssd.Records {
112 | j, err := chromedb.SessionStorageRecordToJson(r)
113 | if err != nil {
114 | fmt.Println("Error converting record to JSON:", err)
115 | os.Exit(1)
116 | }
117 |
118 | fmt.Println(j)
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/cookies.go:
--------------------------------------------------------------------------------
1 | package chromedb
2 |
3 | import (
4 | "bytes"
5 | "crypto/aes"
6 | "crypto/cipher"
7 | "crypto/sha1"
8 | "crypto/sha256"
9 | "database/sql"
10 | "fmt"
11 | "os"
12 | "strings"
13 |
14 | _ "github.com/mattn/go-sqlite3"
15 | "golang.org/x/crypto/pbkdf2"
16 | )
17 |
18 | type Cookie struct {
19 | Domain string `json:"domain"`
20 | Name string `json:"name"`
21 | EncryptedValue []byte `json:"encrypted_value"`
22 | Value string `json:"value"`
23 | }
24 |
25 | func GetCookies(cookiesPath string) ([]Cookie, error) {
26 |
27 | db, err := sql.Open("sqlite3", cookiesPath)
28 | if err != nil {
29 | return nil, err
30 | }
31 | defer db.Close()
32 |
33 | // Check the database version - we'll use this later for decryption
34 | var dbVersion int
35 | row := db.QueryRow("SELECT value FROM meta WHERE key = 'version'")
36 | if err := row.Scan(&dbVersion); err != nil {
37 | // If we can't get the version, assume it's an older version
38 | dbVersion = 0
39 | }
40 |
41 | // Store the database version in a package variable for DecryptValue to use
42 | currentDBVersion = dbVersion
43 |
44 | // query := "SELECT name, value, host_key, encrypted_value FROM cookies WHERE host_key like ?"
45 | // rows, err := db.Query(query, fmt.Sprintf("%%%s%%", domain))
46 | query := "SELECT name, value, host_key, encrypted_value FROM cookies"
47 | rows, err := db.Query(query)
48 | if err != nil {
49 | return nil, err
50 | }
51 | defer rows.Close()
52 |
53 | var cookies []Cookie
54 | for rows.Next() {
55 | var cookie Cookie
56 | err := rows.Scan(&cookie.Name, &cookie.Value, &cookie.Domain, &cookie.EncryptedValue)
57 | if err != nil {
58 | return nil, err
59 | }
60 | cookies = append(cookies, cookie)
61 | }
62 |
63 | return cookies, nil
64 | }
65 |
66 | func GetKey() ([]byte, error) {
67 | browserPassword := os.Getenv("BROWSER_PASSWORD")
68 | if browserPassword == "" {
69 | return []byte{}, fmt.Errorf("BROWSER_PASSWORD environment variable not set")
70 | }
71 | password := strings.TrimSpace(string(browserPassword))
72 | return pbkdf2.Key([]byte(password), []byte("saltysalt"), 1003, 16, sha1.New), nil
73 | }
74 |
75 | // Package variable to store the current database version
76 | var currentDBVersion int
77 |
78 | func DecryptValue(encryptedValue, key []byte, domain string) (string, error) {
79 | block, err := aes.NewCipher(key)
80 | if err != nil {
81 | return "", err
82 | }
83 |
84 | // The EncryptedValue is prefixed with "v10", remove it
85 | // TODO check if prefix is v10
86 | if len(encryptedValue) < 3 {
87 | return "", fmt.Errorf("encrypted length less than 3")
88 | }
89 | version := string(encryptedValue[0:3])
90 | if version != "v10" {
91 | return "", fmt.Errorf("unsported encrypted value version: %s", version)
92 | }
93 | encryptedValue = encryptedValue[3:]
94 | decrypted := make([]byte, len(encryptedValue))
95 | const (
96 | aescbcSalt = `saltysalt`
97 | aescbcIV = ` `
98 | aescbcIterationsLinux = 1
99 | aescbcIterationsMacOS = 1003
100 | aescbcLength = 16
101 | )
102 | cbc := cipher.NewCBCDecrypter(block, []byte(aescbcIV))
103 | cbc.CryptBlocks(decrypted, encryptedValue)
104 |
105 | if len(decrypted) == 0 {
106 | return "", fmt.Errorf("not enough bits")
107 | }
108 |
109 | if len(decrypted)%aescbcLength != 0 {
110 | return "", fmt.Errorf("decrypted data block length is not a multiple of %d", aescbcLength)
111 | }
112 | paddingLen := int(decrypted[len(decrypted)-1])
113 | if paddingLen > 16 {
114 | return "", fmt.Errorf("invalid last block padding length: %d", paddingLen)
115 | }
116 |
117 | // In Chrome database versions ≥ 24, the first 32 bytes contain a SHA256 digest of the host_key (domain)
118 | // This was added in Chrome v130 (https://github.com/chromium/chromium/commit/5ea6d65c622a3d5ff75db9dc0257ea3869f31289)
119 | if currentDBVersion >= 24 {
120 | // Need to verify and skip the first 32 bytes (SHA256 digest of domain)
121 | if len(decrypted) <= 32 {
122 | return "", fmt.Errorf("decrypted data too short for db version %d, expected more than 32 bytes but got %d", currentDBVersion, len(decrypted))
123 | }
124 |
125 | // If domain is provided, verify the SHA256 hash matches
126 | if domain != "" {
127 | // Calculate SHA256 hash of the domain
128 | domainHash := sha256.Sum256([]byte(domain))
129 |
130 | // Check if the hash in the decrypted value matches the calculated hash
131 | if !bytes.Equal(domainHash[:], decrypted[:32]) { // SHA256 is 32 bytes
132 | return "", fmt.Errorf("domain hash verification failed")
133 | }
134 | }
135 |
136 | return string(decrypted[32:len(decrypted)-paddingLen]), nil
137 | }
138 |
139 | // For older versions, return the full decrypted value (minus padding)
140 | return string(decrypted[:len(decrypted)-paddingLen]), nil
141 | }
142 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/noperator/chromedb
2 |
3 | go 1.21.5
4 |
5 | require (
6 | github.com/h2non/filetype v1.1.3
7 | github.com/mattn/go-sqlite3 v1.14.22
8 | github.com/syndtr/goleveldb v1.0.0
9 | golang.org/x/crypto v0.23.0
10 | golang.org/x/text v0.15.0
11 | google.golang.org/protobuf v1.34.1
12 | )
13 |
14 | require github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
15 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
2 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
3 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
4 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
5 | github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
6 | github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
7 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
8 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
9 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
10 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
11 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
12 | github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
13 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
14 | github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
15 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
16 | github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
17 | github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
18 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
19 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
20 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
21 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
22 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
23 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
24 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
25 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
26 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
27 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
28 | golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
29 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
30 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
31 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
33 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
34 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
35 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
36 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
37 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
38 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
39 |
--------------------------------------------------------------------------------
/localstorage.go:
--------------------------------------------------------------------------------
1 | package chromedb
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "encoding/json"
7 | "fmt"
8 | "io"
9 | "net/http"
10 | "os"
11 | "path/filepath"
12 | "regexp"
13 | "strconv"
14 | "strings"
15 | "time"
16 |
17 | "github.com/h2non/filetype"
18 | "github.com/syndtr/goleveldb/leveldb"
19 | "github.com/syndtr/goleveldb/leveldb/opt"
20 | "github.com/syndtr/goleveldb/leveldb/storage"
21 | "golang.org/x/text/encoding/unicode"
22 | "google.golang.org/protobuf/encoding/protowire"
23 | )
24 |
25 | func fromChromeTimestamp(microseconds int64) (time.Time, error) {
26 | chromiumEpoch := time.Date(1601, 1, 1, 0, 0, 0, 0, time.UTC).UnixMicro()
27 | microFromEpoch := chromiumEpoch + microseconds
28 | timestamp := time.Unix(0, microFromEpoch*1000)
29 | return timestamp, nil
30 | }
31 |
32 | func decodeString(raw []byte) (string, string, error) {
33 | prefix := raw[0]
34 | if prefix == 0 {
35 | decoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder()
36 | utf8bytes, err := decoder.Bytes(raw[1:])
37 | if err != nil {
38 | return "", "", fmt.Errorf("failed to decode UTF-16-LE: %w", err)
39 | }
40 | return string(utf8bytes), "UTF-16-LE", nil
41 | } else if prefix == 1 {
42 | return string(raw[1:]), "ISO-8859-1", nil
43 | }
44 | return "", "", fmt.Errorf("unknown string encoding prefix: %d", prefix)
45 | }
46 |
47 | type StorageMetadata struct {
48 | StorageKey string `json:"storage_key"`
49 | Timestamp time.Time `json:"timestamp"`
50 | Size int `json:"size"`
51 | }
52 |
53 | type LocalStorageRecord struct {
54 | StorageKey string `json:"storage_key"`
55 | ScriptKey string `json:"script_key"`
56 | Charset string `json:"charset"`
57 | Decoded string `json:"-"`
58 | MIME string `json:"mime"`
59 | Conversions []string `json:"conversions"`
60 | JsonType string `json:"-"`
61 | Value json.RawMessage `json:"value"`
62 | }
63 |
64 | type LocalStoreDb struct {
65 | ldb *leveldb.DB
66 | Records []LocalStorageRecord `json:"records"`
67 | metadata []StorageMetadata `json:"metadata"`
68 | }
69 |
70 | func StorageMetadataFromProtobuff(sm *StorageMetadata, data []byte) error {
71 |
72 | fieldNum, wireType, n := protowire.ConsumeTag(data)
73 | if fieldNum != 1 || wireType != protowire.VarintType {
74 | return fmt.Errorf("Expected field number 1 with varint type, got field number %d with wire type %d", fieldNum, wireType)
75 | }
76 | timestamp, m := protowire.ConsumeVarint(data[n:])
77 | if m < 0 {
78 | return fmt.Errorf("Failed to decode timestamp")
79 | }
80 |
81 | fieldNum, wireType, n = protowire.ConsumeTag(data[n+m:])
82 | if fieldNum != 2 || wireType != protowire.VarintType {
83 | return fmt.Errorf("Expected field number 2 with varint type, got field number %d with wire type %d", fieldNum, wireType)
84 | }
85 | size, m := protowire.ConsumeVarint(data[n+m:])
86 | if m < 0 {
87 | return fmt.Errorf("Failed to decode size")
88 | }
89 |
90 | ts, err := fromChromeTimestamp(int64(timestamp))
91 | if err != nil {
92 | return fmt.Errorf("Failed to decode timestamp: %w", err)
93 | }
94 |
95 | sm.Timestamp = ts
96 | sm.Size = int(size)
97 |
98 | return nil
99 | }
100 |
101 | func LoadLocalStorage(dir string) (*LocalStoreDb, error) {
102 | db := &leveldb.DB{}
103 | db, err := leveldb.OpenFile(dir, &opt.Options{
104 | ReadOnly: true,
105 | })
106 |
107 | // We try the ReadOnly option above, but it weirdly doesn't work when the
108 | // db is locked. When this happens, we simply copy the db to memory and
109 | // read from there.
110 | if err != nil {
111 |
112 | srcDir := dir
113 |
114 | memStorage := storage.NewMemStorage()
115 |
116 | // Copy the LevelDB directory contents into the memory storage
117 | err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
118 |
119 | if err != nil {
120 | return err
121 | }
122 |
123 | relPath, err := filepath.Rel(srcDir, path)
124 | if err != nil {
125 | return err
126 | }
127 |
128 | // Skip directories, we only need files
129 | if info.IsDir() {
130 | return nil
131 | }
132 |
133 | srcFile, err := os.Open(path)
134 | if err != nil {
135 | return err
136 | }
137 | defer srcFile.Close()
138 |
139 | data, err := io.ReadAll(srcFile)
140 | if err != nil {
141 | return err
142 | }
143 |
144 | var num int64
145 | num = 0
146 | re := regexp.MustCompile(`\d+`)
147 | match := re.FindString(relPath)
148 | if match != "" {
149 |
150 | matchInt, err := strconv.Atoi(match)
151 | if err == nil {
152 | num = int64(matchInt)
153 | }
154 | }
155 |
156 | // Determine the file descriptor type
157 | var fileType storage.FileType
158 | switch {
159 | case strings.HasSuffix(relPath, ".ldb"):
160 | fileType = storage.TypeTable
161 | case strings.HasPrefix(relPath, "MANIFEST"):
162 | fileType = storage.TypeManifest
163 | case strings.HasSuffix(relPath, ".log"):
164 | fileType = storage.TypeJournal
165 | case strings.HasSuffix(relPath, ".tmp"):
166 | fileType = storage.TypeTemp
167 | default:
168 | return nil
169 | }
170 |
171 | // Create the file in the memory storage
172 | fd := storage.FileDesc{Type: fileType, Num: num}
173 | if fd.Type == storage.TypeManifest {
174 | err = memStorage.SetMeta(fd)
175 | if err != nil {
176 | return err
177 | }
178 | }
179 | writer, err := memStorage.Create(fd)
180 | if err != nil {
181 | return err
182 | }
183 |
184 | // Write the contents to the memory storage
185 | _, err = writer.Write(data)
186 | if err != nil {
187 | writer.Close()
188 | return err
189 | }
190 |
191 | // Close the writer
192 | err = writer.Close()
193 | if err != nil {
194 | return err
195 | }
196 |
197 | return nil
198 |
199 | })
200 |
201 | if err != nil {
202 | fmt.Println("Error copying directory:", err)
203 | return nil, err
204 | }
205 |
206 | // Open the LevelDB using the memory storage
207 | db, err = leveldb.Open(memStorage, nil)
208 | if err != nil {
209 | fmt.Println("Error opening LevelDB:", err)
210 | return nil, err
211 | }
212 |
213 | }
214 | defer db.Close()
215 |
216 | lsd := &LocalStoreDb{
217 | ldb: db,
218 | }
219 |
220 | iter := db.NewIterator(nil, nil)
221 | defer iter.Release()
222 |
223 | for iter.Next() {
224 | key := iter.Key()
225 | value := iter.Value()
226 |
227 | const (
228 | MetaKeyPrefix = "META:"
229 | RecordKeyPrefix = "_"
230 | )
231 |
232 | // metadata
233 | if bytes.HasPrefix(key, []byte(MetaKeyPrefix)) {
234 | storageKey := string(bytes.TrimPrefix(key, []byte(MetaKeyPrefix)))
235 |
236 | metadata := StorageMetadata{
237 | StorageKey: storageKey,
238 | Timestamp: time.Time{},
239 | Size: 0,
240 | }
241 | err := StorageMetadataFromProtobuff(&metadata, value)
242 | if err != nil {
243 | return nil, err
244 | }
245 |
246 | lsd.metadata = append(lsd.metadata, metadata)
247 |
248 | // record
249 | } else if bytes.HasPrefix(key, []byte(RecordKeyPrefix)) {
250 | parts := bytes.SplitN(bytes.TrimPrefix(key, []byte(RecordKeyPrefix)), []byte{0}, 2)
251 | if len(parts) != 2 {
252 | continue
253 | }
254 |
255 | record := LocalStorageRecord{}
256 |
257 | storageKey := string(parts[0])
258 | record.StorageKey = storageKey
259 | sk, _, err := decodeString(parts[1])
260 | if err != nil {
261 | return nil, fmt.Errorf("failed to decode script key: %w", err)
262 | }
263 | record.ScriptKey = sk
264 |
265 | val, valEnc, err := decodeString(value)
266 | if err != nil {
267 | return nil, fmt.Errorf("failed to decode value: %w", err)
268 | }
269 | record.Decoded = val
270 | record.Charset = valEnc
271 |
272 | lsd.Records = append(lsd.Records, record)
273 | }
274 | }
275 |
276 | return lsd, nil
277 | }
278 |
279 | func LocalStorageRecordToJson(r LocalStorageRecord) (string, error) {
280 |
281 | mime := "application/octet-stream"
282 | xfer := []string{}
283 | b := []byte(r.Decoded)
284 | validJson := json.Valid(b)
285 | jsonType := ""
286 | out := []byte{}
287 |
288 | if validJson {
289 | // Use a custom decoder to handle large numbers as strings
290 | d := json.NewDecoder(bytes.NewReader(b))
291 | d.UseNumber() // This makes the decoder use json.Number for numbers
292 | var v interface{}
293 | err := d.Decode(&v)
294 | if err != nil {
295 | return "", fmt.Errorf("failed to unmarshal supposedly valid JSON: %w", err)
296 | }
297 |
298 | mime = "application/json"
299 |
300 | // Determine the type of the JSON value
301 | switch val := v.(type) {
302 | case json.Number:
303 | jsonType = "number"
304 | // Check if the number might be too large for float64
305 | _, err := val.Float64()
306 | if err != nil {
307 | // If it can't be converted to float64, treat it as a string in the output
308 | strVal := fmt.Sprintf("\"%s\"", val.String())
309 | out = []byte(strVal)
310 | } else {
311 | out = b
312 | }
313 | case string:
314 | jsonType = "string"
315 | out = b
316 | case bool:
317 | jsonType = "boolean"
318 | out = b
319 | case []interface{}:
320 | jsonType = "array"
321 | out = b
322 | case map[string]interface{}:
323 | jsonType = "object"
324 | out = b
325 | case nil:
326 | jsonType = "null"
327 | out = b
328 | default:
329 | jsonType = ""
330 | out = b
331 | }
332 | } else {
333 | quoted := strconv.Quote(r.Decoded)
334 | if json.Valid([]byte(quoted)) {
335 |
336 | out = []byte(quoted)
337 | mime = "text/plain"
338 | xfer = append(xfer, "strconv.Quote")
339 | mime = http.DetectContentType(b)
340 | mime = strings.Split(mime, ";")[0]
341 |
342 | } else {
343 |
344 | b64 := base64.StdEncoding.EncodeToString(b)
345 | xfer = append(xfer, "base64.StdEncoding.EncodeToString")
346 | out = []byte(strconv.Quote(b64))
347 | xfer = append(xfer, "strconv.Quote")
348 |
349 | magic, _ := filetype.Match(b)
350 | if magic != filetype.Unknown {
351 | mime = magic.MIME.Value
352 | }
353 |
354 | }
355 | }
356 |
357 | r.MIME = mime
358 | r.Conversions = xfer
359 | r.Value = out
360 | r.JsonType = jsonType
361 | recordJson, err := json.Marshal(r)
362 | if err != nil {
363 | return "", fmt.Errorf("failed to marshal record to JSON: %w", err)
364 | }
365 |
366 | return string(recordJson), nil
367 | }
368 |
369 | func (lsd *LocalStoreDb) Close() {
370 | lsd.ldb.Close()
371 | }
372 |
--------------------------------------------------------------------------------
/sessionstorage.go:
--------------------------------------------------------------------------------
1 | package chromedb
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "encoding/json"
7 | "fmt"
8 | "io"
9 | "net/http"
10 | "os"
11 | "path/filepath"
12 | "regexp"
13 | "strconv"
14 | "strings"
15 |
16 | // "time"
17 |
18 | "github.com/h2non/filetype"
19 | "github.com/syndtr/goleveldb/leveldb"
20 | "github.com/syndtr/goleveldb/leveldb/opt"
21 | "github.com/syndtr/goleveldb/leveldb/storage"
22 | "golang.org/x/text/encoding/unicode"
23 | )
24 |
25 | type SessionStorageRecord struct {
26 | // Value string `json:"value"`
27 | MapID int `json:"map_id"`
28 | Key string `json:"key"`
29 | Value json.RawMessage `json:"value"`
30 | Charset string `json:"charset"`
31 | Decoded string `json:"-"`
32 | MIME string `json:"mime"`
33 | Conversions []string `json:"conversions"`
34 | JsonType string `json:"json_type"`
35 | }
36 |
37 | type SessionStoreDb struct {
38 | ldb *leveldb.DB
39 | Records []SessionStorageRecord `json:"records"`
40 | }
41 |
42 | func decodeUTF16LE(raw []byte) (string, error) {
43 | decoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder()
44 | utf8bytes, err := decoder.Bytes(raw)
45 | if err != nil {
46 | return "", fmt.Errorf("failed to decode UTF-16-LE: %w", err)
47 | }
48 | return string(utf8bytes), nil
49 | }
50 |
51 | func LoadSessionStorage(dir string) (*SessionStoreDb, error) {
52 | db, err := leveldb.OpenFile(dir, &opt.Options{
53 | ReadOnly: true,
54 | })
55 |
56 | if err != nil {
57 | srcDir := dir
58 | memStorage := storage.NewMemStorage()
59 |
60 | err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
61 | if err != nil {
62 | return err
63 | }
64 |
65 | relPath, err := filepath.Rel(srcDir, path)
66 | if err != nil {
67 | return err
68 | }
69 |
70 | if info.IsDir() {
71 | return nil
72 | }
73 |
74 | srcFile, err := os.Open(path)
75 | if err != nil {
76 | return err
77 | }
78 | defer srcFile.Close()
79 |
80 | data, err := io.ReadAll(srcFile)
81 | if err != nil {
82 | return err
83 | }
84 |
85 | var num int64
86 | num = 0
87 | re := regexp.MustCompile(`\d+`)
88 | match := re.FindString(relPath)
89 | if match != "" {
90 | matchInt, err := strconv.Atoi(match)
91 | if err == nil {
92 | num = int64(matchInt)
93 | }
94 | }
95 |
96 | var fileType storage.FileType
97 | switch {
98 | case strings.HasSuffix(relPath, ".ldb"):
99 | fileType = storage.TypeTable
100 | case strings.HasPrefix(relPath, "MANIFEST"):
101 | fileType = storage.TypeManifest
102 | case strings.HasSuffix(relPath, ".log"):
103 | fileType = storage.TypeJournal
104 | case strings.HasSuffix(relPath, ".tmp"):
105 | fileType = storage.TypeTemp
106 | default:
107 | return nil
108 | }
109 |
110 | fd := storage.FileDesc{Type: fileType, Num: num}
111 | if fd.Type == storage.TypeManifest {
112 | err = memStorage.SetMeta(fd)
113 | if err != nil {
114 | return err
115 | }
116 | }
117 | writer, err := memStorage.Create(fd)
118 | if err != nil {
119 | return err
120 | }
121 |
122 | _, err = writer.Write(data)
123 | if err != nil {
124 | writer.Close()
125 | return err
126 | }
127 |
128 | err = writer.Close()
129 | if err != nil {
130 | return err
131 | }
132 |
133 | return nil
134 | })
135 |
136 | if err != nil {
137 | fmt.Println("Error copying directory:", err)
138 | return nil, err
139 | }
140 |
141 | db, err = leveldb.Open(memStorage, nil)
142 | if err != nil {
143 | fmt.Println("Error opening LevelDB:", err)
144 | return nil, err
145 | }
146 | }
147 | defer db.Close()
148 |
149 | ssd := &SessionStoreDb{
150 | ldb: db,
151 | }
152 |
153 | iter := db.NewIterator(nil, nil)
154 | defer iter.Release()
155 |
156 | for iter.Next() {
157 | key := iter.Key()
158 | value := iter.Value()
159 |
160 | if bytes.HasPrefix(key, []byte("map-")) {
161 | parts := bytes.SplitN(bytes.TrimPrefix(key, []byte("map-")), []byte("-"), 2)
162 | if len(parts) != 2 {
163 | continue
164 | }
165 |
166 | mapID, err := strconv.Atoi(string(parts[0]))
167 | if err != nil {
168 | return nil, fmt.Errorf("failed to decode map ID: %w", err)
169 | }
170 |
171 | keyStr := string(parts[1])
172 | val, err := decodeUTF16LE(value)
173 | if err != nil {
174 | return nil, fmt.Errorf("failed to decode value: %w", err)
175 | }
176 |
177 | record := SessionStorageRecord{
178 | MapID: mapID,
179 | Key: keyStr,
180 | // Value: val,
181 | Decoded: val,
182 | Charset: "UTF-16-LE",
183 | }
184 |
185 | ssd.Records = append(ssd.Records, record)
186 | }
187 | }
188 |
189 | return ssd, nil
190 | }
191 |
192 | func SessionStorageRecordToJson(r SessionStorageRecord) (string, error) {
193 | mime := "application/octet-stream"
194 | xfer := []string{}
195 | b := []byte(r.Decoded)
196 | validJson := json.Valid(b)
197 | jsonType := ""
198 | out := []byte{}
199 |
200 | if validJson {
201 | // Use a custom decoder to handle large numbers as strings
202 | d := json.NewDecoder(bytes.NewReader(b))
203 | d.UseNumber() // This makes the decoder use json.Number for numbers
204 | var v interface{}
205 | err := d.Decode(&v)
206 | if err != nil {
207 | return "", fmt.Errorf("failed to unmarshal supposedly valid JSON: %w", err)
208 | }
209 |
210 | mime = "application/json"
211 |
212 | switch val := v.(type) {
213 | case json.Number:
214 | jsonType = "number"
215 | // Check if the number might be too large for float64
216 | _, err := val.Float64()
217 | if err != nil {
218 | // If it can't be converted to float64, treat it as a string in the output
219 | strVal := fmt.Sprintf("\"%s\"", val.String())
220 | out = []byte(strVal)
221 | } else {
222 | out = b
223 | }
224 | case string:
225 | jsonType = "string"
226 | out = b
227 | case bool:
228 | jsonType = "boolean"
229 | out = b
230 | case []interface{}:
231 | jsonType = "array"
232 | out = b
233 | case map[string]interface{}:
234 | jsonType = "object"
235 | out = b
236 | case nil:
237 | jsonType = "null"
238 | out = b
239 | default:
240 | jsonType = ""
241 | out = b
242 | }
243 | } else {
244 | quoted := strconv.Quote(r.Decoded)
245 | if json.Valid([]byte(quoted)) {
246 | out = []byte(quoted)
247 | mime = "text/plain"
248 | xfer = append(xfer, "strconv.Quote")
249 | mime = http.DetectContentType(b)
250 | mime = strings.Split(mime, ";")[0]
251 | } else {
252 | b64 := base64.StdEncoding.EncodeToString(b)
253 | xfer = append(xfer, "base64.StdEncoding.EncodeToString")
254 | out = []byte(strconv.Quote(b64))
255 | xfer = append(xfer, "strconv.Quote")
256 |
257 | magic, _ := filetype.Match(b)
258 | if magic != filetype.Unknown {
259 | mime = magic.MIME.Value
260 | }
261 | }
262 | }
263 |
264 | r.MIME = mime
265 | r.Conversions = xfer
266 | r.Value = out
267 | r.JsonType = jsonType
268 | recordJson, err := json.Marshal(r)
269 | if err != nil {
270 | return "", fmt.Errorf("failed to marshal record to JSON: %w", err)
271 | }
272 |
273 | return string(recordJson), nil
274 | }
275 |
276 | func (ssd *SessionStoreDb) Close() {
277 | ssd.ldb.Close()
278 | }
279 |
--------------------------------------------------------------------------------