Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cookies from browser #488

Merged
merged 28 commits into from
Jul 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ba6801d
cookies from browser
mbway Jul 11, 2021
6145a22
added to README
mbway Jul 11, 2021
a1e3517
detect cookiesfrombrowser as providing authentication
mbway Jul 11, 2021
8cec23a
de-duplicate supported browsers list
mbway Jul 11, 2021
f25f241
add profile to --help text
mbway Jul 11, 2021
c9b114d
sort browser list in --help text
mbway Jul 11, 2021
d938d95
updated logging functionality
mbway Jul 11, 2021
8a27255
using utility function for subprocess communication
mbway Jul 11, 2021
5a8dc3f
updated github workflow
mbway Jul 11, 2021
1e03d36
removed support for pre-python3.4 pbkdf2
mbway Jul 11, 2021
09c3371
moved class to fix compat ordering
mbway Jul 11, 2021
1f3b507
changed import style
mbway Jul 12, 2021
39e08c5
removed reference to deleted variable
mbway Jul 12, 2021
980eff2
change to make linter happy
mbway Jul 12, 2021
115c77c
Prettify outputs
pukkandan Jul 20, 2021
d2b1980
Add `--no-cookies-from-browser`
pukkandan Jul 20, 2021
ce41641
bugfix
pukkandan Jul 20, 2021
e97e0fc
Delegate argument parsing to `yt_dlp` and document API
pukkandan Jul 20, 2021
64b5765
Remove unncessary `Logger`
pukkandan Jul 20, 2021
127100a
[test] Use assert... methods
pukkandan Jul 21, 2021
a7f9840
removed pre-python3.3 compatibility layer
mbway Jul 21, 2021
0f3802d
Merge branch 'master' into cookies_from_browser
mbway Jul 21, 2021
774b02e
using logging to report warnings rather than warnings library
mbway Jul 21, 2021
9714f59
handle case where Mac password is not found
mbway Jul 21, 2021
eb2d431
use UTC for timestamps
mbway Jul 21, 2021
3433229
added message about keyring backends
mbway Jul 21, 2021
2ede4a2
[doc] make Readme
pukkandan Jul 21, 2021
13d30c8
[doc] minor change
pukkandan Jul 21, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/quick-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install nose
run: pip install nose
- name: Install test requirements
run: pip install nose pycryptodome
- name: Run tests
env:
YTDL_TEST_SET: core
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ The major new features from the latest release of [blackjack4494/yt-dlc](https:/

* **Self-updater**: The releases can be updated using `yt-dlp -U`

* **Cookies from browser**: Cookies can be automatically extracted from all major web browsers using `--cookies-from-browser <browser_name>`


See [changelog](Changelog.md) or [commits](https://github.com/yt-dlp/yt-dlp/commits) for the full list of changes

Expand Down Expand Up @@ -185,6 +187,7 @@ While all the other dependancies are optional, `ffmpeg` and `ffprobe` are highly
* [**mutagen**](https://github.com/quodlibet/mutagen) - For embedding thumbnail in certain formats. Licenced under [GPLv2+](https://github.com/quodlibet/mutagen/blob/master/COPYING)
* [**pycryptodome**](https://github.com/Legrandin/pycryptodome) - For decrypting various data. Licenced under [BSD2](https://github.com/Legrandin/pycryptodome/blob/master/LICENSE.rst)
* [**websockets**](https://github.com/aaugustin/websockets) - For downloading over websocket. Licenced under [BSD3](https://github.com/aaugustin/websockets/blob/main/LICENSE)
* [**keyring**](https://github.com/jaraco/keyring) - For decrypting cookies of chromium-based browsers on Linux. Licenced under [MIT](https://github.com/jaraco/keyring/blob/main/LICENSE)
* [**AtomicParsley**](https://github.com/wez/atomicparsley) - For embedding thumbnail in mp4/m4a if mutagen is not present. Licenced under [GPLv2+](https://github.com/wez/atomicparsley/blob/master/COPYING)
* [**rtmpdump**](http://rtmpdump.mplayerhq.hu) - For downloading `rtmp` streams. ffmpeg will be used as a fallback. Licenced under [GPLv2+](http://rtmpdump.mplayerhq.hu)
* [**mplayer**](http://mplayerhq.hu/design7/info.html) or [**mpv**](https://mpv.io) - For downloading `rstp` streams. ffmpeg will be used as a fallback. Licenced under [GPLv2+](https://github.com/mpv-player/mpv/blob/master/Copyright)
Expand Down Expand Up @@ -520,7 +523,19 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
option)
--cookies FILE File to read cookies from and dump cookie
jar in
--no-cookies Do not read/dump cookies (default)
--no-cookies Do not read/dump cookies from/to file
(default)
--cookies-from-browser BROWSER[:PROFILE]
Load cookies from a user profile of the
given web browser. Currently supported
browsers are: brave|chrome|chromium|edge|fi
refox|opera|safari|vivaldi. You can specify
the user profile name or directory using
"BROWSER:PROFILE_NAME" or
"BROWSER:PROFILE_PATH". If no profile is
given, the most recently accessed one is
used
--no-cookies-from-browser Do not load cookies from browser (default)
--cache-dir DIR Location in the filesystem where youtube-dl
can store some downloaded information (such
as client ids and signatures) permanently.
Expand Down
96 changes: 96 additions & 0 deletions test/test_cookies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import unittest
from datetime import datetime, timezone

from yt_dlp import cookies
from yt_dlp.cookies import (
CRYPTO_AVAILABLE,
LinuxChromeCookieDecryptor,
MacChromeCookieDecryptor,
WindowsChromeCookieDecryptor,
YDLLogger,
parse_safari_cookies,
pbkdf2_sha1,
)


class MonkeyPatch:
def __init__(self, module, temporary_values):
self._module = module
self._temporary_values = temporary_values
self._backup_values = {}

def __enter__(self):
for name, temp_value in self._temporary_values.items():
self._backup_values[name] = getattr(self._module, name)
setattr(self._module, name, temp_value)

def __exit__(self, exc_type, exc_val, exc_tb):
for name, backup_value in self._backup_values.items():
setattr(self._module, name, backup_value)


class TestCookies(unittest.TestCase):
pukkandan marked this conversation as resolved.
Show resolved Hide resolved
def test_chrome_cookie_decryptor_linux_derive_key(self):
key = LinuxChromeCookieDecryptor.derive_key(b'abc')
self.assertEqual(key, b'7\xa1\xec\xd4m\xfcA\xc7\xb19Z\xd0\x19\xdcM\x17')

def test_chrome_cookie_decryptor_mac_derive_key(self):
key = MacChromeCookieDecryptor.derive_key(b'abc')
self.assertEqual(key, b'Y\xe2\xc0\xd0P\xf6\xf4\xe1l\xc1\x8cQ\xcb|\xcdY')

def test_chrome_cookie_decryptor_linux_v10(self):
with MonkeyPatch(cookies, {'_get_linux_keyring_password': lambda *args, **kwargs: b''}):
encrypted_value = b'v10\xccW%\xcd\xe6\xe6\x9fM" \xa7\xb0\xca\xe4\x07\xd6'
value = 'USD'
decryptor = LinuxChromeCookieDecryptor('Chrome', YDLLogger())
self.assertEqual(decryptor.decrypt(encrypted_value), value)

def test_chrome_cookie_decryptor_linux_v11(self):
with MonkeyPatch(cookies, {'_get_linux_keyring_password': lambda *args, **kwargs: b'',
'KEYRING_AVAILABLE': True}):
encrypted_value = b'v11#\x81\x10>`w\x8f)\xc0\xb2\xc1\r\xf4\x1al\xdd\x93\xfd\xf8\xf8N\xf2\xa9\x83\xf1\xe9o\x0elVQd'
value = 'tz=Europe.London'
decryptor = LinuxChromeCookieDecryptor('Chrome', YDLLogger())
self.assertEqual(decryptor.decrypt(encrypted_value), value)

@unittest.skipIf(not CRYPTO_AVAILABLE, 'cryptography library not available')
def test_chrome_cookie_decryptor_windows_v10(self):
with MonkeyPatch(cookies, {
'_get_windows_v10_key': lambda *args, **kwargs: b'Y\xef\xad\xad\xeerp\xf0Y\xe6\x9b\x12\xc2<z\x16]\n\xbb\xb8\xcb\xd7\x9bA\xc3\x14e\x99{\xd6\xf4&'
}):
encrypted_value = b'v10T\xb8\xf3\xb8\x01\xa7TtcV\xfc\x88\xb8\xb8\xef\x05\xb5\xfd\x18\xc90\x009\xab\xb1\x893\x85)\x87\xe1\xa9-\xa3\xad='
value = '32101439'
decryptor = WindowsChromeCookieDecryptor('', YDLLogger())
self.assertEqual(decryptor.decrypt(encrypted_value), value)

def test_chrome_cookie_decryptor_mac_v10(self):
with MonkeyPatch(cookies, {'_get_mac_keyring_password': lambda *args, **kwargs: b'6eIDUdtKAacvlHwBVwvg/Q=='}):
encrypted_value = b'v10\xb3\xbe\xad\xa1[\x9fC\xa1\x98\xe0\x9a\x01\xd9\xcf\xbfc'
value = '2021-06-01-22'
decryptor = MacChromeCookieDecryptor('', YDLLogger())
self.assertEqual(decryptor.decrypt(encrypted_value), value)

def test_safari_cookie_parsing(self):
cookies = \
b'cook\x00\x00\x00\x01\x00\x00\x00i\x00\x00\x01\x00\x01\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00Y' \
b'\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x008\x00\x00\x00B\x00\x00\x00F\x00\x00\x00H' \
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x03\xa5>\xc3A\x00\x00\x80\xc3\x07:\xc3A' \
b'localhost\x00foo\x00/\x00test%20%3Bcookie\x00\x00\x00\x054\x07\x17 \x05\x00\x00\x00Kbplist00\xd1\x01' \
b'\x02_\x10\x18NSHTTPCookieAcceptPolicy\x10\x02\x08\x0b&\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00' \
b'\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00('

jar = parse_safari_cookies(cookies)
self.assertEqual(len(jar), 1)
cookie = list(jar)[0]
self.assertEqual(cookie.domain, 'localhost')
self.assertEqual(cookie.port, None)
self.assertEqual(cookie.path, '/')
self.assertEqual(cookie.name, 'foo')
self.assertEqual(cookie.value, 'test%20%3Bcookie')
self.assertFalse(cookie.secure)
expected_expiration = datetime(2021, 6, 18, 21, 39, 19, tzinfo=timezone.utc)
self.assertEqual(cookie.expires, int(expected_expiration.timestamp()))

def test_pbkdf2_sha1(self):
key = pbkdf2_sha1(b'peanuts', b' ' * 16, 1, 16)
self.assertEqual(key, b'g\xe1\x8e\x0fQ\x1c\x9b\xf3\xc9`!\xaa\x90\xd9\xd34')
15 changes: 6 additions & 9 deletions yt_dlp/YoutubeDL.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@

from .compat import (
compat_basestring,
compat_cookiejar,
compat_get_terminal_size,
compat_kwargs,
compat_numeric_types,
Expand All @@ -42,6 +41,7 @@
compat_urllib_request,
compat_urllib_request_DataHandler,
)
from .cookies import load_cookies
from .utils import (
age_restricted,
args_to_str,
Expand Down Expand Up @@ -110,7 +110,6 @@
version_tuple,
write_json_file,
write_string,
YoutubeDLCookieJar,
YoutubeDLCookieProcessor,
YoutubeDLHandler,
YoutubeDLRedirectHandler,
Expand Down Expand Up @@ -290,6 +289,9 @@ class YoutubeDL(object):
break_on_reject: Stop the download process when encountering a video that
has been filtered out.
cookiefile: File name where cookies should be read from and dumped to
cookiesfrombrowser: A tuple containing the name of the browser and the profile
name/path from where cookies are loaded.
Eg: ('chrome', ) or (vivaldi, 'default')
nocheckcertificate:Do not verify SSL certificates
prefer_insecure: Use HTTP instead of HTTPS to retrieve information.
At the moment, this is only supported by YouTube.
Expand Down Expand Up @@ -3211,16 +3213,11 @@ def _setup_opener(self):
timeout_val = self.params.get('socket_timeout')
self._socket_timeout = 600 if timeout_val is None else float(timeout_val)

opts_cookiesfrombrowser = self.params.get('cookiesfrombrowser')
opts_cookiefile = self.params.get('cookiefile')
opts_proxy = self.params.get('proxy')

if opts_cookiefile is None:
self.cookiejar = compat_cookiejar.CookieJar()
else:
opts_cookiefile = expand_path(opts_cookiefile)
self.cookiejar = YoutubeDLCookieJar(opts_cookiefile)
if os.access(opts_cookiefile, os.R_OK):
self.cookiejar.load(ignore_discard=True, ignore_expires=True)
self.cookiejar = load_cookies(opts_cookiefile, opts_cookiesfrombrowser, self)

cookie_processor = YoutubeDLCookieProcessor(self.cookiejar)
if opts_proxy is not None:
Expand Down
8 changes: 8 additions & 0 deletions yt_dlp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
compat_getpass,
workaround_optparse_bug9161,
)
from .cookies import SUPPORTED_BROWSERS
from .utils import (
DateRange,
decodeOption,
Expand Down Expand Up @@ -242,6 +243,12 @@ def parse_retries(retries, name=''):
if opts.convertthumbnails not in FFmpegThumbnailsConvertorPP.SUPPORTED_EXTS:
parser.error('invalid thumbnail format specified')

if opts.cookiesfrombrowser is not None:
opts.cookiesfrombrowser = [
part.strip() or None for part in opts.cookiesfrombrowser.split(':', 1)]
if opts.cookiesfrombrowser[0] not in SUPPORTED_BROWSERS:
parser.error('unsupported browser specified for cookies')

if opts.date is not None:
date = DateRange.day(opts.date)
else:
Expand Down Expand Up @@ -628,6 +635,7 @@ def report_args_compat(arg, name):
'break_on_reject': opts.break_on_reject,
'skip_playlist_after_errors': opts.skip_playlist_after_errors,
'cookiefile': opts.cookiefile,
'cookiesfrombrowser': opts.cookiesfrombrowser,
'nocheckcertificate': opts.no_check_certificate,
'prefer_insecure': opts.prefer_insecure,
'proxy': opts.proxy,
Expand Down