From 56db211367d171aed5ee9aee1eccd87041e14de8 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 21 Aug 2017 12:23:09 -0700 Subject: [PATCH] Change certbot-auto's installation path to /opt (#4970) * Update comment about root usage. * run all of certbot-auto as root * remove other $SUDO uses from template * remove $SUDO usage from bootstrappers * default venv path = /opt/eff.org/certbot/venv * Create symlinks from old default venvs * Delete old venv path when it exists. Also, quote expansion of paths. * fix typo * Separate venv_dir and le_auto_path * Deduplicate code with test_dirs() * Ignore cleanup errors. This is caused by subdirectories being owned by root. * Split test into test_phase2_upgrade. * Rename test_dirs to temp_paths for clarity. * Check both venvs before bootstrapping again. * Use OLD_VENV_PATH/bin * Preserve environment with sudo. * Remove "esp. under sudo" comment. * Export *VENV_PATH. * Change check for OLD_VENV installation. This approach better handles manually set VENV_PATH values. * Remove SUDO_ENV. * Print message before requesting root privileges. * Make a function for selecting root auth method. * Address @erikrose's feedback. --- letsencrypt-auto-source/letsencrypt-auto | 179 ++++++++++-------- .../letsencrypt-auto.template | 129 +++++++------ .../pieces/bootstrappers/arch_common.sh | 6 +- .../pieces/bootstrappers/deb_common.sh | 10 +- .../pieces/bootstrappers/free_bsd.sh | 2 +- .../pieces/bootstrappers/gentoo_common.sh | 6 +- .../pieces/bootstrappers/mac.sh | 6 +- .../pieces/bootstrappers/mageia_common.sh | 4 +- .../pieces/bootstrappers/rpm_common.sh | 14 +- .../pieces/bootstrappers/suse_common.sh | 2 +- letsencrypt-auto-source/tests/auto_test.py | 79 ++++---- 11 files changed, 241 insertions(+), 196 deletions(-) diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 39f8728e8..8ce3342be 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -23,9 +23,11 @@ fi if [ -z "$XDG_DATA_HOME" ]; then XDG_DATA_HOME=~/.local/share fi -VENV_NAME="letsencrypt" if [ -z "$VENV_PATH" ]; then - VENV_PATH="$XDG_DATA_HOME/$VENV_NAME" + # We export these values so they are preserved properly if this script is + # rerun with sudo/su where $HOME/$XDG_DATA_HOME may have a different value. + export OLD_VENV_PATH="$XDG_DATA_HOME/letsencrypt" + export VENV_PATH="/opt/eff.org/certbot/venv" fi VENV_BIN="$VENV_PATH/bin" LE_AUTO_VERSION="0.18.0.dev0" @@ -49,6 +51,7 @@ Help for certbot itself cannot be provided until it is installed. implies --non-interactive All arguments are accepted and forwarded to the Certbot client when run." +export CERTBOT_AUTO="$0" for arg in "$@" ; do case "$arg" in @@ -119,16 +122,18 @@ else exit 1 fi -# 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`. Auto-detection can be overridden by explicitly setting the -# environment variable LE_AUTO_SUDO to 'sudo', 'sudo_su' or '' as used below. +# Certbot itself needs root access for almost all modes of operation. +# certbot-auto needs root access to bootstrap OS dependencies and install +# Certbot at a protected path so it can be safely run as root. To accomplish +# this, this script will attempt to run itself as root if it doesn't have the +# necessary privileges by using `sudo` or falling back to `su` if it is not +# available. The mechanism used to obtain root access can be set explicitly by +# setting the environment variable LE_AUTO_SUDO to 'sudo', 'su', 'su_sudo', +# 'SuSudo', or '' as used below. # Because the parameters in `su -c` has to be a string, # we need to properly escape it. -su_sudo() { +SuSudo() { args="" # This `while` loop iterates over all parameters given to this function. # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string @@ -147,34 +152,47 @@ su_sudo() { su root -c "$args" } -SUDO_ENV="" -export CERTBOT_AUTO="$0" -if [ -n "${LE_AUTO_SUDO+x}" ]; then - case "$LE_AUTO_SUDO" in - su_sudo|su) - SUDO=su_sudo - ;; - sudo) - SUDO=sudo - SUDO_ENV="CERTBOT_AUTO=$0" - ;; - '') ;; # Nothing to do for plain root method. - *) - error "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'." - exit 1 - esac - say "Using preset root authorization mechanism '$LE_AUTO_SUDO'." -else - if test "`id -u`" -ne "0" ; then - if $EXISTS sudo 1>/dev/null 2>&1; then - SUDO=sudo - SUDO_ENV="CERTBOT_AUTO=$0" - else - say \"sudo\" is not available, will use \"su\" for installation steps... - SUDO=su_sudo - fi +# Sets the environment variable SUDO to be the name of the program or function +# to call to get root access. If this script already has root privleges, SUDO +# is set to an empty string. The value in SUDO should be run with the command +# to called with root privileges as arguments. +SetRootAuthMechanism() { + SUDO="" + if [ -n "${LE_AUTO_SUDO+x}" ]; then + case "$LE_AUTO_SUDO" in + SuSudo|su_sudo|su) + SUDO=SuSudo + ;; + sudo) + SUDO="sudo -E" + ;; + '') ;; # Nothing to do for plain root method. + *) + error "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'." + exit 1 + esac + say "Using preset root authorization mechanism '$LE_AUTO_SUDO'." else - SUDO= + if test "`id -u`" -ne "0" ; then + if $EXISTS sudo 1>/dev/null 2>&1; then + SUDO="sudo -E" + else + say \"sudo\" is not available, will use \"su\" for installation steps... + SUDO=SuSudo + fi + fi + fi +} + +if [ "$1" = "--cb-auto-has-root" ]; then + shift 1 +elif [ "$1" != "--le-auto-phase2" ]; then + # if $1 is --le-auto-phase2, we've executed this branch before + SetRootAuthMechanism + if [ -n "$SUDO" ]; then + echo "Requesting to rerun $0 with root privileges..." + $SUDO "$0" --cb-auto-has-root "$@" + exit 0 fi fi @@ -261,7 +279,7 @@ BootstrapDebCommon() { QUIET_FLAG='-qq' fi - $SUDO apt-get $QUIET_FLAG update || error apt-get update hit problems but continuing anyway... + apt-get $QUIET_FLAG update || error apt-get update hit problems but continuing anyway... # virtualenv binary can be found in different packages depending on # distro version (#346) @@ -311,13 +329,13 @@ BootstrapDebCommon() { esac fi if [ "$add_backports" = 1 ]; then - $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" - $SUDO apt-get $QUIET_FLAG update + sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" + apt-get $QUIET_FLAG update fi fi fi if [ "$add_backports" != 0 ]; then - $SUDO apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg + apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg augeas_pkg= fi } @@ -336,7 +354,7 @@ BootstrapDebCommon() { # XXX add a case for ubuntu PPAs fi - $SUDO apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends \ + apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends \ python \ python-dev \ $virtualenv \ @@ -380,9 +398,9 @@ BootstrapRpmCommon() { QUIET_FLAG='--quiet' fi - if ! $SUDO $tool list *virtualenv >/dev/null 2>&1; then + if ! $tool list *virtualenv >/dev/null 2>&1; then echo "To use Certbot, packages from the EPEL repository need to be installed." - if ! $SUDO $tool list epel-release >/dev/null 2>&1; then + if ! $tool list epel-release >/dev/null 2>&1; then error "Enable the EPEL repository and try running Certbot again." exit 1 fi @@ -394,7 +412,7 @@ BootstrapRpmCommon() { /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." sleep 1s fi - if ! $SUDO $tool install $yes_flag $QUIET_FLAG epel-release; then + if ! $tool install $yes_flag $QUIET_FLAG epel-release; then error "Could not enable EPEL. Aborting bootstrap!" exit 1 fi @@ -411,7 +429,7 @@ BootstrapRpmCommon() { " # Most RPM distros use the "python" or "python-" naming convention. Let's try that first. - if $SUDO $tool list python >/dev/null 2>&1; then + if $tool list python >/dev/null 2>&1; then pkgs="$pkgs python python-devel @@ -421,7 +439,7 @@ BootstrapRpmCommon() { " # Fedora 26 starts to use the prefix python2 for python2 based packages. # this elseif is theoretically for any Fedora over version 26: - elif $SUDO $tool list python2 >/dev/null 2>&1; then + elif $tool list python2 >/dev/null 2>&1; then pkgs="$pkgs python2 python2-libs @@ -443,13 +461,13 @@ BootstrapRpmCommon() { " fi - if $SUDO $tool list installed "httpd" >/dev/null 2>&1; then + if $tool list installed "httpd" >/dev/null 2>&1; then pkgs="$pkgs mod_ssl " fi - if ! $SUDO $tool install $yes_flag $QUIET_FLAG $pkgs; then + if ! $tool install $yes_flag $QUIET_FLAG $pkgs; then error "Could not install OS dependencies. Aborting bootstrap!" exit 1 fi @@ -467,7 +485,7 @@ BootstrapSuseCommon() { QUIET_FLAG='-qq' fi - $SUDO zypper $QUIET_FLAG $zypper_flags in $install_flags \ + zypper $QUIET_FLAG $zypper_flags in $install_flags \ python \ python-devel \ python-virtualenv \ @@ -498,7 +516,7 @@ BootstrapArchCommon() { " # pacman -T exits with 127 if there are missing dependencies - missing=$($SUDO pacman -T $deps) || true + missing=$(pacman -T $deps) || true if [ "$ASSUME_YES" = 1 ]; then noconfirm="--noconfirm" @@ -506,9 +524,9 @@ BootstrapArchCommon() { if [ "$missing" ]; then if [ "$QUIET" = 1 ]; then - $SUDO pacman -S --needed $missing $noconfirm > /dev/null + pacman -S --needed $missing $noconfirm > /dev/null else - $SUDO pacman -S --needed $missing $noconfirm + pacman -S --needed $missing $noconfirm fi fi } @@ -530,13 +548,13 @@ BootstrapGentooCommon() { case "$PACKAGE_MANAGER" in (paludis) - $SUDO cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x + cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x ;; (pkgcore) - $SUDO pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES + pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES ;; (portage|*) - $SUDO emerge --noreplace --oneshot $ASK_OPTION $PACKAGES + emerge --noreplace --oneshot $ASK_OPTION $PACKAGES ;; esac } @@ -546,7 +564,7 @@ BootstrapFreeBsd() { QUIET_FLAG="--quiet" fi - $SUDO pkg install -Ay $QUIET_FLAG \ + pkg install -Ay $QUIET_FLAG \ python \ py27-virtualenv \ augeas \ @@ -561,7 +579,7 @@ BootstrapMac() { elif hash port 2>/dev/null; then say "Using MacPorts to install dependencies..." pkgman=port - pkgcmd="$SUDO port install" + pkgcmd="port install" else say "No Homebrew/MacPorts; installing Homebrew..." ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" @@ -581,8 +599,8 @@ BootstrapMac() { # Workaround for _dlopen not finding augeas on macOS if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then say "Applying augeas workaround" - $SUDO mkdir -p /usr/local/lib/ - $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/ + mkdir -p /usr/local/lib/ + ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/ fi if ! hash pip 2>/dev/null; then @@ -608,7 +626,7 @@ BootstrapMageiaCommon() { QUIET_FLAG='--quiet' fi - if ! $SUDO urpmi --force $QUIET_FLAG \ + if ! urpmi --force $QUIET_FLAG \ python \ libpython-devel \ python-virtualenv @@ -617,7 +635,7 @@ BootstrapMageiaCommon() { exit 1 fi - if ! $SUDO urpmi --force $QUIET_FLAG \ + if ! urpmi --force $QUIET_FLAG \ git \ gcc \ python-augeas \ @@ -1144,20 +1162,15 @@ UNLIKELY_EOF rm -rf "$VENV_PATH" exit 1 fi + + if [ -d "$OLD_VENV_PATH" -a ! -L "$OLD_VENV_PATH" ]; then + rm -rf "$OLD_VENV_PATH" + ln -s "$VENV_PATH" "$OLD_VENV_PATH" + fi + say "Installation succeeded." fi - if [ -n "$SUDO" ]; then - # SUDO is su wrapper or sudo - say "Requesting root privileges to run certbot..." - say " $VENV_BIN/letsencrypt" "$@" - fi - if [ -z "$SUDO_ENV" ] ; then - # SUDO is su wrapper / noop - $SUDO "$VENV_BIN/letsencrypt" "$@" - else - # sudo - $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" - fi + "$VENV_BIN/letsencrypt" "$@" else # Phase 1: Upgrade certbot-auto if necessary, then self-invoke. @@ -1168,12 +1181,14 @@ else # 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 + if [ -z "$OLD_VENV_PATH" -o ! -f "$OLD_VENV_PATH/bin/letsencrypt" ]; then + if [ "$HELP" = 1 ]; then + echo "$USAGE" + exit 0 + fi + # If it looks like we've never bootstrapped before, bootstrap: + Bootstrap fi - # If it looks like we've never bootstrapped before, bootstrap: - Bootstrap fi if [ "$OS_PACKAGES_ONLY" = 1 ]; then say "OS packages installed." @@ -1333,13 +1348,13 @@ UNLIKELY_EOF say "Replacing certbot-auto..." # Clone permissions with cp. chmod and chown don't have a --reference # option on macOS or BSD, and stat -c on Linux is stat -f on macOS and BSD: - $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" - $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone" + cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" + 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. - $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" + # cp is unlikely to fail if the rm doesn't. + mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" fi # A newer version is available. fi # Self-upgrading is allowed. diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index 284241a82..29aaf1291 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -23,9 +23,11 @@ fi if [ -z "$XDG_DATA_HOME" ]; then XDG_DATA_HOME=~/.local/share fi -VENV_NAME="letsencrypt" if [ -z "$VENV_PATH" ]; then - VENV_PATH="$XDG_DATA_HOME/$VENV_NAME" + # We export these values so they are preserved properly if this script is + # rerun with sudo/su where $HOME/$XDG_DATA_HOME may have a different value. + export OLD_VENV_PATH="$XDG_DATA_HOME/letsencrypt" + export VENV_PATH="/opt/eff.org/certbot/venv" fi VENV_BIN="$VENV_PATH/bin" LE_AUTO_VERSION="{{ LE_AUTO_VERSION }}" @@ -49,6 +51,7 @@ Help for certbot itself cannot be provided until it is installed. implies --non-interactive All arguments are accepted and forwarded to the Certbot client when run." +export CERTBOT_AUTO="$0" for arg in "$@" ; do case "$arg" in @@ -119,16 +122,18 @@ else exit 1 fi -# 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`. Auto-detection can be overridden by explicitly setting the -# environment variable LE_AUTO_SUDO to 'sudo', 'sudo_su' or '' as used below. +# Certbot itself needs root access for almost all modes of operation. +# certbot-auto needs root access to bootstrap OS dependencies and install +# Certbot at a protected path so it can be safely run as root. To accomplish +# this, this script will attempt to run itself as root if it doesn't have the +# necessary privileges by using `sudo` or falling back to `su` if it is not +# available. The mechanism used to obtain root access can be set explicitly by +# setting the environment variable LE_AUTO_SUDO to 'sudo', 'su', 'su_sudo', +# 'SuSudo', or '' as used below. # Because the parameters in `su -c` has to be a string, # we need to properly escape it. -su_sudo() { +SuSudo() { args="" # This `while` loop iterates over all parameters given to this function. # For each parameter, all `'` will be replace by `'"'"'`, and the escaped string @@ -147,34 +152,47 @@ su_sudo() { su root -c "$args" } -SUDO_ENV="" -export CERTBOT_AUTO="$0" -if [ -n "${LE_AUTO_SUDO+x}" ]; then - case "$LE_AUTO_SUDO" in - su_sudo|su) - SUDO=su_sudo - ;; - sudo) - SUDO=sudo - SUDO_ENV="CERTBOT_AUTO=$0" - ;; - '') ;; # Nothing to do for plain root method. - *) - error "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'." - exit 1 - esac - say "Using preset root authorization mechanism '$LE_AUTO_SUDO'." -else - if test "`id -u`" -ne "0" ; then - if $EXISTS sudo 1>/dev/null 2>&1; then - SUDO=sudo - SUDO_ENV="CERTBOT_AUTO=$0" - else - say \"sudo\" is not available, will use \"su\" for installation steps... - SUDO=su_sudo - fi +# Sets the environment variable SUDO to be the name of the program or function +# to call to get root access. If this script already has root privleges, SUDO +# is set to an empty string. The value in SUDO should be run with the command +# to called with root privileges as arguments. +SetRootAuthMechanism() { + SUDO="" + if [ -n "${LE_AUTO_SUDO+x}" ]; then + case "$LE_AUTO_SUDO" in + SuSudo|su_sudo|su) + SUDO=SuSudo + ;; + sudo) + SUDO="sudo -E" + ;; + '') ;; # Nothing to do for plain root method. + *) + error "Error: unknown root authorization mechanism '$LE_AUTO_SUDO'." + exit 1 + esac + say "Using preset root authorization mechanism '$LE_AUTO_SUDO'." else - SUDO= + if test "`id -u`" -ne "0" ; then + if $EXISTS sudo 1>/dev/null 2>&1; then + SUDO="sudo -E" + else + say \"sudo\" is not available, will use \"su\" for installation steps... + SUDO=SuSudo + fi + fi + fi +} + +if [ "$1" = "--cb-auto-has-root" ]; then + shift 1 +elif [ "$1" != "--le-auto-phase2" ]; then + # if $1 is --le-auto-phase2, we've executed this branch before + SetRootAuthMechanism + if [ -n "$SUDO" ]; then + echo "Requesting to rerun $0 with root privileges..." + $SUDO "$0" --cb-auto-has-root "$@" + exit 0 fi fi @@ -385,20 +403,15 @@ UNLIKELY_EOF rm -rf "$VENV_PATH" exit 1 fi + + if [ -d "$OLD_VENV_PATH" -a ! -L "$OLD_VENV_PATH" ]; then + rm -rf "$OLD_VENV_PATH" + ln -s "$VENV_PATH" "$OLD_VENV_PATH" + fi + say "Installation succeeded." fi - if [ -n "$SUDO" ]; then - # SUDO is su wrapper or sudo - say "Requesting root privileges to run certbot..." - say " $VENV_BIN/letsencrypt" "$@" - fi - if [ -z "$SUDO_ENV" ] ; then - # SUDO is su wrapper / noop - $SUDO "$VENV_BIN/letsencrypt" "$@" - else - # sudo - $SUDO "$SUDO_ENV" "$VENV_BIN/letsencrypt" "$@" - fi + "$VENV_BIN/letsencrypt" "$@" else # Phase 1: Upgrade certbot-auto if necessary, then self-invoke. @@ -409,12 +422,14 @@ else # 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 + if [ -z "$OLD_VENV_PATH" -o ! -f "$OLD_VENV_PATH/bin/letsencrypt" ]; then + if [ "$HELP" = 1 ]; then + echo "$USAGE" + exit 0 + fi + # If it looks like we've never bootstrapped before, bootstrap: + Bootstrap fi - # If it looks like we've never bootstrapped before, bootstrap: - Bootstrap fi if [ "$OS_PACKAGES_ONLY" = 1 ]; then say "OS packages installed." @@ -445,13 +460,13 @@ UNLIKELY_EOF say "Replacing certbot-auto..." # Clone permissions with cp. chmod and chown don't have a --reference # option on macOS or BSD, and stat -c on Linux is stat -f on macOS and BSD: - $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" - $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone" + cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" + 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. - $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" + # cp is unlikely to fail if the rm doesn't. + mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" fi # A newer version is available. fi # Self-upgrading is allowed. diff --git a/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh index e9d91fe70..3983bc1d8 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/arch_common.sh @@ -18,7 +18,7 @@ BootstrapArchCommon() { " # pacman -T exits with 127 if there are missing dependencies - missing=$($SUDO pacman -T $deps) || true + missing=$(pacman -T $deps) || true if [ "$ASSUME_YES" = 1 ]; then noconfirm="--noconfirm" @@ -26,9 +26,9 @@ BootstrapArchCommon() { if [ "$missing" ]; then if [ "$QUIET" = 1 ]; then - $SUDO pacman -S --needed $missing $noconfirm > /dev/null + pacman -S --needed $missing $noconfirm > /dev/null else - $SUDO pacman -S --needed $missing $noconfirm + pacman -S --needed $missing $noconfirm fi fi } diff --git a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh index afd279ac2..12aa80d63 100644 --- a/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh @@ -21,7 +21,7 @@ BootstrapDebCommon() { QUIET_FLAG='-qq' fi - $SUDO apt-get $QUIET_FLAG update || error apt-get update hit problems but continuing anyway... + apt-get $QUIET_FLAG update || error apt-get update hit problems but continuing anyway... # virtualenv binary can be found in different packages depending on # distro version (#346) @@ -71,13 +71,13 @@ BootstrapDebCommon() { esac fi if [ "$add_backports" = 1 ]; then - $SUDO sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" - $SUDO apt-get $QUIET_FLAG update + sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" + apt-get $QUIET_FLAG update fi fi fi if [ "$add_backports" != 0 ]; then - $SUDO apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg + apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg augeas_pkg= fi } @@ -96,7 +96,7 @@ BootstrapDebCommon() { # XXX add a case for ubuntu PPAs fi - $SUDO apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends \ + apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends \ python \ python-dev \ $virtualenv \ diff --git a/letsencrypt-auto-source/pieces/bootstrappers/free_bsd.sh b/letsencrypt-auto-source/pieces/bootstrappers/free_bsd.sh index f1bc00f3b..cfbd2b8b1 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/free_bsd.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/free_bsd.sh @@ -3,7 +3,7 @@ BootstrapFreeBsd() { QUIET_FLAG="--quiet" fi - $SUDO pkg install -Ay $QUIET_FLAG \ + pkg install -Ay $QUIET_FLAG \ python \ py27-virtualenv \ augeas \ diff --git a/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh index 86a1ec7d6..46543bab9 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/gentoo_common.sh @@ -15,13 +15,13 @@ BootstrapGentooCommon() { case "$PACKAGE_MANAGER" in (paludis) - $SUDO cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x + cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x ;; (pkgcore) - $SUDO pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES + pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES ;; (portage|*) - $SUDO emerge --noreplace --oneshot $ASK_OPTION $PACKAGES + emerge --noreplace --oneshot $ASK_OPTION $PACKAGES ;; esac } diff --git a/letsencrypt-auto-source/pieces/bootstrappers/mac.sh b/letsencrypt-auto-source/pieces/bootstrappers/mac.sh index b88e96999..b9f347f67 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/mac.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/mac.sh @@ -6,7 +6,7 @@ BootstrapMac() { elif hash port 2>/dev/null; then say "Using MacPorts to install dependencies..." pkgman=port - pkgcmd="$SUDO port install" + pkgcmd="port install" else say "No Homebrew/MacPorts; installing Homebrew..." ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" @@ -26,8 +26,8 @@ BootstrapMac() { # Workaround for _dlopen not finding augeas on macOS if [ "$pkgman" = "port" ] && ! [ -e "/usr/local/lib/libaugeas.dylib" ] && [ -e "/opt/local/lib/libaugeas.dylib" ]; then say "Applying augeas workaround" - $SUDO mkdir -p /usr/local/lib/ - $SUDO ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/ + mkdir -p /usr/local/lib/ + ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/ fi if ! hash pip 2>/dev/null; then diff --git a/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh index 1c76bbcac..c9a540ce1 100644 --- a/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/mageia_common.sh @@ -3,7 +3,7 @@ BootstrapMageiaCommon() { QUIET_FLAG='--quiet' fi - if ! $SUDO urpmi --force $QUIET_FLAG \ + if ! urpmi --force $QUIET_FLAG \ python \ libpython-devel \ python-virtualenv @@ -12,7 +12,7 @@ BootstrapMageiaCommon() { exit 1 fi - if ! $SUDO urpmi --force $QUIET_FLAG \ + if ! urpmi --force $QUIET_FLAG \ git \ gcc \ python-augeas \ diff --git a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh index 965ee32f3..129684338 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/rpm_common.sh @@ -24,9 +24,9 @@ BootstrapRpmCommon() { QUIET_FLAG='--quiet' fi - if ! $SUDO $tool list *virtualenv >/dev/null 2>&1; then + if ! $tool list *virtualenv >/dev/null 2>&1; then echo "To use Certbot, packages from the EPEL repository need to be installed." - if ! $SUDO $tool list epel-release >/dev/null 2>&1; then + if ! $tool list epel-release >/dev/null 2>&1; then error "Enable the EPEL repository and try running Certbot again." exit 1 fi @@ -38,7 +38,7 @@ BootstrapRpmCommon() { /bin/echo -e "\e[0K\rEnabling the EPEL repository in 1 seconds..." sleep 1s fi - if ! $SUDO $tool install $yes_flag $QUIET_FLAG epel-release; then + if ! $tool install $yes_flag $QUIET_FLAG epel-release; then error "Could not enable EPEL. Aborting bootstrap!" exit 1 fi @@ -55,7 +55,7 @@ BootstrapRpmCommon() { " # Most RPM distros use the "python" or "python-" naming convention. Let's try that first. - if $SUDO $tool list python >/dev/null 2>&1; then + if $tool list python >/dev/null 2>&1; then pkgs="$pkgs python python-devel @@ -65,7 +65,7 @@ BootstrapRpmCommon() { " # Fedora 26 starts to use the prefix python2 for python2 based packages. # this elseif is theoretically for any Fedora over version 26: - elif $SUDO $tool list python2 >/dev/null 2>&1; then + elif $tool list python2 >/dev/null 2>&1; then pkgs="$pkgs python2 python2-libs @@ -87,13 +87,13 @@ BootstrapRpmCommon() { " fi - if $SUDO $tool list installed "httpd" >/dev/null 2>&1; then + if $tool list installed "httpd" >/dev/null 2>&1; then pkgs="$pkgs mod_ssl " fi - if ! $SUDO $tool install $yes_flag $QUIET_FLAG $pkgs; then + if ! $tool install $yes_flag $QUIET_FLAG $pkgs; then error "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 e60ca8628..56e7acda3 100755 --- a/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh +++ b/letsencrypt-auto-source/pieces/bootstrappers/suse_common.sh @@ -10,7 +10,7 @@ BootstrapSuseCommon() { QUIET_FLAG='-qq' fi - $SUDO zypper $QUIET_FLAG $zypper_flags in $install_flags \ + zypper $QUIET_FLAG $zypper_flags in $install_flags \ python \ python-devel \ python-virtualenv \ diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 6f21c28d5..5c63325ee 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -4,7 +4,7 @@ from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler from contextlib import contextmanager from functools import partial from json import dumps -from os import chmod, environ +from os import chmod, environ, makedirs from os.path import abspath, dirname, exists, join import re from shutil import copy, rmtree @@ -118,12 +118,13 @@ LE_AUTO_PATH = join(dirname(tests_dir()), 'letsencrypt-auto') @contextmanager -def ephemeral_dir(): +def temp_paths(): + """Creates and deletes paths for letsencrypt-auto and its venv.""" dir = mkdtemp(prefix='le-test-') try: - yield dir + yield join(dir, 'letsencrypt-auto'), join(dir, 'venv') finally: - rmtree(dir) + rmtree(dir, ignore_errors=True) def out_and_err(command, input=None, shell=False, env=None): @@ -160,21 +161,20 @@ def signed(content, private_key_name='signing.key'): return out -def install_le_auto(contents, venv_dir): +def install_le_auto(contents, install_path): """Install some given source code as the letsencrypt-auto script at the root level of a virtualenv. :arg contents: The contents of the built letsencrypt-auto script - :arg venv_dir: The path under which to install the script + :arg install_path: The path where to install the script """ - venv_le_auto_path = join(venv_dir, 'letsencrypt-auto') - with open(venv_le_auto_path, 'w') as le_auto: + with open(install_path, 'w') as le_auto: le_auto.write(contents) - chmod(venv_le_auto_path, S_IRUSR | S_IXUSR) + chmod(install_path, S_IRUSR | S_IXUSR) -def run_le_auto(venv_dir, base_url, **kwargs): +def run_le_auto(le_auto_path, venv_dir, base_url, **kwargs): """Run the prebuilt version of letsencrypt-auto, returning stdout and stderr strings. @@ -182,7 +182,7 @@ def run_le_auto(venv_dir, base_url, **kwargs): """ env = environ.copy() - d = dict(XDG_DATA_HOME=venv_dir, + d = dict(VENV_PATH=venv_dir, # URL to PyPI-style JSON that tell us the latest released version # of LE: LE_AUTO_JSON_URL=base_url + 'certbot/json', @@ -201,7 +201,7 @@ iQIDAQAB **kwargs) env.update(d) return out_and_err( - join(venv_dir, 'letsencrypt-auto') + ' --version', + le_auto_path + ' --version', shell=True, env=env) @@ -213,10 +213,12 @@ def set_le_script_version(venv_dir, version): print its version. """ - with open(join(venv_dir, 'letsencrypt', 'bin', 'letsencrypt'), 'w') as script: + letsencrypt_path = join(venv_dir, 'bin', 'letsencrypt') + with open(letsencrypt_path, 'w') as script: script.write("#!/usr/bin/env python\n" "from sys import stderr\n" "stderr.write('letsencrypt %s\\n')" % version) + chmod(letsencrypt_path, S_IRUSR | S_IXUSR) class AutoTests(TestCase): @@ -237,6 +239,11 @@ class AutoTests(TestCase): test suites. """ + NEW_LE_AUTO = build_le_auto( + version='99.9.9', + requirements='letsencrypt==99.9.9 --hash=sha256:1cc14d61ab424cdee446f51e50f1123f8482ec740587fe78626c933bba2873a0') + NEW_LE_AUTO_SIG = signed(NEW_LE_AUTO) + def test_successes(self): """Exercise most branches of letsencrypt-auto. @@ -252,20 +259,16 @@ class AutoTests(TestCase): the next, saving code. """ - NEW_LE_AUTO = build_le_auto( - version='99.9.9', - requirements='letsencrypt==99.9.9 --hash=sha256:1cc14d61ab424cdee446f51e50f1123f8482ec740587fe78626c933bba2873a0') - NEW_LE_AUTO_SIG = signed(NEW_LE_AUTO) - - with ephemeral_dir() as venv_dir: + with temp_paths() as (le_auto_path, venv_dir): # This serves a PyPI page with a higher version, a GitHub-alike # with a corresponding le-auto script, and a matching signature. resources = {'certbot/json': dumps({'releases': {'99.9.9': None}}), - 'v99.9.9/letsencrypt-auto': NEW_LE_AUTO, - 'v99.9.9/letsencrypt-auto.sig': NEW_LE_AUTO_SIG} + 'v99.9.9/letsencrypt-auto': self.NEW_LE_AUTO, + 'v99.9.9/letsencrypt-auto.sig': self.NEW_LE_AUTO_SIG} with serving(resources) as base_url: run_letsencrypt_auto = partial( run_le_auto, + le_auto_path, venv_dir, base_url, PIP_FIND_LINKS=join(tests_dir(), @@ -274,7 +277,7 @@ class AutoTests(TestCase): # Test when a phase-1 upgrade is needed, there's no LE binary # installed, and pip hashes verify: - install_le_auto(build_le_auto(version='50.0.0'), venv_dir) + install_le_auto(build_le_auto(version='50.0.0'), le_auto_path) out, err = run_letsencrypt_auto() ok_(re.match(r'letsencrypt \d+\.\d+\.\d+', err.strip().splitlines()[-1])) @@ -291,16 +294,28 @@ class AutoTests(TestCase): self.assertFalse('Upgrading certbot-auto ' in out) self.assertFalse('Creating virtual environment...' in out) - # Test when a phase-1 upgrade is not needed but a phase-2 - # upgrade is: + def test_phase2_upgrade(self): + """Test a phase-2 upgrade without a phase-1 upgrade.""" + with temp_paths() as (le_auto_path, venv_dir): + resources = {'certbot/json': dumps({'releases': {'99.9.9': None}}), + 'v99.9.9/letsencrypt-auto': self.NEW_LE_AUTO, + 'v99.9.9/letsencrypt-auto.sig': self.NEW_LE_AUTO_SIG} + with serving(resources) as base_url: + venv_bin = join(venv_dir, 'bin') + makedirs(venv_bin) set_le_script_version(venv_dir, '0.0.1') - out, err = run_letsencrypt_auto() + + install_le_auto(self.NEW_LE_AUTO, le_auto_path) + pip_find_links=join(tests_dir(), 'fake-letsencrypt', 'dist') + out, err = run_le_auto(le_auto_path, venv_dir, base_url, + PIP_FIND_LINKS=pip_find_links) + self.assertFalse('Upgrading certbot-auto ' in out) self.assertTrue('Creating virtual environment...' in out) def test_openssl_failure(self): """Make sure we stop if the openssl signature check fails.""" - with ephemeral_dir() as venv_dir: + with temp_paths() as (le_auto_path, venv_dir): # Serve an unrelated hash signed with the good key (easier than # making a bad key, and a mismatch is a mismatch): resources = {'': 'certbot/', @@ -308,9 +323,9 @@ class AutoTests(TestCase): 'v99.9.9/letsencrypt-auto': build_le_auto(version='99.9.9'), 'v99.9.9/letsencrypt-auto.sig': signed('something else')} with serving(resources) as base_url: - copy(LE_AUTO_PATH, venv_dir) + copy(LE_AUTO_PATH, le_auto_path) try: - out, err = run_le_auto(venv_dir, base_url) + out, err = run_le_auto(le_auto_path, venv_dir, base_url) except CalledProcessError as exc: eq_(exc.returncode, 1) self.assertTrue("Couldn't verify signature of downloaded " @@ -320,7 +335,7 @@ class AutoTests(TestCase): def test_pip_failure(self): """Make sure pip stops us if there is a hash mismatch.""" - with ephemeral_dir() as venv_dir: + with temp_paths() as (le_auto_path, venv_dir): resources = {'': 'certbot/', 'certbot/json': dumps({'releases': {'99.9.9': None}})} with serving(resources) as base_url: @@ -329,14 +344,14 @@ class AutoTests(TestCase): build_le_auto( version='99.9.9', requirements='configobj==5.0.6 --hash=sha256:badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb'), - venv_dir) + le_auto_path) try: - out, err = run_le_auto(venv_dir, base_url) + out, err = run_le_auto(le_auto_path, venv_dir, base_url) except CalledProcessError as exc: eq_(exc.returncode, 1) self.assertTrue("THESE PACKAGES DO NOT MATCH THE HASHES " "FROM THE REQUIREMENTS FILE" in exc.output) - ok_(not exists(join(venv_dir, 'letsencrypt')), + ok_(not exists(venv_dir), msg="The virtualenv was left around, even though " "installation didn't succeed. We shouldn't do " "this, as it foils our detection of whether we "