Skip to content

Commit

Permalink
Add option --netrc-cmd (#6682)
Browse files Browse the repository at this point in the history
Authored by: NDagestad, pukkandan
Closes #1706
  • Loading branch information
NDagestad committed Jun 21, 2023
1 parent af7585c commit db3ad8a
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 28 deletions.
15 changes: 13 additions & 2 deletions README.md
Expand Up @@ -49,7 +49,7 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
* [Extractor Options](#extractor-options)
* [CONFIGURATION](#configuration)
* [Configuration file encoding](#configuration-file-encoding)
* [Authentication with .netrc file](#authentication-with-netrc-file)
* [Authentication with netrc](#authentication-with-netrc)
* [Notes about environment variables](#notes-about-environment-variables)
* [OUTPUT TEMPLATE](#output-template)
* [Output template examples](#output-template-examples)
Expand Down Expand Up @@ -910,6 +910,8 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
--netrc-location PATH Location of .netrc authentication data;
either the path or its containing directory.
Defaults to ~/.netrc
--netrc-cmd NETRC_CMD Command to execute to get the credentials
credentials for an extractor.
--video-password PASSWORD Video password (vimeo, youku)
--ap-mso MSO Adobe Pass multiple-system operator (TV
provider) identifier, use --ap-list-mso for
Expand Down Expand Up @@ -1203,7 +1205,7 @@ The configuration files are decoded according to the UTF BOM if present, and in

If you want your file to be decoded differently, add `# coding: ENCODING` to the beginning of the file (e.g. `# coding: shift-jis`). There must be no characters before that, even spaces or BOM.

### Authentication with `.netrc` file
### Authentication with netrc

You may also want to configure automatic credentials storage for extractors that support authentication (by providing login and password with `--username` and `--password`) in order not to pass credentials as command line arguments on every yt-dlp execution and prevent tracking plain text passwords in the shell command history. You can achieve this using a [`.netrc` file](https://stackoverflow.com/tags/.netrc/info) on a per-extractor basis. For that you will need to create a `.netrc` file in `--netrc-location` and restrict permissions to read/write by only you:
```
Expand All @@ -1223,6 +1225,15 @@ To activate authentication with the `.netrc` file you should pass `--netrc` to y

The default location of the .netrc file is `~` (see below).

As an alternative to using the `.netrc` file, which has the disadvantage of keeping your passwords in a plain text file, you can configure a custom shell command to provide the credentials for an extractor. This is done by providing the `--netrc-cmd` parameter, it shall output the credentials in the netrc format and return `0` on success, other values will be treated as an error. `{}` in the command will be replaced by the name of the extractor to make it possible to select the credentials for the right extractor.
To use braces in the command, they need to be escaped by doubling them. (see example bellow)

E.g. To use an encrypted `.netrc` file stored as `.authinfo.gpg`
```
yt-dlp --netrc-cmd 'gpg --decrypt ~/.authinfo.gpg' https://www.youtube.com/watch?v=BaW_jenozKc
```


### Notes about environment variables
* Environment variables are normally specified as `${VARIABLE}`/`$VARIABLE` on UNIX and `%VARIABLE%` on Windows; but is always shown as `${VARIABLE}` in this documentation
* yt-dlp also allow using UNIX-style variables on Windows for path-like options; e.g. `--output`, `--config-location`
Expand Down
1 change: 1 addition & 0 deletions yt_dlp/YoutubeDL.py
Expand Up @@ -190,6 +190,7 @@ class YoutubeDL:
ap_password: Multiple-system operator account password.
usenetrc: Use netrc for authentication instead.
netrc_location: Location of the netrc file. Defaults to ~/.netrc.
netrc_cmd: Use a shell command to get credentials
verbose: Print additional info to stdout.
quiet: Do not print messages to stdout.
no_warnings: Do not print out anything for warnings.
Expand Down
5 changes: 3 additions & 2 deletions yt_dlp/__init__.py
Expand Up @@ -188,8 +188,8 @@ def validate_minmax(min_val, max_val, min_name, max_name=None):
raise ValueError(f'{max_name} "{max_val}" must be must be greater than or equal to {min_name} "{min_val}"')

# Usernames and passwords
validate(not opts.usenetrc or (opts.username is None and opts.password is None),
'.netrc', msg='using {name} conflicts with giving username/password')
validate(sum(map(bool, (opts.usenetrc, opts.netrc_cmd, opts.username))) <= 1, '.netrc',
msg='{name}, netrc command and username/password are mutually exclusive options')
validate(opts.password is None or opts.username is not None, 'account username', msg='{name} missing')
validate(opts.ap_password is None or opts.ap_username is not None,
'TV Provider account username', msg='{name} missing')
Expand Down Expand Up @@ -741,6 +741,7 @@ def parse_options(argv=None):
return ParsedOptions(parser, opts, urls, {
'usenetrc': opts.usenetrc,
'netrc_location': opts.netrc_location,
'netrc_cmd': opts.netrc_cmd,
'username': opts.username,
'password': opts.password,
'twofactor': opts.twofactor,
Expand Down
53 changes: 29 additions & 24 deletions yt_dlp/extractor/common.py
Expand Up @@ -13,6 +13,7 @@
import os
import random
import re
import subprocess
import sys
import time
import types
Expand All @@ -34,6 +35,7 @@
GeoUtils,
HEADRequest,
LenientJSONDecoder,
Popen,
RegexNotFoundError,
RetryManager,
UnsupportedError,
Expand Down Expand Up @@ -70,6 +72,7 @@
smuggle_url,
str_or_none,
str_to_int,
netrc_from_content,
strip_or_none,
traverse_obj,
truncate_string,
Expand Down Expand Up @@ -535,7 +538,7 @@ class InfoExtractor:
_EMBED_REGEX = []

def _login_hint(self, method=NO_DEFAULT, netrc=None):
password_hint = f'--username and --password, or --netrc ({netrc or self._NETRC_MACHINE}) to provide account credentials'
password_hint = f'--username and --password, --netrc-cmd, or --netrc ({netrc or self._NETRC_MACHINE}) to provide account credentials'
return {
None: '',
'any': f'Use --cookies, --cookies-from-browser, {password_hint}',
Expand Down Expand Up @@ -1291,45 +1294,47 @@ def _html_search_regex(self, pattern, string, name, default=NO_DEFAULT, fatal=Tr
return clean_html(res)

def _get_netrc_login_info(self, netrc_machine=None):
username = None
password = None
netrc_machine = netrc_machine or self._NETRC_MACHINE

if self.get_param('usenetrc', False):
try:
netrc_file = compat_expanduser(self.get_param('netrc_location') or '~')
if os.path.isdir(netrc_file):
netrc_file = os.path.join(netrc_file, '.netrc')
info = netrc.netrc(file=netrc_file).authenticators(netrc_machine)
if info is not None:
username = info[0]
password = info[2]
else:
raise netrc.NetrcParseError(
'No authenticators for %s' % netrc_machine)
except (OSError, netrc.NetrcParseError) as err:
self.report_warning(
'parsing .netrc: %s' % error_to_compat_str(err))
cmd = self.get_param('netrc_cmd', '').format(netrc_machine)
if cmd:
self.to_screen(f'Executing command: {cmd}')
stdout, _, ret = Popen.run(cmd, text=True, shell=True, stdout=subprocess.PIPE)
if ret != 0:
raise OSError(f'Command returned error code {ret}')
info = netrc_from_content(stdout).authenticators(netrc_machine)

return username, password
elif self.get_param('usenetrc', False):
netrc_file = compat_expanduser(self.get_param('netrc_location') or '~')
if os.path.isdir(netrc_file):
netrc_file = os.path.join(netrc_file, '.netrc')
info = netrc.netrc(netrc_file).authenticators(netrc_machine)

else:
return None, None
if not info:
raise netrc.NetrcParseError(f'No authenticators for {netrc_machine}')
return info[0], info[2]

def _get_login_info(self, username_option='username', password_option='password', netrc_machine=None):
"""
Get the login info as (username, password)
First look for the manually specified credentials using username_option
and password_option as keys in params dictionary. If no such credentials
available look in the netrc file using the netrc_machine or _NETRC_MACHINE
value.
are available try the netrc_cmd if it is defined or look in the
netrc file using the netrc_machine or _NETRC_MACHINE value.
If there's no info available, return (None, None)
"""

# Attempt to use provided username and password or .netrc data
username = self.get_param(username_option)
if username is not None:
password = self.get_param(password_option)
else:
username, password = self._get_netrc_login_info(netrc_machine)

try:
username, password = self._get_netrc_login_info(netrc_machine)
except (OSError, netrc.NetrcParseError) as err:
self.report_warning(f'Failed to parse .netrc: {err}')
return None, None
return username, password

def _get_tfa_info(self, note='two-factor verification code'):
Expand Down
4 changes: 4 additions & 0 deletions yt_dlp/options.py
Expand Up @@ -720,6 +720,10 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
'--netrc-location',
dest='netrc_location', metavar='PATH',
help='Location of .netrc authentication data; either the path or its containing directory. Defaults to ~/.netrc')
authentication.add_option(
'--netrc-cmd',
dest='netrc_cmd', metavar='NETRC_CMD',
help='Command to execute to get the credentials for an extractor.')
authentication.add_option(
'--video-password',
dest='videopassword', metavar='PASSWORD',
Expand Down
8 changes: 8 additions & 0 deletions yt_dlp/utils/_utils.py
Expand Up @@ -25,6 +25,7 @@
import locale
import math
import mimetypes
import netrc
import operator
import os
import platform
Expand Down Expand Up @@ -864,6 +865,13 @@ def escapeHTML(text):
)


class netrc_from_content(netrc.netrc):
def __init__(self, content):
self.hosts, self.macros = {}, {}
with io.StringIO(content) as stream:
self._parse('-', stream, False)


def process_communicate_or_kill(p, *args, **kwargs):
deprecation_warning(f'"{__name__}.process_communicate_or_kill" is deprecated and may be removed '
f'in a future version. Use "{__name__}.Popen.communicate_or_kill" instead')
Expand Down

0 comments on commit db3ad8a

Please sign in to comment.