From 5cc69d92e7fc67c52a967af9f9af65df1016d177 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Thu, 3 Dec 2015 18:51:18 -0500 Subject: [PATCH] In Phase 1, download a new letsencrypt-auto script only if necessary. * Temp dir creation is now always done in shell. * Split download_upgrade.py into 2 phases itself so we can have it report back the latest LE version and make a decision based on it before doing and major downloading. Rename it for clarity. --- letsencrypt_auto/letsencrypt-auto.template | 79 +++++++++---------- .../pieces/{download_upgrade.py => fetch.py} | 61 +++++++------- 2 files changed, 70 insertions(+), 70 deletions(-) rename letsencrypt_auto/pieces/{download_upgrade.py => fetch.py} (69%) diff --git a/letsencrypt_auto/letsencrypt-auto.template b/letsencrypt_auto/letsencrypt-auto.template index 70dafde2c..293cc8f84 100755 --- a/letsencrypt_auto/letsencrypt-auto.template +++ b/letsencrypt_auto/letsencrypt-auto.template @@ -13,6 +13,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share} VENV_NAME="letsencrypt" VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"} VENV_BIN=${VENV_PATH}/bin +LE_AUTO_VERSION="{{ LE_AUTO_VERSION }}" ExperimentalBootstrap() { # Arguments: Platform name, bootstrap function name @@ -102,6 +103,10 @@ Bootstrap() { fi } +TempDir() { + mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || OS X +} + # 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) @@ -165,7 +170,7 @@ elif [ "$1" = "--no-self-upgrade" ]; then else INSTALLED_VERSION="0.0.0" fi - if [ "{{ LE_AUTO_VERSION }}" = $INSTALLED_VERSION ]; then + if [ "$LE_AUTO_VERSION" = "$INSTALLED_VERSION" ]; then echo "Reusing old virtual environment." else echo "Creating virtual environment..." @@ -177,9 +182,9 @@ elif [ "$1" = "--no-self-upgrade" ]; then virtualenv --no-site-packages --python $LE_PYTHON $VENV_PATH > /dev/null fi - # Install Python dependencies with peep, then run letsencrypt. - echo "Installing Python package dependencies..." - TEMP_DIR=`mktemp -d 2>/dev/null || mktemp -d -t 'le'` # Linux || OS X + echo "Installing Python packages..." + TEMP_DIR=$(TempDir) + # There is no $ interpolation due to quotes on heredoc delimiters. # --------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > $TEMP_DIR/letsencrypt-auto-requirements.txt {{ letsencrypt-auto-requirements.txt }} @@ -195,7 +200,7 @@ UNLIKELY_EOF set -e rm -rf $TEMP_DIR if [ "$PEEP_STATUS" != 0 ]; then - # Report error: + # Report error. (Otherwise, be quiet.) echo $PEEP_OUT exit 1 fi @@ -205,45 +210,37 @@ UNLIKELY_EOF $SUDO $VENV_BIN/letsencrypt "$@" else # Phase 1: Upgrade letsencrypt-auto if neceesary, then self-invoke. - - if [ ! -f $VENV_BIN/letsencrypt ]; then - OLD_VERSION="0.0.0" # ($VENV_BIN/letsencrypt --version) - else - OLD_VERSION="0.0.0" - fi - - # TODO: Don't bother upgrading if we're already up to date. - if [ "$OLD_VERSION" != "1.2.3" ]; then - echo "Upgrading letsencrypt-auto..." - DeterminePythonVersion + # + # 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. + + echo "Checking for new version..." + TEMP_DIR=$(TempDir) + # ------------------------------------------------------------------------- + cat << "UNLIKELY_EOF" > $TEMP_DIR/fetch.py +{{ fetch.py }} +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..." # 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. - # - # There is no $ interpolation due to quotes on heredoc delimiters. - set +e - # ------------------------------------------------------------------------- - TEMP_DIR=`$LE_PYTHON - << "UNLIKELY_EOF" -{{ download_upgrade.py }} -UNLIKELY_EOF` - # ------------------------------------------------------------------------- - DOWNLOAD_STATUS=$? - set -e - if [ "$DOWNLOAD_STATUS" = 0 ]; then - # Install new copy of letsencrypt-auto. This preserves permissions and - # ownership from the old copy. - # TODO: Deal with quotes in pathnames. - echo "Installing new version of letsencrypt-auto..." - echo " " $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$0" - $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$0" - # TODO: Clean up temp dir safely, even if it has quotes in its path. - rm -rf $TEMP_DIR - "$0" --no-self-upgrade "$@" - else - # Report error: - echo $TEMP_DIR - exit 1 - fi + $LE_PYTHON "$TEMP_DIR/fetch.py" --le-auto-script "v$REMOTE_VERSION" + + # Install new copy of letsencrypt-auto. This preserves permissions and + # ownership from the old copy. + # TODO: Deal with quotes in pathnames. + echo "Installing new version of letsencrypt-auto..." + echo " " $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$0" + $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$0" + # TODO: Clean up temp dir safely, even if it has quotes in its path. + rm -rf $TEMP_DIR fi # should upgrade + "$0" --no-self-upgrade "$@" fi diff --git a/letsencrypt_auto/pieces/download_upgrade.py b/letsencrypt_auto/pieces/fetch.py similarity index 69% rename from letsencrypt_auto/pieces/download_upgrade.py rename to letsencrypt_auto/pieces/fetch.py index ec0a11549..9e7a431b8 100644 --- a/letsencrypt_auto/pieces/download_upgrade.py +++ b/letsencrypt_auto/pieces/fetch.py @@ -1,4 +1,11 @@ -"""Print a path to a temp dir containing a new copy of letsencrypt-auto. +"""Do downloading and JSON parsing without additional dependencies. :: + + # Print latest released version of LE to stdout: + python fetch.py --latest-version + + # Download letsencrypt-auto script from git tag v1.2.3 into the folder I'm + # in, and make sure its signature verifies: + python fetch.py --le-auto-script v1.2.3 On failure, return non-zero. @@ -9,8 +16,7 @@ from os import devnull from os.path import dirname, join import re from subprocess import check_call, CalledProcessError -from sys import exit -from tempfile import mkdtemp +from sys import argv, exit from urllib2 import build_opener, HTTPHandler, HTTPSHandler, HTTPError @@ -59,62 +65,59 @@ class HttpsGetter(object): raise ExpectedError("Couldn't download %s." % url, exc) -class TempDir(object): - def __init__(self): - self.path = mkdtemp() - - def write(self, contents, filename): - """Write something to a named file in me.""" - with open(join(self.path, filename), 'w') as file: - file.write(contents) +def write(contents, dir, filename): + """Write something to a file in a certain directory.""" + with open(join(dir, filename), 'w') as file: + file.write(contents) -def latest_stable_version(get, package): - """Apply a fairly safe heuristic to determine the latest stable release of - a PyPI package.""" +def latest_stable_version(get): + """Return the latest stable release of letsencrypt.""" metadata = loads(get('https://pypi.python.org/pypi/letsencrypt/json')) # metadata['info']['version'] actually returns the latest of any kind of # release release, contrary to https://wiki.python.org/moin/PyPIJSON. + # The regex is a sufficient regex for picking out prereleases for most + # packages, LE included. return str(max(LooseVersion(r) for r in metadata['releases'].iterkeys() if re.match('^[0-9.]+$', r))) -def verified_new_le_auto(get, tag, temp): +def verified_new_le_auto(get, tag, temp_dir): """Return the path to a verified, up-to-date letsencrypt-auto script. - If the download's signature does not verify or something else goes wrong, - raise ExpectedError. + If the download's signature does not verify or something else goes wrong + with the verification process, raise ExpectedError. """ le_auto_dir = ('https://raw.githubusercontent.com/letsencrypt/letsencrypt/' '%s/letsencrypt-auto/' % tag) - temp.write(get(le_auto_dir + 'letsencrypt-auto'), 'letsencrypt-auto') - temp.write(get(le_auto_dir + 'letsencrypt-auto.sig'), 'letsencrypt-auto.sig') - temp.write(PUBLIC_KEY, 'public_key.pem') - le_auto_path = join(temp.path, 'letsencrypt-auto') + write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto') + write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig') + write(PUBLIC_KEY, temp_dir, 'public_key.pem') try: with open(devnull, 'w') as dev_null: check_call(['openssl', 'dgst', '-sha256', '-verify', - join(temp.path, 'public_key.pem'), + join(temp_dir, 'public_key.pem'), '-signature', - join(temp.path, 'letsencrypt-auto.sig'), - le_auto_path], + join(temp_dir, 'letsencrypt-auto.sig'), + join(temp_dir, 'letsencrypt-auto')], stdout=dev_null, stderr=dev_null) except CalledProcessError as exc: raise ExpectedError("Couldn't verify signature of downloaded " "letsencrypt-auto.", exc) - else: # belt & suspenders - return le_auto_path def main(): get = HttpsGetter().get - temp = TempDir() + flag = argv[1] try: - stable_tag = 'v' + latest_stable_version(get) - print dirname(verified_new_le_auto(get, stable_tag, temp)) + if flag == '--latest-version': + print latest_stable_version(get) + elif flag == '--le-auto-script': + tag = argv[2] + verified_new_le_auto(get, tag, dirname(argv[0])) except ExpectedError as exc: print exc.args[0], exc.args[1] return 1