diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index c8e95a423..7403cde81 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -484,9 +484,11 @@ class ClientNetworkTest(unittest.TestCase): def test_check_response_not_ok_jobj_no_error(self): self.response.ok = False self.response.json.return_value = {} - # pylint: disable=protected-access - self.assertRaises( - errors.ClientError, self.net._check_response, self.response) + with mock.patch('acme.client.messages.Error.from_json') as from_json: + from_json.side_effect = jose.DeserializationError + # pylint: disable=protected-access + self.assertRaises( + errors.ClientError, self.net._check_response, self.response) def test_check_response_not_ok_jobj_error(self): self.response.ok = False diff --git a/acme/acme/messages.py b/acme/acme/messages.py index 24a3b580c..56bcb1de2 100644 --- a/acme/acme/messages.py +++ b/acme/acme/messages.py @@ -37,9 +37,9 @@ class Error(jose.JSONObjectWithFields, errors.Error): ) ) - typ = jose.Field('type') + typ = jose.Field('type', omitempty=True, default='about:blank') title = jose.Field('title', omitempty=True) - detail = jose.Field('detail') + detail = jose.Field('detail', omitempty=True) @property def description(self): @@ -143,12 +143,6 @@ class Directory(jose.JSONDeSerializable): def __init__(self, jobj): canon_jobj = util.map_keys(jobj, self._canon_key) - if not set(canon_jobj).issubset( - set(self._REGISTERED_TYPES).union(['meta'])): - # TODO: acme-spec is not clear about this: 'It is a JSON - # dictionary, whose keys are the "resource" values listed - # in {{https-requests}}' - raise ValueError('Wrong directory fields') # TODO: check that everything is an absolute URL; acme-spec is # not clear on that self._jobj = canon_jobj @@ -171,10 +165,7 @@ class Directory(jose.JSONDeSerializable): @classmethod def from_json(cls, jobj): jobj['meta'] = cls.Meta.from_json(jobj.pop('meta', {})) - try: - return cls(jobj) - except ValueError as error: - raise jose.DeserializationError(str(error)) + return cls(jobj) class Resource(jose.JSONObjectWithFields): diff --git a/acme/acme/messages_test.py b/acme/acme/messages_test.py index b2b7febdc..36d0dd618 100644 --- a/acme/acme/messages_test.py +++ b/acme/acme/messages_test.py @@ -28,6 +28,14 @@ class ErrorTest(unittest.TestCase): self.error_custom = Error(typ='custom', detail='bar') self.jobj_cusom = {'type': 'custom', 'detail': 'bar'} + def test_default_typ(self): + from acme.messages import Error + self.assertEqual(Error().typ, 'about:blank') + + def test_from_json_empty(self): + from acme.messages import Error + self.assertEqual(Error(), Error.from_json('{}')) + def test_from_json_hashable(self): from acme.messages import Error hash(Error.from_json(self.error.to_json())) @@ -97,9 +105,9 @@ class DirectoryTest(unittest.TestCase): ), }) - def test_init_wrong_key_value_error(self): + def test_init_wrong_key_value_success(self): # pylint: disable=no-self-use from acme.messages import Directory - self.assertRaises(ValueError, Directory, {'foo': 'bar'}) + Directory({'foo': 'bar'}) def test_getitem(self): self.assertEqual('reg', self.dir['new-reg']) @@ -127,10 +135,9 @@ class DirectoryTest(unittest.TestCase): }, }) - def test_from_json_deserialization_error_on_wrong_key(self): + def test_from_json_deserialization_unknown_key_success(self): # pylint: disable=no-self-use from acme.messages import Directory - self.assertRaises( - jose.DeserializationError, Directory.from_json, {'foo': 'bar'}) + Directory.from_json({'foo': 'bar'}) class RegistrationTest(unittest.TestCase): diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py index cef0756d7..8ab7adb0a 100644 --- a/certbot-nginx/certbot_nginx/nginxparser.py +++ b/certbot-nginx/certbot_nginx/nginxparser.py @@ -17,7 +17,7 @@ class RawNginxParser(object): right_bracket = Literal("}").suppress() semicolon = Literal(";").suppress() space = White().suppress() - key = Word(alphanums + "_/") + key = Word(alphanums + "_/+-.") # Matches anything that is not a special character AND any chars in single # or double quotes value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+") diff --git a/certbot/cli.py b/certbot/cli.py index 97b1a5399..90e86a751 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -37,8 +37,14 @@ helpful_parser = None # should only be used for purposes where inability to detect letsencrypt-auto # fails safely +LEAUTO = "letsencrypt-auto" +if "CERTBOT_AUTO" in os.environ: + # if we're here, this is probably going to be certbot-auto, unless the + # user saved the script under a different name + LEAUTO = os.path.basename(os.environ["CERTBOT_AUTO"]) + fragment = os.path.join(".local", "share", "letsencrypt") -cli_command = "letsencrypt-auto" if fragment in sys.argv[0] else "certbot" +cli_command = LEAUTO if fragment in sys.argv[0] else "certbot" # Argparse's help formatting has a lot of unhelpful peculiarities, so we want # to replace as much of it as we can... @@ -141,6 +147,22 @@ def usage_strings(plugins): return USAGE % (apache_doc, nginx_doc), SHORT_USAGE +def possible_deprecation_warning(config): + "A deprecation warning for users with the old, not-self-upgrading letsencrypt-auto." + if cli_command != LEAUTO: + return + if config.no_self_upgrade: + # users setting --no-self-upgrade might be hanging on a clent version like 0.3.0 + # or 0.5.0 which is the new script, but doesn't set CERTBOT_AUTO; they don't + # need warnings + return + if "CERTBOT_AUTO" not in os.environ: + logger.warn("You are running with an old copy of letsencrypt-auto that does " + "not receive updates, and is less reliable than more recent versions. " + "We recommend upgrading to the latest certbot-auto script, or using native " + "OS packages.") + + class _Default(object): """A class to use as a default to detect if a value is set by a user""" diff --git a/certbot/main.py b/certbot/main.py index 309889e8e..0405d6eb5 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -661,6 +661,7 @@ def main(cli_args=sys.argv[1:]): le_util.make_or_verify_dir( config.logs_dir, 0o700, os.geteuid(), "--strict-permissions" in cli_args) setup_logging(config, _cli_log_handler, logfile='letsencrypt.log') + cli.possible_deprecation_warning(config) logger.debug("certbot version: %s", certbot.__version__) # do not log `config`, as it contains sensitive data (e.g. revoke --key)! diff --git a/certbot/plugins/webroot.py b/certbot/plugins/webroot.py index fbe703f40..c344954ae 100644 --- a/certbot/plugins/webroot.py +++ b/certbot/plugins/webroot.py @@ -241,6 +241,9 @@ to serve all files under specified web root ({0}).""" elif exc.errno == errno.EACCES: logger.debug("Challenges cleaned up but no permissions for %s", root_path) + elif exc.errno == errno.ENOENT: + logger.debug("Challenges cleaned up, %s does not exists", + root_path) else: raise diff --git a/certbot/plugins/webroot_test.py b/certbot/plugins/webroot_test.py index 3f429ec34..ab2a9e9a4 100644 --- a/certbot/plugins/webroot_test.py +++ b/certbot/plugins/webroot_test.py @@ -218,13 +218,26 @@ class AuthenticatorTest(unittest.TestCase): self.auth.perform([self.achall]) os_error = OSError() - os_error.errno = errno.ENOENT + os_error.errno = errno.EPERM mock_rmdir.side_effect = os_error self.assertRaises(OSError, self.auth.cleanup, [self.achall]) self.assertFalse(os.path.exists(self.validation_path)) self.assertTrue(os.path.exists(self.root_challenge_path)) + @mock.patch('os.rmdir') + def test_cleanup_file_not_exists(self, mock_rmdir): + self.auth.prepare() + self.auth.perform([self.achall]) + + os_error = OSError() + os_error.errno = errno.ENOENT + mock_rmdir.side_effect = os_error + + self.auth.cleanup([self.achall]) + self.assertFalse(os.path.exists(self.validation_path)) + self.assertTrue(os.path.exists(self.root_challenge_path)) + class WebrootActionTest(unittest.TestCase): """Tests for webroot argparse actions.""" diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 111f2b272..8578feef2 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -1,6 +1,6 @@ #!/bin/sh # -# Download and run the latest release version of the Let's Encrypt client. +# Download and run the latest release version of the Certbot client. # # NOTE: THIS SCRIPT IS AUTO-GENERATED AND SELF-UPDATING # @@ -20,10 +20,35 @@ VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" LE_AUTO_VERSION="0.6.0.dev0" +BASENAME=$(basename $0) +USAGE="Usage: $BASENAME [OPTIONS] +A self-updating wrapper script for the Certbot ACME client. When run, updates +to both this script and certbot will be downloaded and installed. After +ensuring you have the latest versions installed, certbot will be invoked with +all arguments you have provided. + +Help for certbot itself cannot be provided until it is installed. + + --debug attempt experimental installation + -h, --help print this help + -n, --non-interactive, --noninteractive run without asking for user input + --no-self-upgrade do not download updates + --os-packages-only install OS dependencies and exit + -v, --verbose provide more output + +All arguments are accepted and forwarded to the Certbot client when run." + +while getopts ":hnv" arg; do + case $arg in + h) + HELP=1;; + n) + ASSUME_YES=1;; + v) + VERBOSE=1;; + esac +done -# This script takes the same arguments as the main letsencrypt program, but it -# additionally responds to --verbose (more output) and --debug (allow support -# for experimental platforms) for arg in "$@" ; do case "$arg" in --debug) @@ -34,25 +59,26 @@ for arg in "$@" ; do # Do not upgrade this script (also prevents client upgrades, because each # copy of the script pins a hash of the python client) NO_SELF_UPGRADE=1;; + --help) + HELP=1;; + --noninteractive|--non-interactive) + ASSUME_YES=1;; --verbose) VERBOSE=1;; - [!-]*|-*[!v]*|-) - # Anything that isn't -v, -vv, etc.: that is, anything that does not - # start with a -, contains anything that's not a v, or is just "-" - ;; - *) # -v+ remains. - VERBOSE=1;; esac done -# letsencrypt-auto needs root access to bootstrap OS dependencies, and -# letsencrypt itself needs root access for almost all modes of operation +# certbot-auto needs root access to bootstrap OS dependencies, and +# certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but # this script *can* be run as root (not recommended), or fall back to using # `su` +SUDO_ENV="" +export CERTBOT_AUTO="$0" if test "`id -u`" -ne "0" ; then if command -v sudo 1>/dev/null 2>&1; then SUDO=sudo + SUDO_ENV="CERTBOT_AUTO=$0" else echo \"sudo\" is not available, will use \"su\" for installation steps... # Because the parameters in `su -c` has to be a string, @@ -81,6 +107,12 @@ else SUDO= fi +if [ $BASENAME = "letsencrypt-auto" ]; then + # letsencrypt-auto does not respect --help or --yes for backwards compatibility + ASSUME_YES=1 + HELP=0 +fi + ExperimentalBootstrap() { # Arguments: Platform name, bootstrap function name if [ "$DEBUG" = 1 ]; then @@ -151,30 +183,45 @@ BootstrapDebCommon() { augeas_pkg="libaugeas0 augeas-lenses" AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2` + if [ "$ASSUME_YES" = 1 ]; then + YES_FLAG="-y" + fi + AddBackportRepo() { # ARGS: BACKPORT_NAME="$1" BACKPORT_SOURCELINE="$2" + echo "To use the Apache Certbot plugin, augeas needs to be installed from $BACKPORT_NAME." if ! grep -v -e ' *#' /etc/apt/sources.list | grep -q "$BACKPORT_NAME" ; then # This can theoretically error if sources.list.d is empty, but in that case we don't care. if ! grep -v -e ' *#' /etc/apt/sources.list.d/* 2>/dev/null | grep -q "$BACKPORT_NAME"; then - /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." - sleep 1s - /bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..." - sleep 1s - /bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..." - sleep 1s - if echo $BACKPORT_NAME | grep -q wheezy ; then - /bin/echo '(Backports are only installed if explicitly requested via "apt-get install -t wheezy-backports")' + if [ "$ASSUME_YES" = 1 ]; then + /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..." + sleep 1s + add_backports=1 + else + read -p "Would you like to enable the $BACKPORT_NAME repository [Y/n]? " response + case $response in + [yY][eE][sS]|[yY]|"") + add_backports=1;; + *) + add_backports=0;; + esac + fi + if [ "$add_backports" = 1 ]; then + $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" + $SUDO apt-get update fi - - $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" - $SUDO apt-get update fi fi - $SUDO apt-get install -y --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg - augeas_pkg= - + if [ "$add_backports" != 0 ]; then + $SUDO apt-get install $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg + augeas_pkg= + fi } @@ -186,12 +233,12 @@ BootstrapDebCommon() { AddBackportRepo precise-backports "deb http://archive.ubuntu.com/ubuntu precise-backports main restricted universe multiverse" else echo "No libaugeas0 version is available that's new enough to run the" - echo "Let's Encrypt apache plugin..." + echo "Certbot apache plugin..." fi # XXX add a case for ubuntu PPAs fi - $SUDO apt-get install -y --no-install-recommends \ + $SUDO apt-get install $YES_FLAG --no-install-recommends \ python \ python-dev \ $virtualenv \ @@ -212,9 +259,10 @@ BootstrapDebCommon() { BootstrapRpmCommon() { # Tested with: - # - Fedora 22, 23 (x64) + # - Fedora 20, 21, 22, 23 (x64) # - Centos 7 (x64: on DigitalOcean droplet) # - CentOS 7 Minimal install in a Hyper-V VM + # - CentOS 6 (EPEL must be installed manually) if type dnf 2>/dev/null then @@ -228,54 +276,62 @@ BootstrapRpmCommon() { exit 1 fi + pkgs=" + gcc + dialog + augeas-libs + openssl + openssl-devel + libffi-devel + redhat-rpm-config + ca-certificates + " + # Some distros and older versions of current distros use a "python27" # instead of "python" naming convention. Try both conventions. - if ! $SUDO $tool install -y \ - python \ - python-devel \ - python-virtualenv \ - python-tools \ - python-pip - then - if ! $SUDO $tool install -y \ - python27 \ - python27-devel \ - python27-virtualenv \ - python27-tools \ - python27-pip - then - echo "Could not install Python dependencies. Aborting bootstrap!" - exit 1 - fi + if $SUDO $tool list python >/dev/null 2>&1; then + pkgs="$pkgs + python + python-devel + python-virtualenv + python-tools + python-pip + " + else + pkgs="$pkgs + python27 + python27-devel + python27-virtualenv + python27-tools + python27-pip + " fi - if ! $SUDO $tool install -y \ - gcc \ - dialog \ - augeas-libs \ - openssl \ - openssl-devel \ - libffi-devel \ - redhat-rpm-config \ - ca-certificates - then - echo "Could not install additional dependencies. Aborting bootstrap!" - exit 1 - fi - - if $SUDO $tool list installed "httpd" >/dev/null 2>&1; then - if ! $SUDO $tool install -y mod_ssl - then - echo "Apache found, but mod_ssl could not be installed." - fi + pkgs="$pkgs + mod_ssl + " + fi + + if [ "$ASSUME_YES" = 1 ]; then + yes_flag="-y" + fi + + if ! $SUDO $tool install $yes_flag $pkgs; then + echo "Could not install OS dependencies. Aborting bootstrap!" + exit 1 fi } BootstrapSuseCommon() { # SLE12 don't have python-virtualenv - $SUDO zypper -nq in -l \ + if [ "$ASSUME_YES" = 1 ]; then + zypper_flags="-nq" + install_flags="-l" + fi + + $SUDO zypper $zypper_flags in $install_flags \ python \ python-devel \ python-virtualenv \ @@ -310,8 +366,12 @@ BootstrapArchCommon() { # pacman -T exits with 127 if there are missing dependencies missing=$($SUDO pacman -T $deps) || true + if [ "$ASSUME_YES" = 1 ]; then + noconfirm="--noconfirm" + fi + if [ "$missing" ]; then - $SUDO pacman -S --needed $missing + $SUDO pacman -S --needed $missing $noconfirm fi } @@ -426,7 +486,7 @@ Bootstrap() { elif grep -iq "Amazon Linux" /etc/issue ; then ExperimentalBootstrap "Amazon Linux" BootstrapRpmCommon else - echo "Sorry, I don't know how to bootstrap Let's Encrypt on your operating system!" + echo "Sorry, I don't know how to bootstrap Certbot on your operating system!" echo echo "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" @@ -446,7 +506,8 @@ if [ "$1" = "--le-auto-phase2" ]; then shift 1 # the --le-auto-phase2 arg if [ -f "$VENV_BIN/letsencrypt" ]; then # --version output ran through grep due to python-cryptography DeprecationWarnings - INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep ^letsencrypt | cut -d " " -f 2) + # grep for both certbot and letsencrypt until certbot and shim packages have been released + INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep "^certbot\|^letsencrypt" | cut -d " " -f 2) else INSTALLED_VERSION="none" fi @@ -465,8 +526,8 @@ if [ "$1" = "--le-auto-phase2" ]; then # There is no $ interpolation due to quotes on starting heredoc delimiter. # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" -# This is the flattened list of packages letsencrypt-auto installs. To generate -# this, do `pip install --no-cache-dir -e acme -e . -e letsencrypt-apache`, and +# This is the flattened list of packages certbot-auto installs. To generate +# this, do `pip install --no-cache-dir -e acme -e . -e certbot-apache`, and # then use `hashin` or a more secure method to gather the hashes. argparse==1.4.0 \ @@ -823,18 +884,30 @@ UNLIKELY_EOF fi echo "Installation succeeded." fi - echo "Requesting root privileges to run letsencrypt..." - echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" - $SUDO "$VENV_BIN/letsencrypt" "$@" + echo "Requesting root privileges to run certbot..." + if [ -z "$SUDO_ENV" ] ; then + # SUDO is su wrapper / noop + echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" + $SUDO "$VENV_BIN/letsencrypt" "$@" + else + # sudo + echo " " $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" + $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" + fi + else - # Phase 1: Upgrade letsencrypt-auto if neceesary, then self-invoke. + # Phase 1: Upgrade certbot-auto if neceesary, then self-invoke. # # Each phase checks the version of only the thing it is responsible for # upgrading. Phase 1 checks the version of the latest release of - # letsencrypt-auto (which is always the same as that of the letsencrypt - # package). Phase 2 checks the version of the locally installed letsencrypt. + # certbot-auto (which is always the same as that of the certbot + # package). Phase 2 checks the version of the locally installed certbot. if [ ! -f "$VENV_BIN/letsencrypt" ]; then + if [ "$HELP" = 1 ]; then + echo "$USAGE" + exit 0 + fi # If it looks like we've never bootstrapped before, bootstrap: Bootstrap fi @@ -953,7 +1026,7 @@ def verified_new_le_auto(get, tag, temp_dir): stderr=dev_null) except CalledProcessError as exc: raise ExpectedError("Couldn't verify signature of downloaded " - "letsencrypt-auto.", exc) + "certbot-auto.", exc) def main(): @@ -978,29 +1051,27 @@ if __name__ == '__main__': UNLIKELY_EOF # --------------------------------------------------------------------------- DeterminePythonVersion - REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` - if [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then - echo "Upgrading letsencrypt-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." + if ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then + echo "WARNING: unable to check for updates." + elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then + echo "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." # Now we drop into Python so we don't have to install even more # dependencies (curl, etc.), for better flow control, and for the option of # future Windows compatibility. "$LE_PYTHON" "$TEMP_DIR/fetch.py" --le-auto-script "v$REMOTE_VERSION" - # Install new copy of letsencrypt-auto. + # Install new copy of certbot-auto. # TODO: Deal with quotes in pathnames. - echo "Replacing letsencrypt-auto..." + echo "Replacing certbot-auto..." # Clone permissions with cp. chmod and chown don't have a --reference # option on OS X or BSD, and stat -c on Linux is stat -f on OS X and BSD: - echo " " $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" - echo " " $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone" $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone" # Using mv rather than cp leaves the old file descriptor pointing to the # original copy so the shell can continue to read it unmolested. mv across # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the # cp is unlikely to fail (esp. under sudo) if the rm doesn't. - echo " " $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" # TODO: Clean up temp dir safely, even if it has quotes in its path. rm -rf "$TEMP_DIR" diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 2c8e1ec4c..33b140bca 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -20,10 +20,35 @@ VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN="$VENV_PATH/bin" LE_AUTO_VERSION="{{ LE_AUTO_VERSION }}" +BASENAME=$(basename $0) +USAGE="Usage: $BASENAME [OPTIONS] +A self-updating wrapper script for the Certbot ACME client. When run, updates +to both this script and certbot will be downloaded and installed. After +ensuring you have the latest versions installed, certbot will be invoked with +all arguments you have provided. + +Help for certbot itself cannot be provided until it is installed. + + --debug attempt experimental installation + -h, --help print this help + -n, --non-interactive, --noninteractive run without asking for user input + --no-self-upgrade do not download updates + --os-packages-only install OS dependencies and exit + -v, --verbose provide more output + +All arguments are accepted and forwarded to the Certbot client when run." + +while getopts ":hnv" arg; do + case $arg in + h) + HELP=1;; + n) + ASSUME_YES=1;; + v) + VERBOSE=1;; + esac +done -# This script takes the same arguments as the main letsencrypt program, but it -# additionally responds to --verbose (more output) and --debug (allow support -# for experimental platforms) for arg in "$@" ; do case "$arg" in --debug) @@ -34,25 +59,26 @@ for arg in "$@" ; do # Do not upgrade this script (also prevents client upgrades, because each # copy of the script pins a hash of the python client) NO_SELF_UPGRADE=1;; + --help) + HELP=1;; + --noninteractive|--non-interactive) + ASSUME_YES=1;; --verbose) VERBOSE=1;; - [!-]*|-*[!v]*|-) - # Anything that isn't -v, -vv, etc.: that is, anything that does not - # start with a -, contains anything that's not a v, or is just "-" - ;; - *) # -v+ remains. - VERBOSE=1;; esac done -# letsencrypt-auto needs root access to bootstrap OS dependencies, and +# certbot-auto needs root access to bootstrap OS dependencies, and # certbot itself needs root access for almost all modes of operation # The "normal" case is that sudo is used for the steps that need root, but # this script *can* be run as root (not recommended), or fall back to using # `su` +SUDO_ENV="" +export CERTBOT_AUTO="$0" if test "`id -u`" -ne "0" ; then if command -v sudo 1>/dev/null 2>&1; then SUDO=sudo + SUDO_ENV="CERTBOT_AUTO=$0" else echo \"sudo\" is not available, will use \"su\" for installation steps... # Because the parameters in `su -c` has to be a string, @@ -81,6 +107,12 @@ else SUDO= fi +if [ $BASENAME = "letsencrypt-auto" ]; then + # letsencrypt-auto does not respect --help or --yes for backwards compatibility + ASSUME_YES=1 + HELP=0 +fi + ExperimentalBootstrap() { # Arguments: Platform name, bootstrap function name if [ "$DEBUG" = 1 ]; then @@ -177,7 +209,8 @@ if [ "$1" = "--le-auto-phase2" ]; then shift 1 # the --le-auto-phase2 arg if [ -f "$VENV_BIN/letsencrypt" ]; then # --version output ran through grep due to python-cryptography DeprecationWarnings - INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep ^letsencrypt | cut -d " " -f 2) + # grep for both certbot and letsencrypt until certbot and shim packages have been released + INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | grep "^certbot\|^letsencrypt" | cut -d " " -f 2) else INSTALLED_VERSION="none" fi @@ -220,17 +253,29 @@ UNLIKELY_EOF echo "Installation succeeded." fi echo "Requesting root privileges to run certbot..." - echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" - $SUDO "$VENV_BIN/letsencrypt" "$@" + if [ -z "$SUDO_ENV" ] ; then + # SUDO is su wrapper / noop + echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" + $SUDO "$VENV_BIN/letsencrypt" "$@" + else + # sudo + echo " " $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" + $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" + fi + else - # Phase 1: Upgrade letsencrypt-auto if neceesary, then self-invoke. + # Phase 1: Upgrade certbot-auto if neceesary, then self-invoke. # # Each phase checks the version of only the thing it is responsible for # upgrading. Phase 1 checks the version of the latest release of - # letsencrypt-auto (which is always the same as that of the certbot + # certbot-auto (which is always the same as that of the certbot # package). Phase 2 checks the version of the locally installed certbot. if [ ! -f "$VENV_BIN/letsencrypt" ]; then + if [ "$HELP" = 1 ]; then + echo "$USAGE" + exit 0 + fi # If it looks like we've never bootstrapped before, bootstrap: Bootstrap fi @@ -248,29 +293,27 @@ else UNLIKELY_EOF # --------------------------------------------------------------------------- DeterminePythonVersion - REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` - if [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then - echo "Upgrading letsencrypt-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." + if ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then + echo "WARNING: unable to check for updates." + elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then + echo "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." # Now we drop into Python so we don't have to install even more # dependencies (curl, etc.), for better flow control, and for the option of # future Windows compatibility. "$LE_PYTHON" "$TEMP_DIR/fetch.py" --le-auto-script "v$REMOTE_VERSION" - # Install new copy of letsencrypt-auto. + # Install new copy of certbot-auto. # TODO: Deal with quotes in pathnames. - echo "Replacing letsencrypt-auto..." + echo "Replacing certbot-auto..." # Clone permissions with cp. chmod and chown don't have a --reference # option on OS X or BSD, and stat -c on Linux is stat -f on OS X and BSD: - echo " " $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" - echo " " $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone" $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone" # Using mv rather than cp leaves the old file descriptor pointing to the # original copy so the shell can continue to read it unmolested. mv across # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the # cp is unlikely to fail (esp. under sudo) if the rm doesn't. - echo " " $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" # TODO: Clean up temp dir safely, even if it has quotes in its path. rm -rf "$TEMP_DIR" diff --git a/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh index b2fc01a14..39e2da5fe 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh @@ -21,7 +21,11 @@ BootstrapArchCommon() { # pacman -T exits with 127 if there are missing dependencies missing=$($SUDO pacman -T $deps) || true + if [ "$ASSUME_YES" = 1 ]; then + noconfirm="--noconfirm" + fi + if [ "$missing" ]; then - $SUDO pacman -S --needed $missing + $SUDO pacman -S --needed $missing $noconfirm fi } diff --git a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh index 57ed11399..bfbcfa31d 100644 --- a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh @@ -34,30 +34,45 @@ BootstrapDebCommon() { augeas_pkg="libaugeas0 augeas-lenses" AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2` + if [ "$ASSUME_YES" = 1 ]; then + YES_FLAG="-y" + fi + AddBackportRepo() { # ARGS: BACKPORT_NAME="$1" BACKPORT_SOURCELINE="$2" + echo "To use the Apache Certbot plugin, augeas needs to be installed from $BACKPORT_NAME." if ! grep -v -e ' *#' /etc/apt/sources.list | grep -q "$BACKPORT_NAME" ; then # This can theoretically error if sources.list.d is empty, but in that case we don't care. if ! grep -v -e ' *#' /etc/apt/sources.list.d/* 2>/dev/null | grep -q "$BACKPORT_NAME"; then - /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." - sleep 1s - /bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..." - sleep 1s - /bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..." - sleep 1s - if echo $BACKPORT_NAME | grep -q wheezy ; then - /bin/echo '(Backports are only installed if explicitly requested via "apt-get install -t wheezy-backports")' + if [ "$ASSUME_YES" = 1 ]; then + /bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..." + sleep 1s + /bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..." + sleep 1s + /bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..." + sleep 1s + add_backports=1 + else + read -p "Would you like to enable the $BACKPORT_NAME repository [Y/n]? " response + case $response in + [yY][eE][sS]|[yY]|"") + add_backports=1;; + *) + add_backports=0;; + esac + fi + if [ "$add_backports" = 1 ]; then + $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" + $SUDO apt-get update fi - - $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" - $SUDO apt-get update fi fi - $SUDO apt-get install -y --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg - augeas_pkg= - + if [ "$add_backports" != 0 ]; then + $SUDO apt-get install $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg + augeas_pkg= + fi } @@ -74,7 +89,7 @@ BootstrapDebCommon() { # XXX add a case for ubuntu PPAs fi - $SUDO apt-get install -y --no-install-recommends \ + $SUDO apt-get install $YES_FLAG --no-install-recommends \ python \ python-dev \ $virtualenv \ diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh index 68a11a531..0f98b4bbc 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh @@ -1,8 +1,9 @@ BootstrapRpmCommon() { # Tested with: - # - Fedora 22, 23 (x64) + # - Fedora 20, 21, 22, 23 (x64) # - Centos 7 (x64: on DigitalOcean droplet) # - CentOS 7 Minimal install in a Hyper-V VM + # - CentOS 6 (EPEL must be installed manually) if type dnf 2>/dev/null then @@ -16,46 +17,49 @@ BootstrapRpmCommon() { exit 1 fi + pkgs=" + gcc + dialog + augeas-libs + openssl + openssl-devel + libffi-devel + redhat-rpm-config + ca-certificates + " + # Some distros and older versions of current distros use a "python27" # instead of "python" naming convention. Try both conventions. - if ! $SUDO $tool install -y \ - python \ - python-devel \ - python-virtualenv \ - python-tools \ - python-pip - then - if ! $SUDO $tool install -y \ - python27 \ - python27-devel \ - python27-virtualenv \ - python27-tools \ - python27-pip - then - echo "Could not install Python dependencies. Aborting bootstrap!" - exit 1 - fi + if $SUDO $tool list python >/dev/null 2>&1; then + pkgs="$pkgs + python + python-devel + python-virtualenv + python-tools + python-pip + " + else + pkgs="$pkgs + python27 + python27-devel + python27-virtualenv + python27-tools + python27-pip + " fi - if ! $SUDO $tool install -y \ - gcc \ - dialog \ - augeas-libs \ - openssl \ - openssl-devel \ - libffi-devel \ - redhat-rpm-config \ - ca-certificates - then - echo "Could not install additional dependencies. Aborting bootstrap!" - exit 1 - fi - - if $SUDO $tool list installed "httpd" >/dev/null 2>&1; then - if ! $SUDO $tool install -y mod_ssl - then - echo "Apache found, but mod_ssl could not be installed." - fi + pkgs="$pkgs + mod_ssl + " + fi + + if [ "$ASSUME_YES" = 1 ]; then + yes_flag="-y" + fi + + if ! $SUDO $tool install $yes_flag $pkgs; then + echo "Could not install OS dependencies. Aborting bootstrap!" + exit 1 fi } diff --git a/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh index 46c60f992..9ac295922 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh @@ -1,7 +1,12 @@ BootstrapSuseCommon() { # SLE12 don't have python-virtualenv - $SUDO zypper -nq in -l \ + if [ "$ASSUME_YES" = 1 ]; then + zypper_flags="-nq" + install_flags="-l" + fi + + $SUDO zypper $zypper_flags in $install_flags \ python \ python-devel \ python-virtualenv \ diff --git a/letsencrypt-auto-source/pieces/fetch.py b/letsencrypt-auto-source/pieces/fetch.py index 39ff7777c..38f4aa255 100644 --- a/letsencrypt-auto-source/pieces/fetch.py +++ b/letsencrypt-auto-source/pieces/fetch.py @@ -103,7 +103,7 @@ def verified_new_le_auto(get, tag, temp_dir): stderr=dev_null) except CalledProcessError as exc: raise ExpectedError("Couldn't verify signature of downloaded " - "letsencrypt-auto.", exc) + "certbot-auto.", exc) def main(): diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index 27cfb3d43..3a1ce34f1 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -1,4 +1,4 @@ -# This is the flattened list of packages letsencrypt-auto installs. To generate +# This is the flattened list of packages certbot-auto installs. To generate # this, do `pip install --no-cache-dir -e acme -e . -e certbot-apache`, and # then use `hashin` or a more secure method to gather the hashes. diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index edb5f0c04..3b7e8731b 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -231,7 +231,7 @@ class AutoTests(TestCase): * The OpenSSL sig mismatches. For tests which get to the end, we run merely ``letsencrypt --version``. - The functioning of the rest of the letsencrypt script is covered by other + The functioning of the rest of the certbot script is covered by other test suites. """ @@ -277,7 +277,7 @@ class AutoTests(TestCase): ok_(re.match(r'letsencrypt \d+\.\d+\.\d+', err.strip().splitlines()[-1])) # Make a few assertions to test the validity of the next tests: - self.assertIn('Upgrading letsencrypt-auto ', out) + self.assertIn('Upgrading certbot-auto ', out) self.assertIn('Creating virtual environment...', out) # Now we have le-auto 99.9.9 and LE 99.9.9 installed. This @@ -286,14 +286,14 @@ class AutoTests(TestCase): # Test when neither phase-1 upgrade nor phase-2 upgrade is # needed (probably a common case): out, err = run_letsencrypt_auto() - self.assertNotIn('Upgrading letsencrypt-auto ', out) + self.assertNotIn('Upgrading certbot-auto ', out) self.assertNotIn('Creating virtual environment...', out) # Test when a phase-1 upgrade is not needed but a phase-2 # upgrade is: set_le_script_version(venv_dir, '0.0.1') out, err = run_letsencrypt_auto() - self.assertNotIn('Upgrading letsencrypt-auto ', out) + self.assertNotIn('Upgrading certbot-auto ', out) self.assertIn('Creating virtual environment...', out) def test_openssl_failure(self): @@ -312,10 +312,10 @@ class AutoTests(TestCase): except CalledProcessError as exc: eq_(exc.returncode, 1) self.assertIn("Couldn't verify signature of downloaded " - "letsencrypt-auto.", + "certbot-auto.", exc.output) else: - self.fail('Signature check on letsencrypt-auto erroneously passed.') + self.fail('Signature check on certbot-auto erroneously passed.') def test_pip_failure(self): """Make sure pip stops us if there is a hash mismatch."""