[cookies] Support firefox container in --cookies-from-browser (#4753)

Authored by: bashonly
This commit is contained in:
bashonly 2022-08-30 16:54:46 +00:00 committed by GitHub
parent 459262ac97
commit 9bd13fe5bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 54 additions and 21 deletions

View file

@ -706,13 +706,14 @@ ## Filesystem Options:
and dump cookie jar in and dump cookie jar in
--no-cookies Do not read/dump cookies from/to file --no-cookies Do not read/dump cookies from/to file
(default) (default)
--cookies-from-browser BROWSER[+KEYRING][:PROFILE] --cookies-from-browser BROWSER[+KEYRING][:PROFILE[:CONTAINER]]
The name of the browser and (optionally) the The name of the browser and (optionally) the
name/path of the profile to load cookies name/path of the profile to load cookies
from, separated by a ":". Currently from (and container name if Firefox)
supported browsers are: brave, chrome, separated by a ":". Currently supported
chromium, edge, firefox, opera, safari, browsers are: brave, chrome, chromium, edge,
vivaldi. By default, the most recently firefox, opera, safari, vivaldi. By default,
the default container of the most recently
accessed profile is used. The keyring used accessed profile is used. The keyring used
for decrypting Chromium cookies on Linux can for decrypting Chromium cookies on Linux can
be (optionally) specified after the browser be (optionally) specified after the browser

View file

@ -304,8 +304,9 @@ class YoutubeDL:
should act on each input URL as opposed to for the entire queue should act on each input URL as opposed to for the entire queue
cookiefile: File name or text stream from where cookies should be read and dumped to cookiefile: File name or text stream from where cookies should be read and dumped to
cookiesfrombrowser: A tuple containing the name of the browser, the profile cookiesfrombrowser: A tuple containing the name of the browser, the profile
name/path from where cookies are loaded, and the name of the name/path from where cookies are loaded, the name of the keyring,
keyring, e.g. ('chrome', ) or ('vivaldi', 'default', 'BASICTEXT') and the container name, e.g. ('chrome', ) or
('vivaldi', 'default', 'BASICTEXT') or ('firefox', 'default', None, 'Meta')
legacyserverconnect: Explicitly allow HTTPS connection to servers that do not legacyserverconnect: Explicitly allow HTTPS connection to servers that do not
support RFC 5746 secure renegotiation support RFC 5746 secure renegotiation
nocheckcertificate: Do not verify SSL certificates nocheckcertificate: Do not verify SSL certificates

View file

@ -346,6 +346,7 @@ def parse_chapters(name, value):
# Cookies from browser # Cookies from browser
if opts.cookiesfrombrowser: if opts.cookiesfrombrowser:
container = None
mobj = re.match(r'(?P<name>[^+:]+)(\s*\+\s*(?P<keyring>[^:]+))?(\s*:(?P<profile>.+))?', opts.cookiesfrombrowser) mobj = re.match(r'(?P<name>[^+:]+)(\s*\+\s*(?P<keyring>[^:]+))?(\s*:(?P<profile>.+))?', opts.cookiesfrombrowser)
if mobj is None: if mobj is None:
raise ValueError(f'invalid cookies from browser arguments: {opts.cookiesfrombrowser}') raise ValueError(f'invalid cookies from browser arguments: {opts.cookiesfrombrowser}')
@ -354,12 +355,15 @@ def parse_chapters(name, value):
if browser_name not in SUPPORTED_BROWSERS: if browser_name not in SUPPORTED_BROWSERS:
raise ValueError(f'unsupported browser specified for cookies: "{browser_name}". ' raise ValueError(f'unsupported browser specified for cookies: "{browser_name}". '
f'Supported browsers are: {", ".join(sorted(SUPPORTED_BROWSERS))}') f'Supported browsers are: {", ".join(sorted(SUPPORTED_BROWSERS))}')
elif profile and browser_name == 'firefox':
if ':' in profile and not os.path.exists(profile):
profile, container = profile.split(':', 1)
if keyring is not None: if keyring is not None:
keyring = keyring.upper() keyring = keyring.upper()
if keyring not in SUPPORTED_KEYRINGS: if keyring not in SUPPORTED_KEYRINGS:
raise ValueError(f'unsupported keyring specified for cookies: "{keyring}". ' raise ValueError(f'unsupported keyring specified for cookies: "{keyring}". '
f'Supported keyrings are: {", ".join(sorted(SUPPORTED_KEYRINGS))}') f'Supported keyrings are: {", ".join(sorted(SUPPORTED_KEYRINGS))}')
opts.cookiesfrombrowser = (browser_name, profile, keyring) opts.cookiesfrombrowser = (browser_name, profile, keyring, container)
# MetadataParser # MetadataParser
def metadataparser_actions(f): def metadataparser_actions(f):

View file

@ -3,6 +3,7 @@
import http.cookiejar import http.cookiejar
import json import json
import os import os
import re
import shutil import shutil
import struct import struct
import subprocess import subprocess
@ -24,7 +25,7 @@
sqlite3, sqlite3,
) )
from .minicurses import MultilinePrinter, QuietMultilinePrinter from .minicurses import MultilinePrinter, QuietMultilinePrinter
from .utils import Popen, YoutubeDLCookieJar, error_to_str, expand_path from .utils import Popen, YoutubeDLCookieJar, error_to_str, expand_path, try_call
CHROMIUM_BASED_BROWSERS = {'brave', 'chrome', 'chromium', 'edge', 'opera', 'vivaldi'} CHROMIUM_BASED_BROWSERS = {'brave', 'chrome', 'chromium', 'edge', 'opera', 'vivaldi'}
SUPPORTED_BROWSERS = CHROMIUM_BASED_BROWSERS | {'firefox', 'safari'} SUPPORTED_BROWSERS = CHROMIUM_BASED_BROWSERS | {'firefox', 'safari'}
@ -85,8 +86,9 @@ def _create_progress_bar(logger):
def load_cookies(cookie_file, browser_specification, ydl): def load_cookies(cookie_file, browser_specification, ydl):
cookie_jars = [] cookie_jars = []
if browser_specification is not None: if browser_specification is not None:
browser_name, profile, keyring = _parse_browser_specification(*browser_specification) browser_name, profile, keyring, container = _parse_browser_specification(*browser_specification)
cookie_jars.append(extract_cookies_from_browser(browser_name, profile, YDLLogger(ydl), keyring=keyring)) cookie_jars.append(
extract_cookies_from_browser(browser_name, profile, YDLLogger(ydl), keyring=keyring, container=container))
if cookie_file is not None: if cookie_file is not None:
is_filename = YoutubeDLCookieJar.is_path(cookie_file) is_filename = YoutubeDLCookieJar.is_path(cookie_file)
@ -101,9 +103,9 @@ def load_cookies(cookie_file, browser_specification, ydl):
return _merge_cookie_jars(cookie_jars) return _merge_cookie_jars(cookie_jars)
def extract_cookies_from_browser(browser_name, profile=None, logger=YDLLogger(), *, keyring=None): def extract_cookies_from_browser(browser_name, profile=None, logger=YDLLogger(), *, keyring=None, container=None):
if browser_name == 'firefox': if browser_name == 'firefox':
return _extract_firefox_cookies(profile, logger) return _extract_firefox_cookies(profile, container, logger)
elif browser_name == 'safari': elif browser_name == 'safari':
return _extract_safari_cookies(profile, logger) return _extract_safari_cookies(profile, logger)
elif browser_name in CHROMIUM_BASED_BROWSERS: elif browser_name in CHROMIUM_BASED_BROWSERS:
@ -112,7 +114,7 @@ def extract_cookies_from_browser(browser_name, profile=None, logger=YDLLogger(),
raise ValueError(f'unknown browser: {browser_name}') raise ValueError(f'unknown browser: {browser_name}')
def _extract_firefox_cookies(profile, logger): def _extract_firefox_cookies(profile, container, logger):
logger.info('Extracting cookies from firefox') logger.info('Extracting cookies from firefox')
if not sqlite3: if not sqlite3:
logger.warning('Cannot extract cookies from firefox without sqlite3 support. ' logger.warning('Cannot extract cookies from firefox without sqlite3 support. '
@ -126,6 +128,20 @@ def _extract_firefox_cookies(profile, logger):
else: else:
search_root = os.path.join(_firefox_browser_dir(), profile) search_root = os.path.join(_firefox_browser_dir(), profile)
container_id = None
if container is not None:
containers_path = os.path.join(search_root, 'containers.json')
if not os.path.isfile(containers_path) or not os.access(containers_path, os.R_OK):
raise FileNotFoundError(f'could not read containers.json in {search_root}')
with open(containers_path, 'r') as containers:
identities = json.load(containers).get('identities', [])
container_id = next((context.get('userContextId') for context in identities if container in (
context.get('name'),
try_call(lambda: re.fullmatch(r'userContext([^\.]+)\.label', context['l10nID']).group())
)), None)
if not isinstance(container_id, int):
raise ValueError(f'could not find firefox container "{container}" in containers.json')
cookie_database_path = _find_most_recently_used_file(search_root, 'cookies.sqlite', logger) cookie_database_path = _find_most_recently_used_file(search_root, 'cookies.sqlite', logger)
if cookie_database_path is None: if cookie_database_path is None:
raise FileNotFoundError(f'could not find firefox cookies database in {search_root}') raise FileNotFoundError(f'could not find firefox cookies database in {search_root}')
@ -135,6 +151,17 @@ def _extract_firefox_cookies(profile, logger):
cursor = None cursor = None
try: try:
cursor = _open_database_copy(cookie_database_path, tmpdir) cursor = _open_database_copy(cookie_database_path, tmpdir)
origin_attributes = ''
if isinstance(container_id, int):
origin_attributes = f'^userContextId={container_id}'
logger.debug(
f'Only loading cookies from firefox container "{container}", ID {container_id}')
try:
cursor.execute(
'SELECT host, name, value, path, expiry, isSecure FROM moz_cookies WHERE originAttributes=?',
(origin_attributes, ))
except sqlite3.OperationalError:
logger.debug('Database exception, loading all cookies')
cursor.execute('SELECT host, name, value, path, expiry, isSecure FROM moz_cookies') cursor.execute('SELECT host, name, value, path, expiry, isSecure FROM moz_cookies')
jar = YoutubeDLCookieJar() jar = YoutubeDLCookieJar()
with _create_progress_bar(logger) as progress_bar: with _create_progress_bar(logger) as progress_bar:
@ -948,11 +975,11 @@ def _is_path(value):
return os.path.sep in value return os.path.sep in value
def _parse_browser_specification(browser_name, profile=None, keyring=None): def _parse_browser_specification(browser_name, profile=None, keyring=None, container=None):
if browser_name not in SUPPORTED_BROWSERS: if browser_name not in SUPPORTED_BROWSERS:
raise ValueError(f'unsupported browser: "{browser_name}"') raise ValueError(f'unsupported browser: "{browser_name}"')
if keyring not in (None, *SUPPORTED_KEYRINGS): if keyring not in (None, *SUPPORTED_KEYRINGS):
raise ValueError(f'unsupported keyring: "{keyring}"') raise ValueError(f'unsupported keyring: "{keyring}"')
if profile is not None and _is_path(profile): if profile is not None and _is_path(profile):
profile = os.path.expanduser(profile) profile = os.path.expanduser(profile)
return browser_name, profile, keyring return browser_name, profile, keyring, container

View file

@ -1400,12 +1400,12 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
help='Do not read/dump cookies from/to file (default)') help='Do not read/dump cookies from/to file (default)')
filesystem.add_option( filesystem.add_option(
'--cookies-from-browser', '--cookies-from-browser',
dest='cookiesfrombrowser', metavar='BROWSER[+KEYRING][:PROFILE]', dest='cookiesfrombrowser', metavar='BROWSER[+KEYRING][:PROFILE[:CONTAINER]]',
help=( help=(
'The name of the browser and (optionally) the name/path of ' 'The name of the browser and (optionally) the name/path of the profile to load cookies from '
'the profile to load cookies from, separated by a ":". ' '(and container name if Firefox) separated by a ":". '
f'Currently supported browsers are: {", ".join(sorted(SUPPORTED_BROWSERS))}. ' f'Currently supported browsers are: {", ".join(sorted(SUPPORTED_BROWSERS))}. '
'By default, the most recently accessed profile is used. ' 'By default, the default container of the most recently accessed profile is used. '
'The keyring used for decrypting Chromium cookies on Linux can be ' 'The keyring used for decrypting Chromium cookies on Linux can be '
'(optionally) specified after the browser name separated by a "+". ' '(optionally) specified after the browser name separated by a "+". '
f'Currently supported keyrings are: {", ".join(map(str.lower, sorted(SUPPORTED_KEYRINGS)))}')) f'Currently supported keyrings are: {", ".join(map(str.lower, sorted(SUPPORTED_KEYRINGS)))}'))