1 #! /usr/bin/env python3
5 from configparser
import RawConfigParser
7 from tempfile
import NamedTemporaryFile
8 from shutil
import copyfileobj
9 from contextlib
import contextmanager
14 def main(profile
=None, domain
=None, name
=None):
16 profile
= get_default_profile()
19 host
= tuple(reversed(cookie
['host'].rstrip('.').split('.')))
20 return (host
, cookie
['path'])
22 if domain
is not None:
25 with
open(f
'{profile}/cookies.sqlite', 'rb') as orig
, \
26 TemporaryCopy(orig
, prefix
='cookies.', suffix
='.sqlite') as copy
:
28 con
= sqlite3
.connect(copy
)
29 con
.row_factory
= sqlite3
.Row
30 cur
= con
.execute('SELECT * FROM moz_cookies')
31 for row
in sorted(cur
, key
=key
):
32 assert row
['inBrowserElement'] == 0
33 assert row
['isSecure'] in {0, 1}
34 assert row
['isHttpOnly'] in {0, 1}
35 assert row
['sameSite'] in range(3)
36 if domain
is not None:
37 filter_cookie(result
, domain
, name
, row
, row
['name'])
39 EPOCH
= datetime
.datetime(1970, 1, 1)
40 if row
['expiry'] > (datetime
.datetime
.max - EPOCH
) // datetime
.timedelta(seconds
=1):
41 print(end
=f
'exp>{datetime.MAXYEAR} ')
43 print(end
=f
'exp {EPOCH + datetime.timedelta(seconds=row["expiry"])!s}')
44 print(end
=f
' axx {(EPOCH + datetime.timedelta(microseconds=row["lastAccessed"])).isoformat(" ", "microseconds")} cr {(EPOCH + datetime.timedelta(microseconds=row["creationTime"])).isoformat(" ", "microseconds")} ')
49 print(end
=f
'//{row["host"]}{row["path"]} {row["name"]}={row["value"]}')
51 print(end
=' HttpOnly')
52 if row
['sameSite'] or row
['rawSameSite']:
53 print(end
=f
' SameSite={row["sameSite"]}')
54 if row
['rawSameSite'] != row
['sameSite']:
55 print(end
=f
' (raw {row["rawSameSite"]})')
56 if row
['originAttributes'] > '':
57 print(end
=f
" originAttributes {row['originAttributes']}")
61 session
= open(f
'{profile}/sessionstore.jsonlz4', 'rb')
62 except FileNotFoundError
:
63 session
= open(f
'{profile}/sessionstore-backups/recovery.jsonlz4',
67 magic
= session
.read(8)
68 assert magic
== b
'mozLz40\x00'
69 uncomp
= session
.read(4)
70 assert len(uncomp
) == 4
71 uncomp
= int.from_bytes(uncomp
, 'little')
73 assert uncomp
<= 8*MiB
74 session
= session
.read()
75 LEGACY_FRAME
= 0x184C2102 . to_bytes(4, 'little')
76 session
= LEGACY_FRAME
+ len(session
).to_bytes(4, 'little') + session
77 session
= subprocess
.check_output(('lz4', '-d'), input=session
)
78 assert len(session
) == uncomp
80 for cookie
in sorted(json
.loads(session
)['cookies'], key
=key
):
81 partitionKey
= cookie
['originAttributes'].pop('partitionKey', '')
82 assert cookie
['originAttributes'] == {
83 'firstPartyDomain': '',
84 'geckoViewSessionContextId': '',
85 'inIsolatedMozBrowser': False,
86 'privateBrowsingId': 0,
89 assert cookie
.get('secure', True) is True
90 assert cookie
.get('httponly', True) is True
91 assert cookie
.get('sameSite', 1) in {1, 2}
92 found_name
= cookie
.get("name", "")
93 if domain
is not None:
94 filter_cookie(result
, domain
, name
, cookie
, found_name
)
96 if 'secure' in cookie
:
100 print(end
=f
'//{cookie["host"]}{cookie["path"]} {found_name}={cookie["value"]}')
101 if 'httponly' in cookie
:
102 print(end
=' httponly')
103 if 'sameSite' in cookie
:
104 print(end
=f
' sameSite={cookie["sameSite"]}')
105 if partitionKey
> '':
106 print(end
=f
' partitionKey={partitionKey}')
109 if domain
is not None:
112 raise LookupError(f
'Cookie {name!r} not found')
113 print(' '.join(result
))
115 def filter_cookie(result
, domain
, name
, cookie
, found_name
):
116 if cookie
['host'].startswith('.'):
117 if not f
'.{domain.lower()}'.endswith(cookie
['host']):
120 if cookie
['host'] != domain
.lower():
123 result
.append(f
'{found_name}={cookie["value"]}')
124 elif name
== found_name
:
126 raise LookupError('Multiple matching entries')
127 result
.append(cookie
['value'])
129 def get_default_profile():
130 if sys
.platform
== 'win32':
131 dir = f
"{os.environ['APPDATA']}\\Mozilla\\Firefox"
133 dir = f
"{os.environ['HOME']}/.mozilla/firefox"
135 parser
= RawConfigParser(delimiters
=('=',), comment_prefixes
=())
136 with
open(f
'{dir}/profiles.ini', 'rt', encoding
='ascii') as file:
137 parser
.read_file(file)
141 for section
in parser
.sections():
142 if section
.startswith('Install'):
143 assert profile
is None
144 profile
= parser
.get(section
, 'Default')
145 profiles
+= section
.startswith('Profile')
146 if profile
is not None:
147 return os
.path
.join(dir, profile
)
150 for i
in range(profiles
):
151 if parser
.has_option(f
'Profile{i}', 'Default'):
152 assert parser
.get(f
'Profile{i}', 'Default') == '1'
156 if parser
.get(f
'Profile{default}', 'IsRelative') == '1':
157 return os
.path
.join(dir, parser
.get(f
'Profile{default}', 'Path'))
159 assert parser
.get(f
'Profile{default}', 'IsRelative') == '0'
160 return parser
.get(f
'Profile{default}', 'Path')
163 def TemporaryCopy(orig
, *pos
, **kw
):
164 if sys
.platform
== 'win32':
165 with
NamedTemporaryFile(*pos
, **kw
, delete
=False) as temp
:
166 copyfileobj(orig
, temp
)
173 with
NamedTemporaryFile(*pos
, **kw
) as temp
:
174 copyfileobj(orig
, temp
)
177 if __name__
== '__main__':