17 from datetime
import datetime
, timedelta
, timezone
18 from enum
import Enum
, auto
19 from hashlib
import pbkdf2_hmac
22 aes_cbc_decrypt_bytes
,
23 aes_gcm_decrypt_and_verify_bytes
,
26 from .compat
import functools
27 from .dependencies
import (
28 _SECRETSTORAGE_UNAVAILABLE_REASON
,
32 from .minicurses
import MultilinePrinter
, QuietMultilinePrinter
45 CHROMIUM_BASED_BROWSERS
= {'brave', 'chrome', 'chromium', 'edge', 'opera', 'vivaldi'}
46 SUPPORTED_BROWSERS
= CHROMIUM_BASED_BROWSERS | {'firefox', 'safari'}
50 def __init__(self
, ydl
=None):
53 def debug(self
, message
):
55 self
._ydl
.write_debug(message
)
57 def info(self
, message
):
59 self
._ydl
.to_screen(f
'[Cookies] {message}')
61 def warning(self
, message
, only_once
=False):
63 self
._ydl
.report_warning(message
, only_once
)
65 def error(self
, message
):
67 self
._ydl
.report_error(message
)
69 class ProgressBar(MultilinePrinter
):
70 _DELAY
, _timer
= 0.1, 0
72 def print(self
, message
):
73 if time
.time() - self
._timer
> self
._DELAY
:
74 self
.print_at_line(f
'[Cookies] {message}', 0)
75 self
._timer
= time
.time()
77 def progress_bar(self
):
78 """Return a context manager with a print method. (Optional)"""
79 # Do not print to files/pipes, loggers, or when --no-progress is used
80 if not self
._ydl
or self
._ydl
.params
.get('noprogress') or self
._ydl
.params
.get('logger'):
82 file = self
._ydl
._out
_files
.error
88 return self
.ProgressBar(file, preserve_output
=False)
91 def _create_progress_bar(logger
):
92 if hasattr(logger
, 'progress_bar'):
93 printer
= logger
.progress_bar()
96 printer
= QuietMultilinePrinter()
97 printer
.print = lambda _
: None
101 def load_cookies(cookie_file
, browser_specification
, ydl
):
103 if browser_specification
is not None:
104 browser_name
, profile
, keyring
, container
= _parse_browser_specification(*browser_specification
)
106 extract_cookies_from_browser(browser_name
, profile
, YDLLogger(ydl
), keyring
=keyring
, container
=container
))
108 if cookie_file
is not None:
109 is_filename
= is_path_like(cookie_file
)
111 cookie_file
= expand_path(cookie_file
)
113 jar
= YoutubeDLCookieJar(cookie_file
)
114 if not is_filename
or os
.access(cookie_file
, os
.R_OK
):
115 jar
.load(ignore_discard
=True, ignore_expires
=True)
116 cookie_jars
.append(jar
)
118 return _merge_cookie_jars(cookie_jars
)
121 def extract_cookies_from_browser(browser_name
, profile
=None, logger
=YDLLogger(), *, keyring
=None, container
=None):
122 if browser_name
== 'firefox':
123 return _extract_firefox_cookies(profile
, container
, logger
)
124 elif browser_name
== 'safari':
125 return _extract_safari_cookies(profile
, logger
)
126 elif browser_name
in CHROMIUM_BASED_BROWSERS
:
127 return _extract_chrome_cookies(browser_name
, profile
, keyring
, logger
)
129 raise ValueError(f
'unknown browser: {browser_name}')
132 def _extract_firefox_cookies(profile
, container
, logger
):
133 logger
.info('Extracting cookies from firefox')
135 logger
.warning('Cannot extract cookies from firefox without sqlite3 support. '
136 'Please use a python interpreter compiled with sqlite3 support')
137 return YoutubeDLCookieJar()
140 search_root
= _firefox_browser_dir()
141 elif _is_path(profile
):
142 search_root
= profile
144 search_root
= os
.path
.join(_firefox_browser_dir(), profile
)
146 cookie_database_path
= _find_most_recently_used_file(search_root
, 'cookies.sqlite', logger
)
147 if cookie_database_path
is None:
148 raise FileNotFoundError(f
'could not find firefox cookies database in {search_root}')
149 logger
.debug(f
'Extracting cookies from: "{cookie_database_path}"')
152 if container
not in (None, 'none'):
153 containers_path
= os
.path
.join(os
.path
.dirname(cookie_database_path
), 'containers.json')
154 if not os
.path
.isfile(containers_path
) or not os
.access(containers_path
, os
.R_OK
):
155 raise FileNotFoundError(f
'could not read containers.json in {search_root}')
156 with open(containers_path
) as containers
:
157 identities
= json
.load(containers
).get('identities', [])
158 container_id
= next((context
.get('userContextId') for context
in identities
if container
in (
160 try_call(lambda: re
.fullmatch(r
'userContext([^\.]+)\.label', context
['l10nID']).group())
162 if not isinstance(container_id
, int):
163 raise ValueError(f
'could not find firefox container "{container}" in containers.json')
165 with tempfile
.TemporaryDirectory(prefix
='yt_dlp') as tmpdir
:
168 cursor
= _open_database_copy(cookie_database_path
, tmpdir
)
169 if isinstance(container_id
, int):
171 f
'Only loading cookies from firefox container "{container}", ID {container_id}')
173 'SELECT host, name, value, path, expiry, isSecure FROM moz_cookies WHERE originAttributes LIKE ? OR originAttributes LIKE ?',
174 (f
'%userContextId={container_id}', f
'%userContextId={container_id}&%'))
175 elif container
== 'none':
176 logger
.debug('Only loading cookies not belonging to any container')
178 'SELECT host, name, value, path, expiry, isSecure FROM moz_cookies WHERE NOT INSTR(originAttributes,"userContextId=")')
180 cursor
.execute('SELECT host, name, value, path, expiry, isSecure FROM moz_cookies')
181 jar
= YoutubeDLCookieJar()
182 with _create_progress_bar(logger
) as progress_bar
:
183 table
= cursor
.fetchall()
184 total_cookie_count
= len(table
)
185 for i
, (host
, name
, value
, path
, expiry
, is_secure
) in enumerate(table
):
186 progress_bar
.print(f
'Loading cookie {i: 6d}/{total_cookie_count: 6d}')
187 cookie
= http
.cookiejar
.Cookie(
188 version
=0, name
=name
, value
=value
, port
=None, port_specified
=False,
189 domain
=host
, domain_specified
=bool(host
), domain_initial_dot
=host
.startswith('.'),
190 path
=path
, path_specified
=bool(path
), secure
=is_secure
, expires
=expiry
, discard
=False,
191 comment
=None, comment_url
=None, rest
={})
192 jar
.set_cookie(cookie
)
193 logger
.info(f
'Extracted {len(jar)} cookies from firefox')
196 if cursor
is not None:
197 cursor
.connection
.close()
200 def _firefox_browser_dir():
201 if sys
.platform
in ('cygwin', 'win32'):
202 return os
.path
.expandvars(R
'%APPDATA%\Mozilla\Firefox\Profiles')
203 elif sys
.platform
== 'darwin':
204 return os
.path
.expanduser('~/Library/Application Support/Firefox')
205 return os
.path
.expanduser('~/.mozilla/firefox')
208 def _get_chromium_based_browser_settings(browser_name
):
209 # https://chromium.googlesource.com/chromium/src/+/HEAD/docs/user_data_dir.md
210 if sys
.platform
in ('cygwin', 'win32'):
211 appdata_local
= os
.path
.expandvars('%LOCALAPPDATA%')
212 appdata_roaming
= os
.path
.expandvars('%APPDATA%')
214 'brave': os
.path
.join(appdata_local
, R
'BraveSoftware\Brave-Browser\User Data'),
215 'chrome': os
.path
.join(appdata_local
, R
'Google\Chrome\User Data'),
216 'chromium': os
.path
.join(appdata_local
, R
'Chromium\User Data'),
217 'edge': os
.path
.join(appdata_local
, R
'Microsoft\Edge\User Data'),
218 'opera': os
.path
.join(appdata_roaming
, R
'Opera Software\Opera Stable'),
219 'vivaldi': os
.path
.join(appdata_local
, R
'Vivaldi\User Data'),
222 elif sys
.platform
== 'darwin':
223 appdata
= os
.path
.expanduser('~/Library/Application Support')
225 'brave': os
.path
.join(appdata
, 'BraveSoftware/Brave-Browser'),
226 'chrome': os
.path
.join(appdata
, 'Google/Chrome'),
227 'chromium': os
.path
.join(appdata
, 'Chromium'),
228 'edge': os
.path
.join(appdata
, 'Microsoft Edge'),
229 'opera': os
.path
.join(appdata
, 'com.operasoftware.Opera'),
230 'vivaldi': os
.path
.join(appdata
, 'Vivaldi'),
234 config
= _config_home()
236 'brave': os
.path
.join(config
, 'BraveSoftware/Brave-Browser'),
237 'chrome': os
.path
.join(config
, 'google-chrome'),
238 'chromium': os
.path
.join(config
, 'chromium'),
239 'edge': os
.path
.join(config
, 'microsoft-edge'),
240 'opera': os
.path
.join(config
, 'opera'),
241 'vivaldi': os
.path
.join(config
, 'vivaldi'),
244 # Linux keyring names can be determined by snooping on dbus while opening the browser in KDE:
245 # dbus-monitor "interface='org.kde.KWallet'" "type=method_return"
249 'chromium': 'Chromium',
250 'edge': 'Microsoft Edge' if sys
.platform
== 'darwin' else 'Chromium',
251 'opera': 'Opera' if sys
.platform
== 'darwin' else 'Chromium',
252 'vivaldi': 'Vivaldi' if sys
.platform
== 'darwin' else 'Chrome',
255 browsers_without_profiles
= {'opera'}
258 'browser_dir': browser_dir
,
259 'keyring_name': keyring_name
,
260 'supports_profiles': browser_name
not in browsers_without_profiles
264 def _extract_chrome_cookies(browser_name
, profile
, keyring
, logger
):
265 logger
.info(f
'Extracting cookies from {browser_name}')
268 logger
.warning(f
'Cannot extract cookies from {browser_name} without sqlite3 support. '
269 'Please use a python interpreter compiled with sqlite3 support')
270 return YoutubeDLCookieJar()
272 config
= _get_chromium_based_browser_settings(browser_name
)
275 search_root
= config
['browser_dir']
276 elif _is_path(profile
):
277 search_root
= profile
278 config
['browser_dir'] = os
.path
.dirname(profile
) if config
['supports_profiles'] else profile
280 if config
['supports_profiles']:
281 search_root
= os
.path
.join(config
['browser_dir'], profile
)
283 logger
.error(f
'{browser_name} does not support profiles')
284 search_root
= config
['browser_dir']
286 cookie_database_path
= _find_most_recently_used_file(search_root
, 'Cookies', logger
)
287 if cookie_database_path
is None:
288 raise FileNotFoundError(f
'could not find {browser_name} cookies database in "{search_root}"')
289 logger
.debug(f
'Extracting cookies from: "{cookie_database_path}"')
291 decryptor
= get_cookie_decryptor(config
['browser_dir'], config
['keyring_name'], logger
, keyring
=keyring
)
293 with tempfile
.TemporaryDirectory(prefix
='yt_dlp') as tmpdir
:
296 cursor
= _open_database_copy(cookie_database_path
, tmpdir
)
297 cursor
.connection
.text_factory
= bytes
298 column_names
= _get_column_names(cursor
, 'cookies')
299 secure_column
= 'is_secure' if 'is_secure' in column_names
else 'secure'
300 cursor
.execute(f
'SELECT host_key, name, value, encrypted_value, path, expires_utc, {secure_column} FROM cookies')
301 jar
= YoutubeDLCookieJar()
303 unencrypted_cookies
= 0
304 with _create_progress_bar(logger
) as progress_bar
:
305 table
= cursor
.fetchall()
306 total_cookie_count
= len(table
)
307 for i
, line
in enumerate(table
):
308 progress_bar
.print(f
'Loading cookie {i: 6d}/{total_cookie_count: 6d}')
309 is_encrypted
, cookie
= _process_chrome_cookie(decryptor
, *line
)
313 elif not is_encrypted
:
314 unencrypted_cookies
+= 1
315 jar
.set_cookie(cookie
)
316 if failed_cookies
> 0:
317 failed_message
= f
' ({failed_cookies} could not be decrypted)'
320 logger
.info(f
'Extracted {len(jar)} cookies from {browser_name}{failed_message}')
321 counts
= decryptor
._cookie
_counts
.copy()
322 counts
['unencrypted'] = unencrypted_cookies
323 logger
.debug(f
'cookie version breakdown: {counts}')
326 if cursor
is not None:
327 cursor
.connection
.close()
330 def _process_chrome_cookie(decryptor
, host_key
, name
, value
, encrypted_value
, path
, expires_utc
, is_secure
):
331 host_key
= host_key
.decode()
333 value
= value
.decode()
335 is_encrypted
= not value
and encrypted_value
338 value
= decryptor
.decrypt(encrypted_value
)
340 return is_encrypted
, None
342 return is_encrypted
, http
.cookiejar
.Cookie(
343 version
=0, name
=name
, value
=value
, port
=None, port_specified
=False,
344 domain
=host_key
, domain_specified
=bool(host_key
), domain_initial_dot
=host_key
.startswith('.'),
345 path
=path
, path_specified
=bool(path
), secure
=is_secure
, expires
=expires_utc
, discard
=False,
346 comment
=None, comment_url
=None, rest
={})
349 class ChromeCookieDecryptor
:
354 - cookies are either v10 or v11
355 - v10: AES-CBC encrypted with a fixed key
356 - also attempts empty password if decryption fails
357 - v11: AES-CBC encrypted with an OS protected key (keyring)
358 - also attempts empty password if decryption fails
359 - v11 keys can be stored in various places depending on the activate desktop environment [2]
362 - cookies are either v10 or not v10
363 - v10: AES-CBC encrypted with an OS protected key (keyring) and more key derivation iterations than linux
364 - not v10: 'old data' stored as plaintext
367 - cookies are either v10 or not v10
368 - v10: AES-GCM encrypted with a key which is encrypted with DPAPI
369 - not v10: encrypted with DPAPI
372 - [1] https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/
373 - [2] https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/sync/key_storage_linux.cc
374 - KeyStorageLinux::CreateService
379 def decrypt(self
, encrypted_value
):
380 raise NotImplementedError('Must be implemented by sub classes')
383 def get_cookie_decryptor(browser_root
, browser_keyring_name
, logger
, *, keyring
=None):
384 if sys
.platform
== 'darwin':
385 return MacChromeCookieDecryptor(browser_keyring_name
, logger
)
386 elif sys
.platform
in ('win32', 'cygwin'):
387 return WindowsChromeCookieDecryptor(browser_root
, logger
)
388 return LinuxChromeCookieDecryptor(browser_keyring_name
, logger
, keyring
=keyring
)
391 class LinuxChromeCookieDecryptor(ChromeCookieDecryptor
):
392 def __init__(self
, browser_keyring_name
, logger
, *, keyring
=None):
393 self
._logger
= logger
394 self
._v
10_key
= self
.derive_key(b
'peanuts')
395 self
._empty
_key
= self
.derive_key(b
'')
396 self
._cookie
_counts
= {'v10': 0, 'v11': 0, 'other': 0}
397 self
._browser
_keyring
_name
= browser_keyring_name
398 self
._keyring
= keyring
400 @functools.cached_property
402 password
= _get_linux_keyring_password(self
._browser
_keyring
_name
, self
._keyring
, self
._logger
)
403 return None if password
is None else self
.derive_key(password
)
406 def derive_key(password
):
408 # https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/sync/os_crypt_linux.cc
409 return pbkdf2_sha1(password
, salt
=b
'saltysalt', iterations
=1, key_length
=16)
411 def decrypt(self
, encrypted_value
):
414 following the same approach as the fix in [1]: if cookies fail to decrypt then attempt to decrypt
415 with an empty password. The failure detection is not the same as what chromium uses so the
416 results won't be perfect
419 - [1] https://chromium.googlesource.com/chromium/src/+/bbd54702284caca1f92d656fdcadf2ccca6f4165%5E%21/
420 - a bugfix to try an empty password as a fallback
422 version
= encrypted_value
[:3]
423 ciphertext
= encrypted_value
[3:]
425 if version
== b
'v10':
426 self
._cookie
_counts
['v10'] += 1
427 return _decrypt_aes_cbc_multi(ciphertext
, (self
._v
10_key
, self
._empty
_key
), self
._logger
)
429 elif version
== b
'v11':
430 self
._cookie
_counts
['v11'] += 1
431 if self
._v
11_key
is None:
432 self
._logger
.warning('cannot decrypt v11 cookies: no key found', only_once
=True)
434 return _decrypt_aes_cbc_multi(ciphertext
, (self
._v
11_key
, self
._empty
_key
), self
._logger
)
437 self
._logger
.warning(f
'unknown cookie version: "{version}"', only_once
=True)
438 self
._cookie
_counts
['other'] += 1
442 class MacChromeCookieDecryptor(ChromeCookieDecryptor
):
443 def __init__(self
, browser_keyring_name
, logger
):
444 self
._logger
= logger
445 password
= _get_mac_keyring_password(browser_keyring_name
, logger
)
446 self
._v
10_key
= None if password
is None else self
.derive_key(password
)
447 self
._cookie
_counts
= {'v10': 0, 'other': 0}
450 def derive_key(password
):
452 # https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/sync/os_crypt_mac.mm
453 return pbkdf2_sha1(password
, salt
=b
'saltysalt', iterations
=1003, key_length
=16)
455 def decrypt(self
, encrypted_value
):
456 version
= encrypted_value
[:3]
457 ciphertext
= encrypted_value
[3:]
459 if version
== b
'v10':
460 self
._cookie
_counts
['v10'] += 1
461 if self
._v
10_key
is None:
462 self
._logger
.warning('cannot decrypt v10 cookies: no key found', only_once
=True)
465 return _decrypt_aes_cbc_multi(ciphertext
, (self
._v
10_key
,), self
._logger
)
468 self
._cookie
_counts
['other'] += 1
469 # other prefixes are considered 'old data' which were stored as plaintext
470 # https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/sync/os_crypt_mac.mm
471 return encrypted_value
474 class WindowsChromeCookieDecryptor(ChromeCookieDecryptor
):
475 def __init__(self
, browser_root
, logger
):
476 self
._logger
= logger
477 self
._v
10_key
= _get_windows_v10_key(browser_root
, logger
)
478 self
._cookie
_counts
= {'v10': 0, 'other': 0}
480 def decrypt(self
, encrypted_value
):
481 version
= encrypted_value
[:3]
482 ciphertext
= encrypted_value
[3:]
484 if version
== b
'v10':
485 self
._cookie
_counts
['v10'] += 1
486 if self
._v
10_key
is None:
487 self
._logger
.warning('cannot decrypt v10 cookies: no key found', only_once
=True)
490 # https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/sync/os_crypt_win.cc
492 nonce_length
= 96 // 8
494 # EVP_AEAD_AES_GCM_TAG_LEN
495 authentication_tag_length
= 16
497 raw_ciphertext
= ciphertext
498 nonce
= raw_ciphertext
[:nonce_length
]
499 ciphertext
= raw_ciphertext
[nonce_length
:-authentication_tag_length
]
500 authentication_tag
= raw_ciphertext
[-authentication_tag_length
:]
502 return _decrypt_aes_gcm(ciphertext
, self
._v
10_key
, nonce
, authentication_tag
, self
._logger
)
505 self
._cookie
_counts
['other'] += 1
506 # any other prefix means the data is DPAPI encrypted
507 # https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/sync/os_crypt_win.cc
508 return _decrypt_windows_dpapi(encrypted_value
, self
._logger
).decode()
511 def _extract_safari_cookies(profile
, logger
):
512 if sys
.platform
!= 'darwin':
513 raise ValueError(f
'unsupported platform: {sys.platform}')
516 cookies_path
= os
.path
.expanduser(profile
)
517 if not os
.path
.isfile(cookies_path
):
518 raise FileNotFoundError('custom safari cookies database not found')
521 cookies_path
= os
.path
.expanduser('~/Library/Cookies/Cookies.binarycookies')
523 if not os
.path
.isfile(cookies_path
):
524 logger
.debug('Trying secondary cookie location')
525 cookies_path
= os
.path
.expanduser('~/Library/Containers/com.apple.Safari/Data/Library/Cookies/Cookies.binarycookies')
526 if not os
.path
.isfile(cookies_path
):
527 raise FileNotFoundError('could not find safari cookies database')
529 with open(cookies_path
, 'rb') as f
:
530 cookies_data
= f
.read()
532 jar
= parse_safari_cookies(cookies_data
, logger
=logger
)
533 logger
.info(f
'Extracted {len(jar)} cookies from safari')
537 class ParserError(Exception):
542 def __init__(self
, data
, logger
):
545 self
._logger
= logger
547 def read_bytes(self
, num_bytes
):
549 raise ParserError(f
'invalid read of {num_bytes} bytes')
550 end
= self
.cursor
+ num_bytes
551 if end
> len(self
._data
):
552 raise ParserError('reached end of input')
553 data
= self
._data
[self
.cursor
:end
]
557 def expect_bytes(self
, expected_value
, message
):
558 value
= self
.read_bytes(len(expected_value
))
559 if value
!= expected_value
:
560 raise ParserError(f
'unexpected value: {value} != {expected_value} ({message})')
562 def read_uint(self
, big_endian
=False):
563 data_format
= '>I' if big_endian
else '<I'
564 return struct
.unpack(data_format
, self
.read_bytes(4))[0]
566 def read_double(self
, big_endian
=False):
567 data_format
= '>d' if big_endian
else '<d'
568 return struct
.unpack(data_format
, self
.read_bytes(8))[0]
570 def read_cstring(self
):
573 c
= self
.read_bytes(1)
575 return b
''.join(buffer).decode()
579 def skip(self
, num_bytes
, description
='unknown'):
581 self
._logger
.debug(f
'skipping {num_bytes} bytes ({description}): {self.read_bytes(num_bytes)!r}')
583 raise ParserError(f
'invalid skip of {num_bytes} bytes')
585 def skip_to(self
, offset
, description
='unknown'):
586 self
.skip(offset
- self
.cursor
, description
)
588 def skip_to_end(self
, description
='unknown'):
589 self
.skip_to(len(self
._data
), description
)
592 def _mac_absolute_time_to_posix(timestamp
):
593 return int((datetime(2001, 1, 1, 0, 0, tzinfo
=timezone
.utc
) + timedelta(seconds
=timestamp
)).timestamp())
596 def _parse_safari_cookies_header(data
, logger
):
597 p
= DataParser(data
, logger
)
598 p
.expect_bytes(b
'cook', 'database signature')
599 number_of_pages
= p
.read_uint(big_endian
=True)
600 page_sizes
= [p
.read_uint(big_endian
=True) for _
in range(number_of_pages
)]
601 return page_sizes
, p
.cursor
604 def _parse_safari_cookies_page(data
, jar
, logger
):
605 p
= DataParser(data
, logger
)
606 p
.expect_bytes(b
'\x00\x00\x01\x00', 'page signature')
607 number_of_cookies
= p
.read_uint()
608 record_offsets
= [p
.read_uint() for _
in range(number_of_cookies
)]
609 if number_of_cookies
== 0:
610 logger
.debug(f
'a cookies page of size {len(data)} has no cookies')
613 p
.skip_to(record_offsets
[0], 'unknown page header field')
615 with _create_progress_bar(logger
) as progress_bar
:
616 for i
, record_offset
in enumerate(record_offsets
):
617 progress_bar
.print(f
'Loading cookie {i: 6d}/{number_of_cookies: 6d}')
618 p
.skip_to(record_offset
, 'space between records')
619 record_length
= _parse_safari_cookies_record(data
[record_offset
:], jar
, logger
)
620 p
.read_bytes(record_length
)
621 p
.skip_to_end('space in between pages')
624 def _parse_safari_cookies_record(data
, jar
, logger
):
625 p
= DataParser(data
, logger
)
626 record_size
= p
.read_uint()
627 p
.skip(4, 'unknown record field 1')
628 flags
= p
.read_uint()
629 is_secure
= bool(flags
& 0x0001)
630 p
.skip(4, 'unknown record field 2')
631 domain_offset
= p
.read_uint()
632 name_offset
= p
.read_uint()
633 path_offset
= p
.read_uint()
634 value_offset
= p
.read_uint()
635 p
.skip(8, 'unknown record field 3')
636 expiration_date
= _mac_absolute_time_to_posix(p
.read_double())
637 _creation_date
= _mac_absolute_time_to_posix(p
.read_double()) # noqa: F841
640 p
.skip_to(domain_offset
)
641 domain
= p
.read_cstring()
643 p
.skip_to(name_offset
)
644 name
= p
.read_cstring()
646 p
.skip_to(path_offset
)
647 path
= p
.read_cstring()
649 p
.skip_to(value_offset
)
650 value
= p
.read_cstring()
651 except UnicodeDecodeError:
652 logger
.warning('failed to parse Safari cookie because UTF-8 decoding failed', only_once
=True)
655 p
.skip_to(record_size
, 'space at the end of the record')
657 cookie
= http
.cookiejar
.Cookie(
658 version
=0, name
=name
, value
=value
, port
=None, port_specified
=False,
659 domain
=domain
, domain_specified
=bool(domain
), domain_initial_dot
=domain
.startswith('.'),
660 path
=path
, path_specified
=bool(path
), secure
=is_secure
, expires
=expiration_date
, discard
=False,
661 comment
=None, comment_url
=None, rest
={})
662 jar
.set_cookie(cookie
)
666 def parse_safari_cookies(data
, jar
=None, logger
=YDLLogger()):
669 - https://github.com/libyal/dtformats/blob/main/documentation/Safari%20Cookies.asciidoc
670 - this data appears to be out of date but the important parts of the database structure is the same
671 - there are a few bytes here and there which are skipped during parsing
674 jar
= YoutubeDLCookieJar()
675 page_sizes
, body_start
= _parse_safari_cookies_header(data
, logger
)
676 p
= DataParser(data
[body_start
:], logger
)
677 for page_size
in page_sizes
:
678 _parse_safari_cookies_page(p
.read_bytes(page_size
), jar
, logger
)
679 p
.skip_to_end('footer')
683 class _LinuxDesktopEnvironment(Enum
):
685 https://chromium.googlesource.com/chromium/src/+/refs/heads/main/base/nix/xdg_util.h
703 class _LinuxKeyring(Enum
):
705 https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/sync/key_storage_util_linux.h
708 KWALLET
= auto() # KDE4
711 GNOMEKEYRING
= auto()
715 SUPPORTED_KEYRINGS
= _LinuxKeyring
.__members
__.keys()
718 def _get_linux_desktop_environment(env
, logger
):
720 https://chromium.googlesource.com/chromium/src/+/refs/heads/main/base/nix/xdg_util.cc
721 GetDesktopEnvironment
723 xdg_current_desktop
= env
.get('XDG_CURRENT_DESKTOP', None)
724 desktop_session
= env
.get('DESKTOP_SESSION', None)
725 if xdg_current_desktop
is not None:
726 xdg_current_desktop
= xdg_current_desktop
.split(':')[0].strip()
728 if xdg_current_desktop
== 'Unity':
729 if desktop_session
is not None and 'gnome-fallback' in desktop_session
:
730 return _LinuxDesktopEnvironment
.GNOME
732 return _LinuxDesktopEnvironment
.UNITY
733 elif xdg_current_desktop
== 'Deepin':
734 return _LinuxDesktopEnvironment
.DEEPIN
735 elif xdg_current_desktop
== 'GNOME':
736 return _LinuxDesktopEnvironment
.GNOME
737 elif xdg_current_desktop
== 'X-Cinnamon':
738 return _LinuxDesktopEnvironment
.CINNAMON
739 elif xdg_current_desktop
== 'KDE':
740 kde_version
= env
.get('KDE_SESSION_VERSION', None)
741 if kde_version
== '5':
742 return _LinuxDesktopEnvironment
.KDE5
743 elif kde_version
== '6':
744 return _LinuxDesktopEnvironment
.KDE6
745 elif kde_version
== '4':
746 return _LinuxDesktopEnvironment
.KDE4
748 logger
.info(f
'unknown KDE version: "{kde_version}". Assuming KDE4')
749 return _LinuxDesktopEnvironment
.KDE4
750 elif xdg_current_desktop
== 'Pantheon':
751 return _LinuxDesktopEnvironment
.PANTHEON
752 elif xdg_current_desktop
== 'XFCE':
753 return _LinuxDesktopEnvironment
.XFCE
754 elif xdg_current_desktop
== 'UKUI':
755 return _LinuxDesktopEnvironment
.UKUI
756 elif xdg_current_desktop
== 'LXQt':
757 return _LinuxDesktopEnvironment
.LXQT
759 logger
.info(f
'XDG_CURRENT_DESKTOP is set to an unknown value: "{xdg_current_desktop}"')
761 elif desktop_session
is not None:
762 if desktop_session
== 'deepin':
763 return _LinuxDesktopEnvironment
.DEEPIN
764 elif desktop_session
in ('mate', 'gnome'):
765 return _LinuxDesktopEnvironment
.GNOME
766 elif desktop_session
in ('kde4', 'kde-plasma'):
767 return _LinuxDesktopEnvironment
.KDE4
768 elif desktop_session
== 'kde':
769 if 'KDE_SESSION_VERSION' in env
:
770 return _LinuxDesktopEnvironment
.KDE4
772 return _LinuxDesktopEnvironment
.KDE3
773 elif 'xfce' in desktop_session
or desktop_session
== 'xubuntu':
774 return _LinuxDesktopEnvironment
.XFCE
775 elif desktop_session
== 'ukui':
776 return _LinuxDesktopEnvironment
.UKUI
778 logger
.info(f
'DESKTOP_SESSION is set to an unknown value: "{desktop_session}"')
781 if 'GNOME_DESKTOP_SESSION_ID' in env
:
782 return _LinuxDesktopEnvironment
.GNOME
783 elif 'KDE_FULL_SESSION' in env
:
784 if 'KDE_SESSION_VERSION' in env
:
785 return _LinuxDesktopEnvironment
.KDE4
787 return _LinuxDesktopEnvironment
.KDE3
788 return _LinuxDesktopEnvironment
.OTHER
791 def _choose_linux_keyring(logger
):
795 There is currently support for forcing chromium to use BASIC_TEXT by creating a file called
796 `Disable Local Encryption` [1] in the user data dir. The function to write this file (`WriteBackendUse()` [1])
797 does not appear to be called anywhere other than in tests, so the user would have to create this file manually
798 and so would be aware enough to tell yt-dlp to use the BASIC_TEXT keyring.
801 - [1] https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/sync/key_storage_util_linux.cc
803 desktop_environment
= _get_linux_desktop_environment(os
.environ
, logger
)
804 logger
.debug(f
'detected desktop environment: {desktop_environment.name}')
805 if desktop_environment
== _LinuxDesktopEnvironment
.KDE4
:
806 linux_keyring
= _LinuxKeyring
.KWALLET
807 elif desktop_environment
== _LinuxDesktopEnvironment
.KDE5
:
808 linux_keyring
= _LinuxKeyring
.KWALLET5
809 elif desktop_environment
== _LinuxDesktopEnvironment
.KDE6
:
810 linux_keyring
= _LinuxKeyring
.KWALLET6
811 elif desktop_environment
in (
812 _LinuxDesktopEnvironment
.KDE3
, _LinuxDesktopEnvironment
.LXQT
, _LinuxDesktopEnvironment
.OTHER
814 linux_keyring
= _LinuxKeyring
.BASICTEXT
816 linux_keyring
= _LinuxKeyring
.GNOMEKEYRING
820 def _get_kwallet_network_wallet(keyring
, logger
):
821 """ The name of the wallet used to store network passwords.
823 https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/sync/kwallet_dbus.cc
824 KWalletDBus::NetworkWallet
825 which does a dbus call to the following function:
826 https://api.kde.org/frameworks/kwallet/html/classKWallet_1_1Wallet.html
827 Wallet::NetworkWallet
829 default_wallet
= 'kdewallet'
831 if keyring
== _LinuxKeyring
.KWALLET
:
832 service_name
= 'org.kde.kwalletd'
833 wallet_path
= '/modules/kwalletd'
834 elif keyring
== _LinuxKeyring
.KWALLET5
:
835 service_name
= 'org.kde.kwalletd5'
836 wallet_path
= '/modules/kwalletd5'
837 elif keyring
== _LinuxKeyring
.KWALLET6
:
838 service_name
= 'org.kde.kwalletd6'
839 wallet_path
= '/modules/kwalletd6'
841 raise ValueError(keyring
)
843 stdout
, _
, returncode
= Popen
.run([
844 'dbus-send', '--session', '--print-reply=literal',
845 f
'--dest={service_name}',
847 'org.kde.KWallet.networkWallet'
848 ], text
=True, stdout
=subprocess
.PIPE
, stderr
=subprocess
.DEVNULL
)
851 logger
.warning('failed to read NetworkWallet')
852 return default_wallet
854 logger
.debug(f
'NetworkWallet = "{stdout.strip()}"')
855 return stdout
.strip()
856 except Exception as e
:
857 logger
.warning(f
'exception while obtaining NetworkWallet: {e}')
858 return default_wallet
861 def _get_kwallet_password(browser_keyring_name
, keyring
, logger
):
862 logger
.debug(f
'using kwallet-query to obtain password from {keyring.name}')
864 if shutil
.which('kwallet-query') is None:
865 logger
.error('kwallet-query command not found. KWallet and kwallet-query '
866 'must be installed to read from KWallet. kwallet-query should be'
867 'included in the kwallet package for your distribution')
870 network_wallet
= _get_kwallet_network_wallet(keyring
, logger
)
873 stdout
, _
, returncode
= Popen
.run([
875 '--read-password', f
'{browser_keyring_name} Safe Storage',
876 '--folder', f
'{browser_keyring_name} Keys',
878 ], stdout
=subprocess
.PIPE
, stderr
=subprocess
.DEVNULL
)
881 logger
.error(f
'kwallet-query failed with return code {returncode}. '
882 'Please consult the kwallet-query man page for details')
885 if stdout
.lower().startswith(b
'failed to read'):
886 logger
.debug('failed to read password from kwallet. Using empty string instead')
887 # this sometimes occurs in KDE because chrome does not check hasEntry and instead
888 # just tries to read the value (which kwallet returns "") whereas kwallet-query
889 # checks hasEntry. To verify this:
890 # dbus-monitor "interface='org.kde.KWallet'" "type=method_return"
891 # while starting chrome.
892 # this was identified as a bug later and fixed in
893 # https://chromium.googlesource.com/chromium/src/+/bbd54702284caca1f92d656fdcadf2ccca6f4165%5E%21/#F0
894 # https://chromium.googlesource.com/chromium/src/+/5463af3c39d7f5b6d11db7fbd51e38cc1974d764
897 logger
.debug('password found')
898 return stdout
.rstrip(b
'\n')
899 except Exception as e
:
900 logger
.warning(f
'exception running kwallet-query: {error_to_str(e)}')
904 def _get_gnome_keyring_password(browser_keyring_name
, logger
):
905 if not secretstorage
:
906 logger
.error(f
'secretstorage not available {_SECRETSTORAGE_UNAVAILABLE_REASON}')
908 # the Gnome keyring does not seem to organise keys in the same way as KWallet,
909 # using `dbus-monitor` during startup, it can be observed that chromium lists all keys
910 # and presumably searches for its key in the list. It appears that we must do the same.
911 # https://github.com/jaraco/keyring/issues/556
912 with contextlib
.closing(secretstorage
.dbus_init()) as con
:
913 col
= secretstorage
.get_default_collection(con
)
914 for item
in col
.get_all_items():
915 if item
.get_label() == f
'{browser_keyring_name} Safe Storage':
916 return item
.get_secret()
918 logger
.error('failed to read from keyring')
922 def _get_linux_keyring_password(browser_keyring_name
, keyring
, logger
):
923 # note: chrome/chromium can be run with the following flags to determine which keyring backend
924 # it has chosen to use
925 # chromium --enable-logging=stderr --v=1 2>&1 | grep key_storage_
926 # Chromium supports a flag: --password-store=<basic|gnome|kwallet> so the automatic detection
927 # will not be sufficient in all cases.
929 keyring
= _LinuxKeyring
[keyring
] if keyring
else _choose_linux_keyring(logger
)
930 logger
.debug(f
'Chosen keyring: {keyring.name}')
932 if keyring
in (_LinuxKeyring
.KWALLET
, _LinuxKeyring
.KWALLET5
, _LinuxKeyring
.KWALLET6
):
933 return _get_kwallet_password(browser_keyring_name
, keyring
, logger
)
934 elif keyring
== _LinuxKeyring
.GNOMEKEYRING
:
935 return _get_gnome_keyring_password(browser_keyring_name
, logger
)
936 elif keyring
== _LinuxKeyring
.BASICTEXT
:
937 # when basic text is chosen, all cookies are stored as v10 (so no keyring password is required)
939 assert False, f
'Unknown keyring {keyring}'
942 def _get_mac_keyring_password(browser_keyring_name
, logger
):
943 logger
.debug('using find-generic-password to obtain password from OSX keychain')
945 stdout
, _
, returncode
= Popen
.run(
946 ['security', 'find-generic-password',
947 '-w', # write password to stdout
948 '-a', browser_keyring_name
, # match 'account'
949 '-s', f
'{browser_keyring_name} Safe Storage'], # match 'service'
950 stdout
=subprocess
.PIPE
, stderr
=subprocess
.DEVNULL
)
952 logger
.warning('find-generic-password failed')
954 return stdout
.rstrip(b
'\n')
955 except Exception as e
:
956 logger
.warning(f
'exception running find-generic-password: {error_to_str(e)}')
960 def _get_windows_v10_key(browser_root
, logger
):
963 - [1] https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/os_crypt/sync/os_crypt_win.cc
965 path
= _find_most_recently_used_file(browser_root
, 'Local State', logger
)
967 logger
.error('could not find local state file')
969 logger
.debug(f
'Found local state file at "{path}"')
970 with open(path
, encoding
='utf8') as f
:
973 # kOsCryptEncryptedKeyPrefName in [1]
974 base64_key
= data
['os_crypt']['encrypted_key']
976 logger
.error('no encrypted key in Local State')
978 encrypted_key
= base64
.b64decode(base64_key
)
979 # kDPAPIKeyPrefix in [1]
981 if not encrypted_key
.startswith(prefix
):
982 logger
.error('invalid key')
984 return _decrypt_windows_dpapi(encrypted_key
[len(prefix
):], logger
)
987 def pbkdf2_sha1(password
, salt
, iterations
, key_length
):
988 return pbkdf2_hmac('sha1', password
, salt
, iterations
, key_length
)
991 def _decrypt_aes_cbc_multi(ciphertext
, keys
, logger
, initialization_vector
=b
' ' * 16):
993 plaintext
= unpad_pkcs7(aes_cbc_decrypt_bytes(ciphertext
, key
, initialization_vector
))
995 return plaintext
.decode()
996 except UnicodeDecodeError:
998 logger
.warning('failed to decrypt cookie (AES-CBC) because UTF-8 decoding failed. Possibly the key is wrong?', only_once
=True)
1002 def _decrypt_aes_gcm(ciphertext
, key
, nonce
, authentication_tag
, logger
):
1004 plaintext
= aes_gcm_decrypt_and_verify_bytes(ciphertext
, key
, authentication_tag
, nonce
)
1006 logger
.warning('failed to decrypt cookie (AES-GCM) because the MAC check failed. Possibly the key is wrong?', only_once
=True)
1010 return plaintext
.decode()
1011 except UnicodeDecodeError:
1012 logger
.warning('failed to decrypt cookie (AES-GCM) because UTF-8 decoding failed. Possibly the key is wrong?', only_once
=True)
1016 def _decrypt_windows_dpapi(ciphertext
, logger
):
1019 - https://docs.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptunprotectdata
1023 import ctypes
.wintypes
1025 class DATA_BLOB(ctypes
.Structure
):
1026 _fields_
= [('cbData', ctypes
.wintypes
.DWORD
),
1027 ('pbData', ctypes
.POINTER(ctypes
.c_char
))]
1029 buffer = ctypes
.create_string_buffer(ciphertext
)
1030 blob_in
= DATA_BLOB(ctypes
.sizeof(buffer), buffer)
1031 blob_out
= DATA_BLOB()
1032 ret
= ctypes
.windll
.crypt32
.CryptUnprotectData(
1033 ctypes
.byref(blob_in
), # pDataIn
1034 None, # ppszDataDescr: human readable description of pDataIn
1035 None, # pOptionalEntropy: salt?
1036 None, # pvReserved: must be NULL
1037 None, # pPromptStruct: information about prompts to display
1039 ctypes
.byref(blob_out
) # pDataOut
1042 logger
.warning('failed to decrypt with DPAPI', only_once
=True)
1045 result
= ctypes
.string_at(blob_out
.pbData
, blob_out
.cbData
)
1046 ctypes
.windll
.kernel32
.LocalFree(blob_out
.pbData
)
1051 return os
.environ
.get('XDG_CONFIG_HOME', os
.path
.expanduser('~/.config'))
1054 def _open_database_copy(database_path
, tmpdir
):
1055 # cannot open sqlite databases if they are already in use (e.g. by the browser)
1056 database_copy_path
= os
.path
.join(tmpdir
, 'temporary.sqlite')
1057 shutil
.copy(database_path
, database_copy_path
)
1058 conn
= sqlite3
.connect(database_copy_path
)
1059 return conn
.cursor()
1062 def _get_column_names(cursor
, table_name
):
1063 table_info
= cursor
.execute(f
'PRAGMA table_info({table_name})').fetchall()
1064 return [row
[1].decode() for row
in table_info
]
1067 def _find_most_recently_used_file(root
, filename
, logger
):
1068 # if there are multiple browser profiles, take the most recently used one
1070 with _create_progress_bar(logger
) as progress_bar
:
1071 for curr_root
, dirs
, files
in os
.walk(root
):
1074 progress_bar
.print(f
'Searching for "{filename}": {i: 6d} files searched')
1075 if file == filename
:
1076 paths
.append(os
.path
.join(curr_root
, file))
1077 return None if not paths
else max(paths
, key
=lambda path
: os
.lstat(path
).st_mtime
)
1080 def _merge_cookie_jars(jars
):
1081 output_jar
= YoutubeDLCookieJar()
1084 output_jar
.set_cookie(cookie
)
1085 if jar
.filename
is not None:
1086 output_jar
.filename
= jar
.filename
1090 def _is_path(value
):
1091 return os
.path
.sep
in value
1094 def _parse_browser_specification(browser_name
, profile
=None, keyring
=None, container
=None):
1095 if browser_name
not in SUPPORTED_BROWSERS
:
1096 raise ValueError(f
'unsupported browser: "{browser_name}"')
1097 if keyring
not in (None, *SUPPORTED_KEYRINGS
):
1098 raise ValueError(f
'unsupported keyring: "{keyring}"')
1099 if profile
is not None and _is_path(expand_path(profile
)):
1100 profile
= expand_path(profile
)
1101 return browser_name
, profile
, keyring
, container
1104 class LenientSimpleCookie(http
.cookies
.SimpleCookie
):
1105 """More lenient version of http.cookies.SimpleCookie"""
1106 # From https://github.com/python/cpython/blob/v3.10.7/Lib/http/cookies.py
1107 # We use Morsel's legal key chars to avoid errors on setting values
1108 _LEGAL_KEY_CHARS
= r
'\w\d' + re
.escape('!#$%&\'*+-.:^_`|~')
1109 _LEGAL_VALUE_CHARS
= _LEGAL_KEY_CHARS
+ re
.escape('(),/<=>?@[]{}')
1123 _FLAGS
= {"secure", "httponly"}
1125 # Added 'bad' group to catch the remaining value
1126 _COOKIE_PATTERN
= re
.compile(r
"""
1127 \s* # Optional whitespace at start of cookie
1128 (?P<key> # Start of group 'key'
1129 [""" + _LEGAL_KEY_CHARS
+ r
"""]+?# Any word of at least one letter
1130 ) # End of group 'key'
1131 ( # Optional group: there may not be a value.
1132 \s*=\s* # Equal Sign
1133 ( # Start of potential value
1134 (?P<val> # Start of group 'val'
1135 "(?:[^\\"]|\\.)*" # Any doublequoted string
1137 \w{3},\s[\w\d\s-]{9,11}\s[\d:]{8}\sGMT # Special case for "expires" attr
1139 [""" + _LEGAL_VALUE_CHARS
+ r
"""]* # Any word or empty string
1140 ) # End of group 'val'
1142 (?P<bad>(?:\\;|[^;])*?) # 'bad' group fallback for invalid values
1143 ) # End of potential value
1144 )? # End of optional value group
1145 \s* # Any number of spaces.
1146 (\s+|;|$) # Ending either at space, semicolon, or EOS.
1147 """, re
.ASCII | re
.VERBOSE
)
1149 def load(self
, data
):
1150 # Workaround for https://github.com/yt-dlp/yt-dlp/issues/4776
1151 if not isinstance(data
, str):
1152 return super().load(data
)
1155 for match
in self
._COOKIE
_PATTERN
.finditer(data
):
1156 if match
.group('bad'):
1160 key
, value
= match
.group('key', 'val')
1162 is_attribute
= False
1163 if key
.startswith('$'):
1167 lower_key
= key
.lower()
1168 if lower_key
in self
._RESERVED
:
1173 if lower_key
not in self
._FLAGS
:
1178 value
, _
= self
.value_decode(value
)
1185 elif value
is not None:
1186 morsel
= self
.get(key
, http
.cookies
.Morsel())
1187 real_value
, coded_value
= self
.value_decode(value
)
1188 morsel
.set(key
, real_value
, coded_value
)
1195 class YoutubeDLCookieJar(http
.cookiejar
.MozillaCookieJar
):
1197 See [1] for cookie file format.
1199 1. https://curl.haxx.se/docs/http-cookies.html
1201 _HTTPONLY_PREFIX
= '#HttpOnly_'
1203 _HEADER
= '''# Netscape HTTP Cookie File
1204 # This file is generated by yt-dlp. Do not edit.
1207 _CookieFileEntry
= collections
.namedtuple(
1209 ('domain_name', 'include_subdomains', 'path', 'https_only', 'expires_at', 'name', 'value'))
1211 def __init__(self
, filename
=None, *args
, **kwargs
):
1212 super().__init
__(None, *args
, **kwargs
)
1213 if is_path_like(filename
):
1214 filename
= os
.fspath(filename
)
1215 self
.filename
= filename
1218 def _true_or_false(cndn
):
1219 return 'TRUE' if cndn
else 'FALSE'
1221 @contextlib.contextmanager
1222 def open(self
, file, *, write
=False):
1223 if is_path_like(file):
1224 with open(file, 'w' if write
else 'r', encoding
='utf-8') as f
:
1231 def _really_save(self
, f
, ignore_discard
=False, ignore_expires
=False):
1234 if (not ignore_discard
and cookie
.discard
1235 or not ignore_expires
and cookie
.is_expired(now
)):
1237 name
, value
= cookie
.name
, cookie
.value
1239 # cookies.txt regards 'Set-Cookie: foo' as a cookie
1240 # with no name, whereas http.cookiejar regards it as a
1241 # cookie with no value.
1242 name
, value
= '', name
1243 f
.write('%s\n' % '\t'.join((
1245 self
._true
_or
_false
(cookie
.domain
.startswith('.')),
1247 self
._true
_or
_false
(cookie
.secure
),
1248 str_or_none(cookie
.expires
, default
=''),
1252 def save(self
, filename
=None, *args
, **kwargs
):
1254 Save cookies to a file.
1255 Code is taken from CPython 3.6
1256 https://github.com/python/cpython/blob/8d999cbf4adea053be6dbb612b9844635c4dfb8e/Lib/http/cookiejar.py#L2091-L2117 """
1258 if filename
is None:
1259 if self
.filename
is not None:
1260 filename
= self
.filename
1262 raise ValueError(http
.cookiejar
.MISSING_FILENAME_TEXT
)
1264 # Store session cookies with `expires` set to 0 instead of an empty string
1266 if cookie
.expires
is None:
1269 with self
.open(filename
, write
=True) as f
:
1270 f
.write(self
._HEADER
)
1271 self
._really
_save
(f
, *args
, **kwargs
)
1273 def load(self
, filename
=None, ignore_discard
=False, ignore_expires
=False):
1274 """Load cookies from a file."""
1275 if filename
is None:
1276 if self
.filename
is not None:
1277 filename
= self
.filename
1279 raise ValueError(http
.cookiejar
.MISSING_FILENAME_TEXT
)
1281 def prepare_line(line
):
1282 if line
.startswith(self
._HTTPONLY
_PREFIX
):
1283 line
= line
[len(self
._HTTPONLY
_PREFIX
):]
1284 # comments and empty lines are fine
1285 if line
.startswith('#') or not line
.strip():
1287 cookie_list
= line
.split('\t')
1288 if len(cookie_list
) != self
._ENTRY
_LEN
:
1289 raise http
.cookiejar
.LoadError('invalid length %d' % len(cookie_list
))
1290 cookie
= self
._CookieFileEntry
(*cookie_list
)
1291 if cookie
.expires_at
and not cookie
.expires_at
.isdigit():
1292 raise http
.cookiejar
.LoadError('invalid expires at %s' % cookie
.expires_at
)
1296 with self
.open(filename
) as f
:
1299 cf
.write(prepare_line(line
))
1300 except http
.cookiejar
.LoadError
as e
:
1301 if f
'{line.strip()} '[0] in '[{"':
1302 raise http
.cookiejar
.LoadError(
1303 'Cookies file must be Netscape formatted, not JSON. See '
1304 'https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp')
1305 write_string(f
'WARNING: skipping cookie file entry due to {e}: {line!r}\n')
1308 self
._really
_load
(cf
, filename
, ignore_discard
, ignore_expires
)
1309 # Session cookies are denoted by either `expires` field set to
1310 # an empty string or 0. MozillaCookieJar only recognizes the former
1311 # (see [1]). So we need force the latter to be recognized as session
1312 # cookies on our own.
1313 # Session cookies may be important for cookies-based authentication,
1314 # e.g. usually, when user does not check 'Remember me' check box while
1315 # logging in on a site, some important cookies are stored as session
1316 # cookies so that not recognizing them will result in failed login.
1317 # 1. https://bugs.python.org/issue17164
1319 # Treat `expires=0` cookies as session cookies
1320 if cookie
.expires
== 0:
1321 cookie
.expires
= None
1322 cookie
.discard
= True
1324 def get_cookie_header(self
, url
):
1325 """Generate a Cookie HTTP header for a given url"""
1326 cookie_req
= urllib
.request
.Request(escape_url(sanitize_url(url
)))
1327 self
.add_cookie_header(cookie_req
)
1328 return cookie_req
.get_header('Cookie')
1330 def clear(self
, *args
, **kwargs
):
1331 with contextlib
.suppress(KeyError):
1332 return super().clear(*args
, **kwargs
)