mirror of
https://github.com/certbot/certbot.git
synced 2026-08-03 08:03:10 +02:00
Merge pull request #1665 from erikrose/letsencrypt-auto-release
Make letsencrypt-auto release-suitable, self-upgrading, and self-contained.
This commit is contained in:
+2
-2
@@ -22,8 +22,8 @@ WORKDIR /opt/letsencrypt
|
|||||||
|
|
||||||
# TODO: Install non-default Python versions for tox.
|
# TODO: Install non-default Python versions for tox.
|
||||||
# TODO: Install Apache/Nginx for plugin development.
|
# TODO: Install Apache/Nginx for plugin development.
|
||||||
COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ubuntu.sh
|
COPY letsencrypt-auto-source/letsencrypt-auto /opt/letsencrypt/src/letsencrypt-auto
|
||||||
RUN /opt/letsencrypt/src/ubuntu.sh && \
|
RUN /opt/letsencrypt/src/letsencrypt-auto --os-packages-only && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/* \
|
rm -rf /var/lib/apt/lists/* \
|
||||||
/tmp/* \
|
/tmp/* \
|
||||||
|
|||||||
Vendored
+1
-1
@@ -7,7 +7,7 @@ VAGRANTFILE_API_VERSION = "2"
|
|||||||
# Setup instructions from docs/contributing.rst
|
# Setup instructions from docs/contributing.rst
|
||||||
$ubuntu_setup_script = <<SETUP_SCRIPT
|
$ubuntu_setup_script = <<SETUP_SCRIPT
|
||||||
cd /vagrant
|
cd /vagrant
|
||||||
./bootstrap/install-deps.sh
|
./letsencrypt-auto-source/letsencrypt-auto --os-packages-only
|
||||||
./bootstrap/dev/venv.sh
|
./bootstrap/dev/venv.sh
|
||||||
SETUP_SCRIPT
|
SETUP_SCRIPT
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ install_requires = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# env markers in extras_require cause problems with older pip: #517
|
# env markers in extras_require cause problems with older pip: #517
|
||||||
|
# Keep in sync with conditional_requirements.py.
|
||||||
if sys.version_info < (2, 7):
|
if sys.version_info < (2, 7):
|
||||||
install_requires.extend([
|
install_requires.extend([
|
||||||
# only some distros recognize stdlib argparse as already satisfying
|
# only some distros recognize stdlib argparse as already satisfying
|
||||||
|
|||||||
+8
-48
@@ -22,7 +22,7 @@ once:
|
|||||||
|
|
||||||
git clone https://github.com/letsencrypt/letsencrypt
|
git clone https://github.com/letsencrypt/letsencrypt
|
||||||
cd letsencrypt
|
cd letsencrypt
|
||||||
./bootstrap/install-deps.sh
|
./letsencrypt-auto-source/letsencrypt-auto --os-packages-only
|
||||||
./bootstrap/dev/venv.sh
|
./bootstrap/dev/venv.sh
|
||||||
|
|
||||||
Then in each shell where you're working on the client, do:
|
Then in each shell where you're working on the client, do:
|
||||||
@@ -365,10 +365,13 @@ Now run tests inside the Docker image:
|
|||||||
Notes on OS dependencies
|
Notes on OS dependencies
|
||||||
========================
|
========================
|
||||||
|
|
||||||
OS level dependencies are managed by scripts in ``bootstrap``. Some notes
|
OS-level dependencies can be installed like so:
|
||||||
are provided here mainly for the :ref:`developers <hacking>` reference.
|
|
||||||
|
|
||||||
In general:
|
.. code-block:: shell
|
||||||
|
|
||||||
|
letsencrypt-auto-source/letsencrypt-auto --os-packages-only
|
||||||
|
|
||||||
|
In general...
|
||||||
|
|
||||||
* ``sudo`` is required as a suggested way of running privileged process
|
* ``sudo`` is required as a suggested way of running privileged process
|
||||||
* `Python`_ 2.6/2.7 is required
|
* `Python`_ 2.6/2.7 is required
|
||||||
@@ -380,62 +383,19 @@ In general:
|
|||||||
.. _Augeas: http://augeas.net/
|
.. _Augeas: http://augeas.net/
|
||||||
.. _Virtualenv: https://virtualenv.pypa.io
|
.. _Virtualenv: https://virtualenv.pypa.io
|
||||||
|
|
||||||
Ubuntu
|
|
||||||
------
|
|
||||||
|
|
||||||
.. code-block:: shell
|
|
||||||
|
|
||||||
sudo ./bootstrap/ubuntu.sh
|
|
||||||
|
|
||||||
|
|
||||||
Debian
|
Debian
|
||||||
------
|
------
|
||||||
|
|
||||||
.. code-block:: shell
|
|
||||||
|
|
||||||
sudo ./bootstrap/debian.sh
|
|
||||||
|
|
||||||
For squeeze you will need to:
|
For squeeze you will need to:
|
||||||
|
|
||||||
- Use ``virtualenv --no-site-packages -p python`` instead of ``-p python2``.
|
- Use ``virtualenv --no-site-packages -p python`` instead of ``-p python2``.
|
||||||
|
|
||||||
|
|
||||||
.. _`#280`: https://github.com/letsencrypt/letsencrypt/issues/280
|
|
||||||
|
|
||||||
|
|
||||||
Mac OSX
|
|
||||||
-------
|
|
||||||
|
|
||||||
.. code-block:: shell
|
|
||||||
|
|
||||||
./bootstrap/mac.sh
|
|
||||||
|
|
||||||
|
|
||||||
Fedora
|
|
||||||
------
|
|
||||||
|
|
||||||
.. code-block:: shell
|
|
||||||
|
|
||||||
sudo ./bootstrap/fedora.sh
|
|
||||||
|
|
||||||
|
|
||||||
Centos 7
|
|
||||||
--------
|
|
||||||
|
|
||||||
.. code-block:: shell
|
|
||||||
|
|
||||||
sudo ./bootstrap/centos.sh
|
|
||||||
|
|
||||||
|
|
||||||
FreeBSD
|
FreeBSD
|
||||||
-------
|
-------
|
||||||
|
|
||||||
.. code-block:: shell
|
Package installation for FreeBSD uses ``pkg``, not ports.
|
||||||
|
|
||||||
sudo ./bootstrap/freebsd.sh
|
|
||||||
|
|
||||||
Bootstrap script for FreeBSD uses ``pkg`` for package installation,
|
|
||||||
i.e. it does not use ports.
|
|
||||||
|
|
||||||
FreeBSD by default uses ``tcsh``. In order to activate virtualenv (see
|
FreeBSD by default uses ``tcsh``. In order to activate virtualenv (see
|
||||||
below), you will need a compatible shell, e.g. ``pkg install bash &&
|
below), you will need a compatible shell, e.g. ``pkg install bash &&
|
||||||
|
|||||||
@@ -63,4 +63,5 @@ setup(
|
|||||||
'apache = letsencrypt_apache.configurator:ApacheConfigurator',
|
'apache = letsencrypt_apache.configurator:ApacheConfigurator',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
test_suite='letsencrypt_apache',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# For running tests, build a docker image with a passwordless sudo and a trust
|
||||||
|
# store we can manipulate.
|
||||||
|
|
||||||
|
FROM ubuntu:trusty
|
||||||
|
|
||||||
|
# Add an unprivileged user:
|
||||||
|
RUN useradd --create-home --home-dir /home/lea --shell /bin/bash --groups sudo --uid 1000 lea
|
||||||
|
|
||||||
|
# Let that user sudo:
|
||||||
|
RUN adduser lea sudo
|
||||||
|
RUN sed -i.bkp -e \
|
||||||
|
's/%sudo\s\+ALL=(ALL\(:ALL\)\?)\s\+ALL/%sudo ALL=NOPASSWD:ALL/g' \
|
||||||
|
/etc/sudoers
|
||||||
|
|
||||||
|
# Install pip and nose:
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get -q -y install python-pip && \
|
||||||
|
apt-get clean
|
||||||
|
RUN pip install nose
|
||||||
|
|
||||||
|
RUN mkdir -p /home/lea/letsencrypt/letsencrypt
|
||||||
|
|
||||||
|
# Install fake testing CA:
|
||||||
|
COPY ./tests/certs/ca/my-root-ca.crt.pem /usr/local/share/ca-certificates/
|
||||||
|
RUN update-ca-certificates
|
||||||
|
|
||||||
|
# Copy code:
|
||||||
|
COPY . /home/lea/letsencrypt/letsencrypt-auto-source
|
||||||
|
|
||||||
|
USER lea
|
||||||
|
WORKDIR /home/lea
|
||||||
|
|
||||||
|
CMD ["nosetests", "-v", "-s", "letsencrypt/letsencrypt-auto-source/tests"]
|
||||||
Executable
+64
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Stitch together the letsencrypt-auto script.
|
||||||
|
|
||||||
|
Implement a simple templating language in which {{ some/file }} turns into the
|
||||||
|
contents of the file at ./pieces/some/file except for certain tokens which have
|
||||||
|
other, special definitions.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from os.path import abspath, dirname, join
|
||||||
|
import re
|
||||||
|
from sys import argv
|
||||||
|
|
||||||
|
|
||||||
|
DIR = dirname(abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
def le_version(build_script_dir):
|
||||||
|
"""Return the version number stamped in letsencrypt/__init__.py."""
|
||||||
|
return re.search('''^__version__ = ['"](.+)['"].*''',
|
||||||
|
file_contents(join(dirname(build_script_dir),
|
||||||
|
'letsencrypt',
|
||||||
|
'__init__.py')),
|
||||||
|
re.M).group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def file_contents(path):
|
||||||
|
with open(path) as file:
|
||||||
|
return file.read()
|
||||||
|
|
||||||
|
|
||||||
|
def build(version=None, requirements=None):
|
||||||
|
"""Return the built contents of the letsencrypt-auto script.
|
||||||
|
|
||||||
|
:arg version: The version to attach to the script. Default: the version of
|
||||||
|
the letsencrypt package
|
||||||
|
:arg requirements: The contents of the requirements file to embed. Default:
|
||||||
|
contents of letsencrypt-auto-requirements.txt
|
||||||
|
|
||||||
|
"""
|
||||||
|
special_replacements = {
|
||||||
|
'LE_AUTO_VERSION': version or le_version(DIR)
|
||||||
|
}
|
||||||
|
if requirements:
|
||||||
|
special_replacements['letsencrypt-auto-requirements.txt'] = requirements
|
||||||
|
|
||||||
|
def replacer(match):
|
||||||
|
token = match.group(1)
|
||||||
|
if token in special_replacements:
|
||||||
|
return special_replacements[token]
|
||||||
|
else:
|
||||||
|
return file_contents(join(DIR, 'pieces', token))
|
||||||
|
|
||||||
|
return re.sub(r'{{\s*([A-Za-z0-9_./-]+)\s*}}',
|
||||||
|
replacer,
|
||||||
|
file_contents(join(DIR, 'letsencrypt-auto.template')))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
with open(join(DIR, 'letsencrypt-auto'), 'w') as out:
|
||||||
|
out.write(build())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Executable
+1789
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+262
@@ -0,0 +1,262 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Download and run the latest release version of the Let's Encrypt client.
|
||||||
|
#
|
||||||
|
# NOTE: THIS SCRIPT IS AUTO-GENERATED AND SELF-UPDATING
|
||||||
|
#
|
||||||
|
# IF YOU WANT TO EDIT IT LOCALLY, *ALWAYS* RUN YOUR COPY WITH THE
|
||||||
|
# "--no-self-upgrade" FLAG
|
||||||
|
#
|
||||||
|
# IF YOU WANT TO SEND PULL REQUESTS, THE REAL SOURCE FOR THIS FILE IS
|
||||||
|
# letsencrypt-auto-source/letsencrypt-auto.template AND
|
||||||
|
# letsencrypt-auto-source/pieces/bootstrappers/*
|
||||||
|
|
||||||
|
set -e # Work even if somebody does "sh thisscript.sh".
|
||||||
|
|
||||||
|
# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script,
|
||||||
|
# if you want to change where the virtual environment will be installed
|
||||||
|
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 }}"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# This first clause is redundant with the third, but hedging on portability
|
||||||
|
if [ "$arg" = "-v" ] || [ "$arg" = "--verbose" ] || echo "$arg" | grep -E -- "-v+$" ; then
|
||||||
|
VERBOSE=1
|
||||||
|
elif [ "$arg" = "--no-self-upgrade" ] ; then
|
||||||
|
# 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
|
||||||
|
elif [ "$arg" = "--os-packages-only" ] ; then
|
||||||
|
OS_PACKAGES_ONLY=1
|
||||||
|
elif [ "$arg" = "--debug" ]; then
|
||||||
|
DEBUG=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# letsencrypt-auto needs root access to bootstrap OS dependencies, and
|
||||||
|
# letsencrypt 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`
|
||||||
|
if test "`id -u`" -ne "0" ; then
|
||||||
|
if command -v sudo 1>/dev/null 2>&1; then
|
||||||
|
SUDO=sudo
|
||||||
|
else
|
||||||
|
echo \"sudo\" is not available, will use \"su\" for installation steps...
|
||||||
|
# Because the parameters in `su -c` has to be a string,
|
||||||
|
# we need properly escape it
|
||||||
|
su_sudo() {
|
||||||
|
args=""
|
||||||
|
# This `while` loop iterates over all parameters given to this function.
|
||||||
|
# For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
|
||||||
|
# will be wrapped in a pair of `'`, then appended to `$args` string
|
||||||
|
# For example, `echo "It's only 1\$\!"` will be escaped to:
|
||||||
|
# 'echo' 'It'"'"'s only 1$!'
|
||||||
|
# │ │└┼┘│
|
||||||
|
# │ │ │ └── `'s only 1$!'` the literal string
|
||||||
|
# │ │ └── `\"'\"` is a single quote (as a string)
|
||||||
|
# │ └── `'It'`, to be concatenated with the strings following it
|
||||||
|
# └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
|
||||||
|
while [ $# -ne 0 ]; do
|
||||||
|
args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
su root -c "$args"
|
||||||
|
}
|
||||||
|
SUDO=su_sudo
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
SUDO=
|
||||||
|
fi
|
||||||
|
|
||||||
|
ExperimentalBootstrap() {
|
||||||
|
# Arguments: Platform name, bootstrap function name
|
||||||
|
if [ "$DEBUG" = 1 ]; then
|
||||||
|
if [ "$2" != "" ]; then
|
||||||
|
echo "Bootstrapping dependencies via $1..."
|
||||||
|
$2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "WARNING: $1 support is very experimental at present..."
|
||||||
|
echo "if you would like to work on improving it, please ensure you have backups"
|
||||||
|
echo "and then run this script again with the --debug flag!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
DeterminePythonVersion() {
|
||||||
|
if command -v python2.7 > /dev/null ; then
|
||||||
|
export LE_PYTHON=${LE_PYTHON:-python2.7}
|
||||||
|
elif command -v python27 > /dev/null ; then
|
||||||
|
export LE_PYTHON=${LE_PYTHON:-python27}
|
||||||
|
elif command -v python2 > /dev/null ; then
|
||||||
|
export LE_PYTHON=${LE_PYTHON:-python2}
|
||||||
|
elif command -v python > /dev/null ; then
|
||||||
|
export LE_PYTHON=${LE_PYTHON:-python}
|
||||||
|
else
|
||||||
|
echo "Cannot find any Pythons... please install one!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PYVER=`"$LE_PYTHON" --version 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'`
|
||||||
|
if [ $PYVER -eq 26 ]; then
|
||||||
|
ExperimentalBootstrap "Python 2.6"
|
||||||
|
elif [ $PYVER -lt 26 ]; then
|
||||||
|
echo "You have an ancient version of Python entombed in your operating system..."
|
||||||
|
echo "This isn't going to work; you'll need at least version 2.6."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
{{ bootstrappers/deb_common.sh }}
|
||||||
|
{{ bootstrappers/rpm_common.sh }}
|
||||||
|
{{ bootstrappers/suse_common.sh }}
|
||||||
|
{{ bootstrappers/arch_common.sh }}
|
||||||
|
{{ bootstrappers/gentoo_common.sh }}
|
||||||
|
{{ bootstrappers/free_bsd.sh }}
|
||||||
|
{{ bootstrappers/mac.sh }}
|
||||||
|
|
||||||
|
# Install required OS packages:
|
||||||
|
Bootstrap() {
|
||||||
|
if [ -f /etc/debian_version ]; then
|
||||||
|
echo "Bootstrapping dependencies for Debian-based OSes..."
|
||||||
|
BootstrapDebCommon
|
||||||
|
elif [ -f /etc/redhat-release ]; then
|
||||||
|
echo "Bootstrapping dependencies for RedHat-based OSes..."
|
||||||
|
BootstrapRpmCommon
|
||||||
|
elif `grep -q openSUSE /etc/os-release` ; then
|
||||||
|
echo "Bootstrapping dependencies for openSUSE-based OSes..."
|
||||||
|
BootstrapSuseCommon
|
||||||
|
elif [ -f /etc/arch-release ]; then
|
||||||
|
if [ "$DEBUG" = 1 ]; then
|
||||||
|
echo "Bootstrapping dependencies for Archlinux..."
|
||||||
|
BootstrapArchCommon
|
||||||
|
else
|
||||||
|
echo "Please use pacman to install letsencrypt packages:"
|
||||||
|
echo "# pacman -S letsencrypt letsencrypt-apache"
|
||||||
|
echo
|
||||||
|
echo "If you would like to use the virtualenv way, please run the script again with the"
|
||||||
|
echo "--debug flag."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [ -f /etc/manjaro-release ]; then
|
||||||
|
ExperimentalBootstrap "Manjaro Linux" BootstrapArchCommon
|
||||||
|
elif [ -f /etc/gentoo-release ]; then
|
||||||
|
ExperimentalBootstrap "Gentoo" BootstrapGentooCommon
|
||||||
|
elif uname | grep -iq FreeBSD ; then
|
||||||
|
ExperimentalBootstrap "FreeBSD" BootstrapFreeBsd
|
||||||
|
elif uname | grep -iq Darwin ; then
|
||||||
|
ExperimentalBootstrap "Mac OS X" BootstrapMac
|
||||||
|
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
|
||||||
|
echo "You will need to bootstrap, configure virtualenv, and run a peep install manually."
|
||||||
|
echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites"
|
||||||
|
echo "for more info."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
TempDir() {
|
||||||
|
mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || OS X
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if [ "$NO_SELF_UPGRADE" = 1 ]; then
|
||||||
|
# Phase 2: Create venv, install LE, and run.
|
||||||
|
|
||||||
|
if [ -f "$VENV_BIN/letsencrypt" ]; then
|
||||||
|
INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | cut -d " " -f 2)
|
||||||
|
else
|
||||||
|
INSTALLED_VERSION="none"
|
||||||
|
fi
|
||||||
|
if [ "$LE_AUTO_VERSION" != "$INSTALLED_VERSION" ]; then
|
||||||
|
echo "Creating virtual environment..."
|
||||||
|
DeterminePythonVersion
|
||||||
|
rm -rf "$VENV_PATH"
|
||||||
|
if [ "$VERBOSE" = 1 ]; then
|
||||||
|
virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH"
|
||||||
|
else
|
||||||
|
virtualenv --no-site-packages --python "$LE_PYTHON" "$VENV_PATH" > /dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Installing Python packages..."
|
||||||
|
TEMP_DIR=$(TempDir)
|
||||||
|
# There is no $ interpolation due to quotes on starting heredoc delimiter.
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt"
|
||||||
|
{{ letsencrypt-auto-requirements.txt }}
|
||||||
|
UNLIKELY_EOF
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
cat << "UNLIKELY_EOF" > "$TEMP_DIR/peep.py"
|
||||||
|
{{ peep.py }}
|
||||||
|
UNLIKELY_EOF
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
set +e
|
||||||
|
PEEP_OUT=`"$VENV_BIN/python" "$TEMP_DIR/peep.py" install -r "$TEMP_DIR/letsencrypt-auto-requirements.txt"`
|
||||||
|
PEEP_STATUS=$?
|
||||||
|
set -e
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
if [ "$PEEP_STATUS" != 0 ]; then
|
||||||
|
# Report error. (Otherwise, be quiet.)
|
||||||
|
echo "Had a problem while downloading and verifying Python packages:"
|
||||||
|
echo "$PEEP_OUT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "Requesting root privileges to run letsencrypt..."
|
||||||
|
echo " " $SUDO "$VENV_BIN/letsencrypt" "$@"
|
||||||
|
$SUDO "$VENV_BIN/letsencrypt" "$@"
|
||||||
|
else
|
||||||
|
# Phase 1: Upgrade letsencrypt-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.
|
||||||
|
|
||||||
|
if [ ! -f "$VENV_BIN/letsencrypt" ]; then
|
||||||
|
# If it looks like we've never bootstrapped before, bootstrap:
|
||||||
|
Bootstrap
|
||||||
|
fi
|
||||||
|
if [ "$OS_PACKAGES_ONLY" = 1 ]; then
|
||||||
|
echo "OS packages installed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
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.
|
||||||
|
"$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 "Replacing 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
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
BootstrapArchCommon() {
|
||||||
|
# Tested with:
|
||||||
|
# - ArchLinux (x86_64)
|
||||||
|
#
|
||||||
|
# "python-virtualenv" is Python3, but "python2-virtualenv" provides
|
||||||
|
# only "virtualenv2" binary, not "virtualenv" necessary in
|
||||||
|
# ./bootstrap/dev/_common_venv.sh
|
||||||
|
|
||||||
|
deps="
|
||||||
|
python2
|
||||||
|
python-virtualenv
|
||||||
|
gcc
|
||||||
|
dialog
|
||||||
|
augeas
|
||||||
|
openssl
|
||||||
|
libffi
|
||||||
|
ca-certificates
|
||||||
|
pkg-config
|
||||||
|
"
|
||||||
|
|
||||||
|
missing=$("$SUDO" pacman -T $deps)
|
||||||
|
|
||||||
|
if [ "$missing" ]; then
|
||||||
|
"$SUDO" pacman -S --needed $missing
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
BootstrapDebCommon() {
|
||||||
|
# Current version tested with:
|
||||||
|
#
|
||||||
|
# - Ubuntu
|
||||||
|
# - 14.04 (x64)
|
||||||
|
# - 15.04 (x64)
|
||||||
|
# - Debian
|
||||||
|
# - 7.9 "wheezy" (x64)
|
||||||
|
# - sid (2015-10-21) (x64)
|
||||||
|
|
||||||
|
# Past versions tested with:
|
||||||
|
#
|
||||||
|
# - Debian 8.0 "jessie" (x64)
|
||||||
|
# - Raspbian 7.8 (armhf)
|
||||||
|
|
||||||
|
# Believed not to work:
|
||||||
|
#
|
||||||
|
# - Debian 6.0.10 "squeeze" (x64)
|
||||||
|
|
||||||
|
$SUDO apt-get update || echo apt-get update hit problems but continuing anyway...
|
||||||
|
|
||||||
|
# virtualenv binary can be found in different packages depending on
|
||||||
|
# distro version (#346)
|
||||||
|
|
||||||
|
virtualenv=
|
||||||
|
if apt-cache show virtualenv > /dev/null 2>&1; then
|
||||||
|
virtualenv="virtualenv"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if apt-cache show python-virtualenv > /dev/null 2>&1; then
|
||||||
|
virtualenv="$virtualenv python-virtualenv"
|
||||||
|
fi
|
||||||
|
|
||||||
|
augeas_pkg="libaugeas0 augeas-lenses"
|
||||||
|
AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
|
||||||
|
|
||||||
|
AddBackportRepo() {
|
||||||
|
# ARGS:
|
||||||
|
BACKPORT_NAME="$1"
|
||||||
|
BACKPORT_SOURCELINE="$2"
|
||||||
|
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")'
|
||||||
|
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 dpkg --compare-versions 1.0 gt "$AUGVERSION" ; then
|
||||||
|
if lsb_release -a | grep -q wheezy ; then
|
||||||
|
AddBackportRepo wheezy-backports "deb http://http.debian.net/debian wheezy-backports main"
|
||||||
|
elif lsb_release -a | grep -q precise ; then
|
||||||
|
# XXX add ARM case
|
||||||
|
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..."
|
||||||
|
fi
|
||||||
|
# XXX add a case for ubuntu PPAs
|
||||||
|
fi
|
||||||
|
|
||||||
|
$SUDO apt-get install -y --no-install-recommends \
|
||||||
|
python \
|
||||||
|
python-dev \
|
||||||
|
$virtualenv \
|
||||||
|
gcc \
|
||||||
|
dialog \
|
||||||
|
$augeas_pkg \
|
||||||
|
libssl-dev \
|
||||||
|
libffi-dev \
|
||||||
|
ca-certificates \
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if ! command -v virtualenv > /dev/null ; then
|
||||||
|
echo Failed to install a working \"virtualenv\" command, exiting
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
BootstrapFreeBsd() {
|
||||||
|
"$SUDO" pkg install -Ay \
|
||||||
|
python \
|
||||||
|
py27-virtualenv \
|
||||||
|
augeas \
|
||||||
|
libffi
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
BootstrapGentooCommon() {
|
||||||
|
PACKAGES="
|
||||||
|
dev-lang/python:2.7
|
||||||
|
dev-python/virtualenv
|
||||||
|
dev-util/dialog
|
||||||
|
app-admin/augeas
|
||||||
|
dev-libs/openssl
|
||||||
|
dev-libs/libffi
|
||||||
|
app-misc/ca-certificates
|
||||||
|
virtual/pkgconfig"
|
||||||
|
|
||||||
|
case "$PACKAGE_MANAGER" in
|
||||||
|
(paludis)
|
||||||
|
"$SUDO" cave resolve --keep-targets if-possible $PACKAGES -x
|
||||||
|
;;
|
||||||
|
(pkgcore)
|
||||||
|
"$SUDO" pmerge --noreplace $PACKAGES
|
||||||
|
;;
|
||||||
|
(portage|*)
|
||||||
|
"$SUDO" emerge --noreplace $PACKAGES
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
BootstrapMac() {
|
||||||
|
if ! hash brew 2>/dev/null; then
|
||||||
|
echo "Homebrew Not Installed\nDownloading..."
|
||||||
|
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
brew install augeas
|
||||||
|
brew install dialog
|
||||||
|
|
||||||
|
if ! hash pip 2>/dev/null; then
|
||||||
|
echo "pip Not Installed\nInstalling python from Homebrew..."
|
||||||
|
brew install python
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! hash virtualenv 2>/dev/null; then
|
||||||
|
echo "virtualenv Not Installed\nInstalling with pip"
|
||||||
|
pip install virtualenv
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
BootstrapRpmCommon() {
|
||||||
|
# Tested with:
|
||||||
|
# - Fedora 22, 23 (x64)
|
||||||
|
# - Centos 7 (x64: on DigitalOcean droplet)
|
||||||
|
|
||||||
|
if type dnf 2>/dev/null
|
||||||
|
then
|
||||||
|
tool=dnf
|
||||||
|
elif type yum 2>/dev/null
|
||||||
|
then
|
||||||
|
tool=yum
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "Neither yum nor dnf found. Aborting bootstrap!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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
|
||||||
|
then
|
||||||
|
if ! $SUDO $tool install -y \
|
||||||
|
python27 \
|
||||||
|
python27-devel \
|
||||||
|
python27-virtualenv
|
||||||
|
then
|
||||||
|
echo "Could not install Python dependencies. Aborting bootstrap!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
BootstrapSuseCommon() {
|
||||||
|
# SLE12 don't have python-virtualenv
|
||||||
|
|
||||||
|
$SUDO zypper -nq in -l \
|
||||||
|
python \
|
||||||
|
python-devel \
|
||||||
|
python-virtualenv \
|
||||||
|
gcc \
|
||||||
|
dialog \
|
||||||
|
augeas-lenses \
|
||||||
|
libopenssl-devel \
|
||||||
|
libffi-devel \
|
||||||
|
ca-certificates
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
"""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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from distutils.version import LooseVersion
|
||||||
|
from json import loads
|
||||||
|
from os import devnull, environ
|
||||||
|
from os.path import dirname, join
|
||||||
|
import re
|
||||||
|
from subprocess import check_call, CalledProcessError
|
||||||
|
from sys import argv, exit
|
||||||
|
from urllib2 import build_opener, HTTPHandler, HTTPSHandler, HTTPError
|
||||||
|
|
||||||
|
PUBLIC_KEY = environ.get('LE_AUTO_PUBLIC_KEY', """-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6MR8W/galdxnpGqBsYbq
|
||||||
|
OzQb2eyW15YFjDDEMI0ZOzt8f504obNs920lDnpPD2/KqgsfjOgw2K7xWDJIj/18
|
||||||
|
xUvWPk3LDkrnokNiRkA3KOx3W6fHycKL+zID7zy+xZYBuh2fLyQtWV1VGQ45iNRp
|
||||||
|
9+Zo7rH86cdfgkdnWTlNSHyTLW9NbXvyv/E12bppPcEvgCTAQXgnDVJ0/sqmeiij
|
||||||
|
n9tTFh03aM+R2V/21h8aTraAS24qiPCz6gkmYGC8yr6mglcnNoYbsLNYZ69zF1XH
|
||||||
|
cXPduCPdPdfLlzVlKK1/U7hkA28eG3BIAMh6uJYBRJTpiGgaGdPd7YekUB8S6cy+
|
||||||
|
CQIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
|
""")
|
||||||
|
|
||||||
|
class ExpectedError(Exception):
|
||||||
|
"""A novice-readable exception that also carries the original exception for
|
||||||
|
debugging"""
|
||||||
|
|
||||||
|
|
||||||
|
class HttpsGetter(object):
|
||||||
|
def __init__(self):
|
||||||
|
"""Build an HTTPS opener."""
|
||||||
|
# Based on pip 1.4.1's URLOpener
|
||||||
|
# This verifies certs on only Python >=2.7.9.
|
||||||
|
self._opener = build_opener(HTTPSHandler())
|
||||||
|
# Strip out HTTPHandler to prevent MITM spoof:
|
||||||
|
for handler in self._opener.handlers:
|
||||||
|
if isinstance(handler, HTTPHandler):
|
||||||
|
self._opener.handlers.remove(handler)
|
||||||
|
|
||||||
|
def get(self, url):
|
||||||
|
"""Return the document contents pointed to by an HTTPS URL.
|
||||||
|
|
||||||
|
If something goes wrong (404, timeout, etc.), raise ExpectedError.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self._opener.open(url).read()
|
||||||
|
except (HTTPError, IOError) as exc:
|
||||||
|
raise ExpectedError("Couldn't download %s." % url, exc)
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""Return the latest stable release of letsencrypt."""
|
||||||
|
metadata = loads(get(
|
||||||
|
environ.get('LE_AUTO_JSON_URL',
|
||||||
|
'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_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
|
||||||
|
with the verification process, raise ExpectedError.
|
||||||
|
|
||||||
|
"""
|
||||||
|
le_auto_dir = environ.get(
|
||||||
|
'LE_AUTO_DIR_TEMPLATE',
|
||||||
|
'https://raw.githubusercontent.com/letsencrypt/letsencrypt/%s/'
|
||||||
|
'letsencrypt-auto-source/') % tag
|
||||||
|
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_dir, 'public_key.pem'),
|
||||||
|
'-signature',
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
get = HttpsGetter().get
|
||||||
|
flag = argv[1]
|
||||||
|
try:
|
||||||
|
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
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
exit(main())
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
# This is the flattened list of packages letsencrypt-auto installs. To generate
|
||||||
|
# this, do `pip install -r -e acme -e . -e letsencrypt-apache`, `pip freeze`,
|
||||||
|
# and then gather the hashes.
|
||||||
|
|
||||||
|
# sha256: wxZH7baf09RlqEfqMVfTe-0flfGXYLEaR6qRwEtmYxQ
|
||||||
|
# sha256: YrCJpVvh2JSc0rx-DfC9254Cj678jDIDjMhIYq791uQ
|
||||||
|
argparse==1.4.0
|
||||||
|
|
||||||
|
# sha256: U8HJ3bMEMVE-t_PN7wo-BrDxJSGIqqd0SvD1pM1F268
|
||||||
|
# sha256: pWj0nfyhKo2fNwGHJX78WKOBCeHu5xTZKFYdegGKZPg
|
||||||
|
# sha256: gJxsqM-8ruv71DK0V2ABtA04_yRjdzy1dXfXXhoCC8M
|
||||||
|
# sha256: hs3KLNnLpBQiIwOQ3xff6qnzRKkR45dci-naV7NVSOk
|
||||||
|
# sha256: JLE9uErsOFyiPHuN7YPvi7QXe8GB0UdY-fl1vl0CDYY
|
||||||
|
# sha256: lprv_XwOCX9r4e_WgsFWriJlkaB5OpS2wtXkKT9MjU4
|
||||||
|
# sha256: AA81jUsPokn-qrnBzn1bL-fgLnvfaAbCZBhQX8aF4mg
|
||||||
|
# sha256: qdhvRgu9g1ii1ROtd54_P8h447k6ALUAL66_YW_-a5w
|
||||||
|
# sha256: MSezqzPrI8ysBx-aCAJ0jlz3xcvNAkgrsGPjW0HbsLA
|
||||||
|
# sha256: 4rLUIjZGmkAiTTnntsYFdfOIsvQj81TH7pClt_WMgGU
|
||||||
|
# sha256: jC3Mr-6JsbQksL7GrS3ZYiyUnSAk6Sn12h7YAerHXx0
|
||||||
|
# sha256: pN56TRGu1Ii6tPsU9JiFh6gpvs5aIEM_eA1uM7CAg8s
|
||||||
|
# sha256: XKj-MEJSZaSSdOSwITobyY9LE0Sa5elvmEdx5dg-WME
|
||||||
|
# sha256: pP04gC9Z5xTrqBoCT2LbcQsn2-J6fqEukRU3MnqoTTA
|
||||||
|
# sha256: hs1pErvIPpQF1Kc81_S07oNTZS0tvHyCAQbtW00bqzo
|
||||||
|
# sha256: jx0XfTZOo1kAQVriTKPkcb49UzTtBBkpQGjEn0WROZg
|
||||||
|
cffi==1.4.2
|
||||||
|
|
||||||
|
# sha256: O1CoPdWBSd_O6Yy2VlJl0QtT6cCivKfu73-19VJIkKc
|
||||||
|
ConfigArgParse==0.10.0
|
||||||
|
|
||||||
|
# sha256: ovVlB3DhyH-zNa8Zqbfrc_wFzPIhROto230AzSvLCQI
|
||||||
|
configobj==5.0.6
|
||||||
|
|
||||||
|
# sha256: 1U_hszrB4J8cEj4vl0948z6V1h1PSALdISIKXD6MEX0
|
||||||
|
# sha256: B1X2aE4RhSAFs2MTdh7ctbqEOmTNAizhrC3L1JqTYG0
|
||||||
|
# sha256: zjhNo4lZlluh90VKJfVp737yqxRd8ueiml4pS3TgRnc
|
||||||
|
# sha256: GvQDkV3LmWHDB2iuZRr6tpKC0dpaut-mN1IhrBGHdQM
|
||||||
|
# sha256: ag08d91PH-W8ZfJ--3fsjQSjiNpesl66DiBAwJgZ30o
|
||||||
|
# sha256: KdelgcO6_wTh--IAaltHjZ7cfPmib8ijWUkkf09lA3k
|
||||||
|
# sha256: IPAWEKpAh_bVadjMIMR4uB8DhIYnWqqx3Dx12VAsZ-A
|
||||||
|
# sha256: l9hGUIulDVomml82OK4cFmWbNTFaH0B_oVF2cH2j0Jc
|
||||||
|
# sha256: djfqRMLL1NsvLKccsmtmPRczORqnafi8g2xZVilbd5g
|
||||||
|
# sha256: gR-eqJVbPquzLgQGU0XDB4Ui5rPuPZLz0n08fNcWpjM
|
||||||
|
# sha256: DXCMjYz97Qm4fCoLqHY856ZjWG4EPmrEL9eDHpKQHLY
|
||||||
|
# sha256: Efnq11YqPgATWGytM5o_em9Yg8zhw7S5jhrGnft3p_Y
|
||||||
|
# sha256: dNhnm55-0ePs-wq1NNyTUruxz3PTYsmQkJTAlyivqJY
|
||||||
|
# sha256: z1Hd-123eBaiB1OKZgEUuC4w4IAD_uhJmwILi4SA2sU
|
||||||
|
# sha256: 47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
|
||||||
|
# sha256: dITvgYGUFB3_eUdf-74vd6-FHiw7v-Lk1ZEjEi-KTjM
|
||||||
|
# sha256: 7gLB6J7l7pUBV6VK1YTXN8Ec83putMCFPozz8n6WLcA
|
||||||
|
# sha256: pfGPaxhQpVVKV9v2YsrSUSpGBW5paHJqmFjngN1bnQo
|
||||||
|
# sha256: 26GA8xrb5xi6qdbPirY0hJSwlLK4GAL_8zvVDSfRPnM
|
||||||
|
# sha256: 5RinlLjzjoOC9_B3kUGBPOtIE6z9MRVBwNsOGJ69eN4
|
||||||
|
# sha256: f1FFn4TWcERCdeYVg59FQsk1R6Euk4oKSQba_l994VM
|
||||||
|
cryptography==1.1.2
|
||||||
|
|
||||||
|
# sha256: JHXX_N31lR6S_1RpcnWIAt5SYL9Akxmp8ZNOa7yLHcc
|
||||||
|
# sha256: NZB977D5krdat3iPZf7cHPIP-iJojg5vbxKvwGs-pQE
|
||||||
|
enum34==1.1.2
|
||||||
|
|
||||||
|
# sha256: _1rZ4vjZ5dHou_vPR3IqtSfPDVHK7u2dptD0B5k4P94
|
||||||
|
# sha256: 2Dzm3wsOpmGHAP4ds1NSY5Goo62ht6ulL-16Ydp3IDM
|
||||||
|
funcsigs==0.4
|
||||||
|
|
||||||
|
# sha256: my_FC9PEujBrllG2lBHvIgJtTYM1uTr8IhTO8SRs5wc
|
||||||
|
# sha256: FhmarZOLKQ9b4QV8Dh78ZUYik5HCPOphypQMEV99PTs
|
||||||
|
idna==2.0
|
||||||
|
|
||||||
|
# sha256: k1cSgAzkdgcB2JrWd2Zs1SaR_S9vCzQMi0I5o8F5iKU
|
||||||
|
# sha256: WjGCsyKnBlJcRigspvBk0noCz_vUSfn0dBbx3JaqcbA
|
||||||
|
ipaddress==1.0.16
|
||||||
|
|
||||||
|
# sha256: 6MFV_evZxLywgQtO0BrhmHVUse4DTddTLXuP2uOKYnQ
|
||||||
|
ndg-httpsclient==0.4.0
|
||||||
|
|
||||||
|
# sha256: OnTxAPkNZZGDFf5kkHca0gi8PxOv0y01_P5OjQs7gSs
|
||||||
|
# sha256: Paa-K-UG9ZzOMuGeMOIBBT4btNB-JWaJGOAPikmtQKs
|
||||||
|
parsedatetime==1.5
|
||||||
|
|
||||||
|
# sha256: Rsjbda51oFa9HMB_ohc0_i5gPRGgeDPswe63TDXHLgw
|
||||||
|
# sha256: 4hJ2JqkebIhduJZol22zECDwry2nKJJLVkgPx8zwlkk
|
||||||
|
pbr==1.8.1
|
||||||
|
|
||||||
|
# sha256: WE8LKfzF1SO0M8uJGLL8dNZ-MO4LRKlbrwMVKPQkYZ8
|
||||||
|
# sha256: KMoLbp2Zqo3Chuh0ekRxNitpgSolKR3im2qNcKFUWg0
|
||||||
|
# sha256: FnrV__UqZyxN3BwaCyUUbWgT67CKmqsKOsRfiltmnDs
|
||||||
|
# sha256: 5t6mFzqYhye7Ij00lzSa1c3vXAsoLv8tg-X5BlxT-F8
|
||||||
|
# sha256: KvXgpKrWYEmVXQc0qk49yMqhep6vi0waJ6Xx7m5A9vw
|
||||||
|
# sha256: 2YhNwNwuVeJEjklXeNyYmcHIvzeusvQ0wb6nSvk8JoM
|
||||||
|
# sha256: 4nwv5t_Mhzi-PSxaAi94XrcpcQV-Gp4eNPunO86KcaY
|
||||||
|
# sha256: Za_W_syPOu0J7kvmNYO8jrRy8GzqpP4kxNHVoaPA4T8
|
||||||
|
# sha256: uhxVj7_N-UUVwjlLEVXB3FbivCqcF9MDSYJ8ntimfkY
|
||||||
|
# sha256: upXqACLctk028MEzXAYF-uNb3z4P6o2S9dD2RWo15Vs
|
||||||
|
# sha256: QhtlkdFrUJqqjYwVgh1mu5TLSo3EOFytXFG4XUoJbYU
|
||||||
|
# sha256: MmswXL22-U2vv-LCaxHaiLCrB7igf4GIq511_wxuhBo
|
||||||
|
# sha256: mu3lsrb-RrN0jqjlIURDiQ0WNAJ77z0zt9rRZVaDAng
|
||||||
|
# sha256: c77R24lNGqnDx-YR0wLN6reuig3A7q92cnh42xrFzYc
|
||||||
|
# sha256: k1td1tVYr1EvQlAafAj0HXr_E5rxuzlZ2qOruFkjTWw
|
||||||
|
# sha256: TKARHPFX3MDy9poyPFtUeHGNaNRfyUNdhL4OwPGGIVs
|
||||||
|
# sha256: tvE8lTmKP88CJsTc-kSFYLpYZSWc2W7CgQZYZR6TIYk
|
||||||
|
# sha256: 7mvjDRY1u96kxDJdUH3IoNu95-HBmL1i3bn0MZi54hQ
|
||||||
|
# sha256: 36eGhYwmjX-74bYXXgAewCc418-uCnzne_m2Ua9nZyk
|
||||||
|
# sha256: qnf53nKvnBbMKIzUokz1iCQ4j1fXqB5ADEYWRXYphw4
|
||||||
|
# sha256: 9QAJM1fQTagUDYeTLKwuVO9ZKlTKinQ6uyhQ9gwsIus
|
||||||
|
psutil==3.3.0
|
||||||
|
|
||||||
|
# sha256: YfnZnjzvZf6xv-Oi7vepPrk4GdNFv1S81C9OY9UgTa4
|
||||||
|
# sha256: GAKm3TIEXkcqQZ2xRBrsq0adM-DSdJ4ZKr3sUhAXJK8
|
||||||
|
# sha256: NQJc2UIsllBJEvBOLxX-eTkKhZe0MMLKXQU0z5MJ_6A
|
||||||
|
# sha256: L5btWgwynKFiMLMmyhK3Rh7I9l4L4-T5l1FvNr-Co0U
|
||||||
|
# sha256: KP7kQheZHPrZ5qC59-PyYEHiHryWYp6U5YXM0F1J-mU
|
||||||
|
# sha256: Mm56hUoX-rB2kSBHR2lfj2ktZ0WIo1XEQfsU9mC_Tmg
|
||||||
|
# sha256: zaWpBIVwnKZ5XIYFbD5f5yZgKLBeU_HVJ_35OmNlprg
|
||||||
|
# sha256: DLKhR0K1Q_3Wj5MaFM44KRhu0rGyJnoGeHOIyWst2b4
|
||||||
|
# sha256: UZH_a5Em0sA53Yf4_wJb7SdLrwf6eK-kb1VrGtcmXW4
|
||||||
|
# sha256: gyPgNjey0HLMcEEwC6xuxEjDwolQq0A3YDZ4jpoa9ik
|
||||||
|
# sha256: hTys2W0fcB3dZ6oD7MBfUYkBNbcmLpInEBEvEqLtKn8
|
||||||
|
pyasn1==0.1.9
|
||||||
|
|
||||||
|
# sha256: eVm0p0q9wnsxL-0cIebK-TCc4LKeqGtZH9Lpns3yf3M
|
||||||
|
pycparser==2.14
|
||||||
|
|
||||||
|
# sha256: iORea7Jd_tJyoe8ucoRh1EtjTCzWiemJtuVqNJxaOuU
|
||||||
|
# sha256: 8KJgcNbbCIHei8x4RpNLfDyTDY-cedRYg-5ImEvA1nI
|
||||||
|
pyOpenSSL==0.15.1
|
||||||
|
|
||||||
|
# sha256: 7qMYNcVuIJavQ2OldFp4SHimHQQ-JH06bWoKMql0H1Y
|
||||||
|
# sha256: jfvGxFi42rocDzYgqMeACLMjomiye3NZ6SpK5BMl9TU
|
||||||
|
pyRFC3339==1.0
|
||||||
|
|
||||||
|
# sha256: Z9WdZs26jWJOA4m4eyqDoXbyHxaodVO1D1cDsj8pusI
|
||||||
|
python-augeas==0.5.0
|
||||||
|
|
||||||
|
# sha256: BOk_JJlcQ92Q8zjV2GXKcs4_taU1jU2qSWVXHbNfw-w
|
||||||
|
# sha256: Pm9ZP-rZj4pSa8PjBpM1MyNuM3KfVS9SiW6lBPVTE_o
|
||||||
|
python2-pythondialog==3.3.0
|
||||||
|
|
||||||
|
# sha256: Or5qbT_C-75MYBRCEfRdou2-MYKm9lEa9ru6BZix-ZI
|
||||||
|
# sha256: k575weEiTZgEBWial__PeCjFbRUXsx1zRkNWwfK3dp4
|
||||||
|
# sha256: 6tSu-nAHJJ4F5RsBCVcZ1ajdlXYAifVzCqxWmLGTKRg
|
||||||
|
# sha256: PMoN8IvQ7ZhDI5BJTOPe0AP15mGqRgvnpzS__jWYNgU
|
||||||
|
# sha256: Pt5HDT0XujwHY436DRBFK8G25a0yYSemW6d-aq6xG-w
|
||||||
|
# sha256: aMR5ZPcYbuwwaxNilidyK5B5zURH7Z5eyuzU6shMpzQ
|
||||||
|
# sha256: 3V05kZUKrkCmyB3hV4lC5z1imAjO_FHRLNFXmA5s_Bg
|
||||||
|
# sha256: p3xSBiwH63x7MFRdvHPjKZW34Rfup1Axe1y1x6RhjxQ
|
||||||
|
# sha256: ga-a7EvJYKmgEnxIjxh3La5GNGiSM_BvZUQ-exHr61E
|
||||||
|
# sha256: 4Hmx2txcBiRswbtv4bI6ULHRFz8u3VEE79QLtzoo9AY
|
||||||
|
# sha256: -9JnRncsJMuTyLl8va1cueRshrvbG52KdD7gDi-x_F0
|
||||||
|
# sha256: mSZu8wo35Dky3uwrfKc-g8jbw7n_cD7HPsprHa5r7-o
|
||||||
|
# sha256: i2zhyZOQl4O8luC0806iI7_3pN8skL25xODxrJKGieM
|
||||||
|
pytz==2015.7
|
||||||
|
|
||||||
|
# sha256: ET-7pVManjSUW302szoIToul0GZLcDyBp8Vy2RkZpbg
|
||||||
|
# sha256: xXeBXdAPE5QgP8ROuXlySwmPiCZKnviY7kW45enPWH8
|
||||||
|
requests==2.9.1
|
||||||
|
|
||||||
|
# sha256: D_eMQD2bzPWkJabTGhKqa0fxwhyk3CVzp-LzKpczXrE
|
||||||
|
# sha256: EF-NaGFvgkjiS_DpNy7wTTzBAQTxmA9U1Xss5zpa1Wo
|
||||||
|
six==1.10.0
|
||||||
|
|
||||||
|
# sha256: aUkbUwUVfDxuDwSnAZhNaud_1yn8HJrNJQd_HfOFMms
|
||||||
|
# sha256: 619wCpv8lkILBVY1r5AC02YuQ9gMP_0x8iTCW8DV9GI
|
||||||
|
Werkzeug==0.11.3
|
||||||
|
|
||||||
|
# sha256: KCwRK1XdjjyGmjVx-GdnwVCrEoSprOK97CJsWSrK-Bo
|
||||||
|
zope.component==4.2.2
|
||||||
|
|
||||||
|
# sha256: 3HpZov2Rcw03kxMaXSYbKek-xOKpfxvEh86N7-4v54Y
|
||||||
|
zope.event==4.1.0
|
||||||
|
|
||||||
|
# sha256: 8HtjH3pgHNjL0zMtVPQxQscIioMpn4WTVvCNHU1CWbM
|
||||||
|
# sha256: 3lzKCDuUOdgAL7drvmtJmMWlpyH6sluEKYln8ALfTJQ
|
||||||
|
# sha256: Z4hBb36n9bipe-lIJTd6ol6L3HNGPge6r5hYsp5zcHc
|
||||||
|
# sha256: bzIw9yVFGCAeWjcIy7LemMhIME8G497Yv7OeWCXLouE
|
||||||
|
# sha256: X6V1pSQPBCAMMIhCfQ1Le3N_bpAYgYpR2ND5J6aiUXo
|
||||||
|
# sha256: UiGUrWpUVzXt11yKg_SNZdGvBk5DKn0yDWT1a6_BLpk
|
||||||
|
# sha256: 6Mey1AlD9xyZFIyX9myqf1E0FH9XQj-NtbSCUJnOmgk
|
||||||
|
# sha256: J5Ak8CCGAcPKqQfFOHbjetiGJffq8cs4QtvjYLIocBc
|
||||||
|
# sha256: LiIanux8zFiImieOoT3P7V75OdgLB4Gamos8scaBSE8
|
||||||
|
# sha256: aRGJZUEOyG1E3GuQF-4929WC4MCr7vYrOhnb9sitEys
|
||||||
|
# sha256: 0E34aG7IZNDK3ozxmff4OuzUFhCaIINNVo-DEN7RLeo
|
||||||
|
# sha256: 51qUfhXul-fnHgLqMC_rL8YtOiu0Zov5377UOlBqx-c
|
||||||
|
# sha256: TkXSL7iDIipaufKCoRb-xe4ujRpWjM_2otdbvQ62vPw
|
||||||
|
# sha256: vOkzm7PHpV4IA7Y9IcWDno5Hm8hcSt9CrkFbcvlPrLI
|
||||||
|
# sha256: koE4NlJFoOiGmlmZ-8wqRUdaCm7VKklNYNvcVAM1_t0
|
||||||
|
# sha256: DYQbobuEDuoOZIncXsr6YSVVSXH1O1rLh3ZEQeYbzro
|
||||||
|
# sha256: sJyMHUezUxxADgGVaX8UFKYyId5u9HhZik8UYPfZo5I
|
||||||
|
zope.interface==4.1.3
|
||||||
|
|
||||||
|
# sha256: fYwCUXn3Wd_tKYHuPfufzDQZuDNng0HZb_th3xepC7U
|
||||||
|
# sha256: dKkCf9CZnKaDTFOof-KoazoewjKTSAVxZUJmnj_3i_U
|
||||||
|
acme==0.2.0
|
||||||
|
|
||||||
|
# sha256: 4x7K5lzKwm_GjYMojvUh053qL4EfIC5hGFmW370-7jI
|
||||||
|
# sha256: kcm3VmxXIGNS7ShcKFnYdA9AfXnqcbV_otMsADr1p2A
|
||||||
|
letsencrypt==0.2.0
|
||||||
|
|
||||||
|
# sha256: AKuIT6b7gXXD2Cs7Qoem8ZrxcqBjABz1IgxhHGxmwX0
|
||||||
|
# sha256: Cak7i4RaDsZixQMXWWpW-blTHaak09l94aLi9v7lljs
|
||||||
|
letsencrypt-apache==0.2.0
|
||||||
|
|
||||||
|
# sha256: uDndLZwRfHAUMMFJlWkYpCOphjtIsJyQ4wpgE-fS9E8
|
||||||
|
# sha256: j4MIDaoknQNsvM-4rlzG_wB7iNbZN1ITca-r57Gbrbw
|
||||||
|
mock==1.0.1
|
||||||
Executable
+959
@@ -0,0 +1,959 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""peep ("prudently examine every package") verifies that packages conform to a
|
||||||
|
trusted, locally stored hash and only then installs them::
|
||||||
|
|
||||||
|
peep install -r requirements.txt
|
||||||
|
|
||||||
|
This makes your deployments verifiably repeatable without having to maintain a
|
||||||
|
local PyPI mirror or use a vendor lib. Just update the version numbers and
|
||||||
|
hashes in requirements.txt, and you're all set.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# This is here so embedded copies of peep.py are MIT-compliant:
|
||||||
|
# Copyright (c) 2013 Erik Rose
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to
|
||||||
|
# deal in the Software without restriction, including without limitation the
|
||||||
|
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
# sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
from __future__ import print_function
|
||||||
|
try:
|
||||||
|
xrange = xrange
|
||||||
|
except NameError:
|
||||||
|
xrange = range
|
||||||
|
from base64 import urlsafe_b64encode, urlsafe_b64decode
|
||||||
|
from binascii import hexlify
|
||||||
|
import cgi
|
||||||
|
from collections import defaultdict
|
||||||
|
from functools import wraps
|
||||||
|
from hashlib import sha256
|
||||||
|
from itertools import chain, islice
|
||||||
|
import mimetypes
|
||||||
|
from optparse import OptionParser
|
||||||
|
from os.path import join, basename, splitext, isdir
|
||||||
|
from pickle import dumps, loads
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from shutil import rmtree, copy
|
||||||
|
from sys import argv, exit
|
||||||
|
from tempfile import mkdtemp
|
||||||
|
import traceback
|
||||||
|
try:
|
||||||
|
from urllib2 import build_opener, HTTPHandler, HTTPSHandler, HTTPError
|
||||||
|
except ImportError:
|
||||||
|
from urllib.request import build_opener, HTTPHandler, HTTPSHandler
|
||||||
|
from urllib.error import HTTPError
|
||||||
|
try:
|
||||||
|
from urlparse import urlparse
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import urlparse # 3.4
|
||||||
|
# TODO: Probably use six to make urllib stuff work across 2/3.
|
||||||
|
|
||||||
|
from pkg_resources import require, VersionConflict, DistributionNotFound
|
||||||
|
|
||||||
|
# We don't admit our dependency on pip in setup.py, lest a naive user simply
|
||||||
|
# say `pip install peep.tar.gz` and thus pull down an untrusted copy of pip
|
||||||
|
# from PyPI. Instead, we make sure it's installed and new enough here and spit
|
||||||
|
# out an error message if not:
|
||||||
|
|
||||||
|
|
||||||
|
def activate(specifier):
|
||||||
|
"""Make a compatible version of pip importable. Raise a RuntimeError if we
|
||||||
|
couldn't."""
|
||||||
|
try:
|
||||||
|
for distro in require(specifier):
|
||||||
|
distro.activate()
|
||||||
|
except (VersionConflict, DistributionNotFound):
|
||||||
|
raise RuntimeError('The installed version of pip is too old; peep '
|
||||||
|
'requires ' + specifier)
|
||||||
|
|
||||||
|
# Before 0.6.2, the log module wasn't there, so some
|
||||||
|
# of our monkeypatching fails. It probably wouldn't be
|
||||||
|
# much work to support even earlier, though.
|
||||||
|
activate('pip>=0.6.2')
|
||||||
|
|
||||||
|
import pip
|
||||||
|
from pip.commands.install import InstallCommand
|
||||||
|
try:
|
||||||
|
from pip.download import url_to_path # 1.5.6
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from pip.util import url_to_path # 0.7.0
|
||||||
|
except ImportError:
|
||||||
|
from pip.util import url_to_filename as url_to_path # 0.6.2
|
||||||
|
from pip.index import PackageFinder, Link
|
||||||
|
try:
|
||||||
|
from pip.log import logger
|
||||||
|
except ImportError:
|
||||||
|
from pip import logger # 6.0
|
||||||
|
from pip.req import parse_requirements
|
||||||
|
try:
|
||||||
|
from pip.utils.ui import DownloadProgressBar, DownloadProgressSpinner
|
||||||
|
except ImportError:
|
||||||
|
class NullProgressBar(object):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def iter(self, ret, *args, **kwargs):
|
||||||
|
return ret
|
||||||
|
|
||||||
|
DownloadProgressBar = DownloadProgressSpinner = NullProgressBar
|
||||||
|
|
||||||
|
__version__ = 2, 5, 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pip.index import FormatControl # noqa
|
||||||
|
FORMAT_CONTROL_ARG = 'format_control'
|
||||||
|
|
||||||
|
# The line-numbering bug will be fixed in pip 8. All 7.x releases had it.
|
||||||
|
PIP_MAJOR_VERSION = int(pip.__version__.split('.')[0])
|
||||||
|
PIP_COUNTS_COMMENTS = PIP_MAJOR_VERSION >= 8
|
||||||
|
except ImportError:
|
||||||
|
FORMAT_CONTROL_ARG = 'use_wheel' # pre-7
|
||||||
|
PIP_COUNTS_COMMENTS = True
|
||||||
|
|
||||||
|
|
||||||
|
ITS_FINE_ITS_FINE = 0
|
||||||
|
SOMETHING_WENT_WRONG = 1
|
||||||
|
# "Traditional" for command-line errors according to optparse docs:
|
||||||
|
COMMAND_LINE_ERROR = 2
|
||||||
|
|
||||||
|
ARCHIVE_EXTENSIONS = ('.tar.bz2', '.tar.gz', '.tgz', '.tar', '.zip')
|
||||||
|
|
||||||
|
MARKER = object()
|
||||||
|
|
||||||
|
|
||||||
|
class PipException(Exception):
|
||||||
|
"""When I delegated to pip, it exited with an error."""
|
||||||
|
|
||||||
|
def __init__(self, error_code):
|
||||||
|
self.error_code = error_code
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedRequirementError(Exception):
|
||||||
|
"""An unsupported line was encountered in a requirements file."""
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadError(Exception):
|
||||||
|
def __init__(self, link, exc):
|
||||||
|
self.link = link
|
||||||
|
self.reason = str(exc)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'Downloading %s failed: %s' % (self.link, self.reason)
|
||||||
|
|
||||||
|
|
||||||
|
def encoded_hash(sha):
|
||||||
|
"""Return a short, 7-bit-safe representation of a hash.
|
||||||
|
|
||||||
|
If you pass a sha256, this results in the hash algorithm that the Wheel
|
||||||
|
format (PEP 427) uses, except here it's intended to be run across the
|
||||||
|
downloaded archive before unpacking.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return urlsafe_b64encode(sha.digest()).decode('ascii').rstrip('=')
|
||||||
|
|
||||||
|
|
||||||
|
def path_and_line(req):
|
||||||
|
"""Return the path and line number of the file from which an
|
||||||
|
InstallRequirement came.
|
||||||
|
|
||||||
|
"""
|
||||||
|
path, line = (re.match(r'-r (.*) \(line (\d+)\)$',
|
||||||
|
req.comes_from).groups())
|
||||||
|
return path, int(line)
|
||||||
|
|
||||||
|
|
||||||
|
def hashes_above(path, line_number):
|
||||||
|
"""Yield hashes from contiguous comment lines before line ``line_number``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def hash_lists(path):
|
||||||
|
"""Yield lists of hashes appearing between non-comment lines.
|
||||||
|
|
||||||
|
The lists will be in order of appearance and, for each non-empty
|
||||||
|
list, their place in the results will coincide with that of the
|
||||||
|
line number of the corresponding result from `parse_requirements`
|
||||||
|
(which changed in pip 7.0 to not count comments).
|
||||||
|
|
||||||
|
"""
|
||||||
|
hashes = []
|
||||||
|
with open(path) as file:
|
||||||
|
for lineno, line in enumerate(file, 1):
|
||||||
|
match = HASH_COMMENT_RE.match(line)
|
||||||
|
if match: # Accumulate this hash.
|
||||||
|
hashes.append(match.groupdict()['hash'])
|
||||||
|
if not IGNORED_LINE_RE.match(line):
|
||||||
|
yield hashes # Report hashes seen so far.
|
||||||
|
hashes = []
|
||||||
|
elif PIP_COUNTS_COMMENTS:
|
||||||
|
# Comment: count as normal req but have no hashes.
|
||||||
|
yield []
|
||||||
|
|
||||||
|
return next(islice(hash_lists(path), line_number - 1, None))
|
||||||
|
|
||||||
|
|
||||||
|
def run_pip(initial_args):
|
||||||
|
"""Delegate to pip the given args (starting with the subcommand), and raise
|
||||||
|
``PipException`` if something goes wrong."""
|
||||||
|
status_code = pip.main(initial_args)
|
||||||
|
|
||||||
|
# Clear out the registrations in the pip "logger" singleton. Otherwise,
|
||||||
|
# loggers keep getting appended to it with every run. Pip assumes only one
|
||||||
|
# command invocation will happen per interpreter lifetime.
|
||||||
|
logger.consumers = []
|
||||||
|
|
||||||
|
if status_code:
|
||||||
|
raise PipException(status_code)
|
||||||
|
|
||||||
|
|
||||||
|
def hash_of_file(path):
|
||||||
|
"""Return the hash of a downloaded file."""
|
||||||
|
with open(path, 'rb') as archive:
|
||||||
|
sha = sha256()
|
||||||
|
while True:
|
||||||
|
data = archive.read(2 ** 20)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
sha.update(data)
|
||||||
|
return encoded_hash(sha)
|
||||||
|
|
||||||
|
|
||||||
|
def is_git_sha(text):
|
||||||
|
"""Return whether this is probably a git sha"""
|
||||||
|
# Handle both the full sha as well as the 7-character abbreviation
|
||||||
|
if len(text) in (40, 7):
|
||||||
|
try:
|
||||||
|
int(text, 16)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def filename_from_url(url):
|
||||||
|
parsed = urlparse(url)
|
||||||
|
path = parsed.path
|
||||||
|
return path.split('/')[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def requirement_args(argv, want_paths=False, want_other=False):
|
||||||
|
"""Return an iterable of filtered arguments.
|
||||||
|
|
||||||
|
:arg argv: Arguments, starting after the subcommand
|
||||||
|
:arg want_paths: If True, the returned iterable includes the paths to any
|
||||||
|
requirements files following a ``-r`` or ``--requirement`` option.
|
||||||
|
:arg want_other: If True, the returned iterable includes the args that are
|
||||||
|
not a requirement-file path or a ``-r`` or ``--requirement`` flag.
|
||||||
|
|
||||||
|
"""
|
||||||
|
was_r = False
|
||||||
|
for arg in argv:
|
||||||
|
# Allow for requirements files named "-r", don't freak out if there's a
|
||||||
|
# trailing "-r", etc.
|
||||||
|
if was_r:
|
||||||
|
if want_paths:
|
||||||
|
yield arg
|
||||||
|
was_r = False
|
||||||
|
elif arg in ['-r', '--requirement']:
|
||||||
|
was_r = True
|
||||||
|
else:
|
||||||
|
if want_other:
|
||||||
|
yield arg
|
||||||
|
|
||||||
|
# any line that is a comment or just whitespace
|
||||||
|
IGNORED_LINE_RE = re.compile(r'^(\s*#.*)?\s*$')
|
||||||
|
|
||||||
|
HASH_COMMENT_RE = re.compile(
|
||||||
|
r"""
|
||||||
|
\s*\#\s+ # Lines that start with a '#'
|
||||||
|
(?P<hash_type>sha256):\s+ # Hash type is hardcoded to be sha256 for now.
|
||||||
|
(?P<hash>[^\s]+) # Hashes can be anything except '#' or spaces.
|
||||||
|
\s* # Suck up whitespace before the comment or
|
||||||
|
# just trailing whitespace if there is no
|
||||||
|
# comment. Also strip trailing newlines.
|
||||||
|
(?:\#(?P<comment>.*))? # Comments can be anything after a whitespace+#
|
||||||
|
# and are optional.
|
||||||
|
$""", re.X)
|
||||||
|
|
||||||
|
|
||||||
|
def peep_hash(argv):
|
||||||
|
"""Return the peep hash of one or more files, returning a shell status code
|
||||||
|
or raising a PipException.
|
||||||
|
|
||||||
|
:arg argv: The commandline args, starting after the subcommand
|
||||||
|
|
||||||
|
"""
|
||||||
|
parser = OptionParser(
|
||||||
|
usage='usage: %prog hash file [file ...]',
|
||||||
|
description='Print a peep hash line for one or more files: for '
|
||||||
|
'example, "# sha256: '
|
||||||
|
'oz42dZy6Gowxw8AelDtO4gRgTW_xPdooH484k7I5EOY".')
|
||||||
|
_, paths = parser.parse_args(args=argv)
|
||||||
|
if paths:
|
||||||
|
for path in paths:
|
||||||
|
print('# sha256:', hash_of_file(path))
|
||||||
|
return ITS_FINE_ITS_FINE
|
||||||
|
else:
|
||||||
|
parser.print_usage()
|
||||||
|
return COMMAND_LINE_ERROR
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyOptions(object):
|
||||||
|
"""Fake optparse options for compatibility with pip<1.2
|
||||||
|
|
||||||
|
pip<1.2 had a bug in parse_requirements() in which the ``options`` kwarg
|
||||||
|
was required. We work around that by passing it a mock object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
default_vcs = None
|
||||||
|
skip_requirements_regex = None
|
||||||
|
isolated_mode = False
|
||||||
|
|
||||||
|
|
||||||
|
def memoize(func):
|
||||||
|
"""Memoize a method that should return the same result every time on a
|
||||||
|
given instance.
|
||||||
|
|
||||||
|
"""
|
||||||
|
@wraps(func)
|
||||||
|
def memoizer(self):
|
||||||
|
if not hasattr(self, '_cache'):
|
||||||
|
self._cache = {}
|
||||||
|
if func.__name__ not in self._cache:
|
||||||
|
self._cache[func.__name__] = func(self)
|
||||||
|
return self._cache[func.__name__]
|
||||||
|
return memoizer
|
||||||
|
|
||||||
|
|
||||||
|
def package_finder(argv):
|
||||||
|
"""Return a PackageFinder respecting command-line options.
|
||||||
|
|
||||||
|
:arg argv: Everything after the subcommand
|
||||||
|
|
||||||
|
"""
|
||||||
|
# We instantiate an InstallCommand and then use some of its private
|
||||||
|
# machinery--its arg parser--for our own purposes, like a virus. This
|
||||||
|
# approach is portable across many pip versions, where more fine-grained
|
||||||
|
# ones are not. Ignoring options that don't exist on the parser (for
|
||||||
|
# instance, --use-wheel) gives us a straightforward method of backward
|
||||||
|
# compatibility.
|
||||||
|
try:
|
||||||
|
command = InstallCommand()
|
||||||
|
except TypeError:
|
||||||
|
# This is likely pip 1.3.0's "__init__() takes exactly 2 arguments (1
|
||||||
|
# given)" error. In that version, InstallCommand takes a top=level
|
||||||
|
# parser passed in from outside.
|
||||||
|
from pip.baseparser import create_main_parser
|
||||||
|
command = InstallCommand(create_main_parser())
|
||||||
|
# The downside is that it essentially ruins the InstallCommand class for
|
||||||
|
# further use. Calling out to pip.main() within the same interpreter, for
|
||||||
|
# example, would result in arguments parsed this time turning up there.
|
||||||
|
# Thus, we deepcopy the arg parser so we don't trash its singletons. Of
|
||||||
|
# course, deepcopy doesn't work on these objects, because they contain
|
||||||
|
# uncopyable regex patterns, so we pickle and unpickle instead. Fun!
|
||||||
|
options, _ = loads(dumps(command.parser)).parse_args(argv)
|
||||||
|
|
||||||
|
# Carry over PackageFinder kwargs that have [about] the same names as
|
||||||
|
# options attr names:
|
||||||
|
possible_options = [
|
||||||
|
'find_links', FORMAT_CONTROL_ARG, 'allow_external', 'allow_unverified',
|
||||||
|
'allow_all_external', ('allow_all_prereleases', 'pre'),
|
||||||
|
'process_dependency_links']
|
||||||
|
kwargs = {}
|
||||||
|
for option in possible_options:
|
||||||
|
kw, attr = option if isinstance(option, tuple) else (option, option)
|
||||||
|
value = getattr(options, attr, MARKER)
|
||||||
|
if value is not MARKER:
|
||||||
|
kwargs[kw] = value
|
||||||
|
|
||||||
|
# Figure out index_urls:
|
||||||
|
index_urls = [options.index_url] + options.extra_index_urls
|
||||||
|
if options.no_index:
|
||||||
|
index_urls = []
|
||||||
|
index_urls += getattr(options, 'mirrors', [])
|
||||||
|
|
||||||
|
# If pip is new enough to have a PipSession, initialize one, since
|
||||||
|
# PackageFinder requires it:
|
||||||
|
if hasattr(command, '_build_session'):
|
||||||
|
kwargs['session'] = command._build_session(options)
|
||||||
|
|
||||||
|
return PackageFinder(index_urls=index_urls, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadedReq(object):
|
||||||
|
"""A wrapper around InstallRequirement which offers additional information
|
||||||
|
based on downloading and examining a corresponding package archive
|
||||||
|
|
||||||
|
These are conceptually immutable, so we can get away with memoizing
|
||||||
|
expensive things.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, req, argv, finder):
|
||||||
|
"""Download a requirement, compare its hashes, and return a subclass
|
||||||
|
of DownloadedReq depending on its state.
|
||||||
|
|
||||||
|
:arg req: The InstallRequirement I am based on
|
||||||
|
:arg argv: The args, starting after the subcommand
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._req = req
|
||||||
|
self._argv = argv
|
||||||
|
self._finder = finder
|
||||||
|
|
||||||
|
# We use a separate temp dir for each requirement so requirements
|
||||||
|
# (from different indices) that happen to have the same archive names
|
||||||
|
# don't overwrite each other, leading to a security hole in which the
|
||||||
|
# latter is a hash mismatch, the former has already passed the
|
||||||
|
# comparison, and the latter gets installed.
|
||||||
|
self._temp_path = mkdtemp(prefix='peep-')
|
||||||
|
# Think of DownloadedReq as a one-shot state machine. It's an abstract
|
||||||
|
# class that ratchets forward to being one of its own subclasses,
|
||||||
|
# depending on its package status. Then it doesn't move again.
|
||||||
|
self.__class__ = self._class()
|
||||||
|
|
||||||
|
def dispose(self):
|
||||||
|
"""Delete temp files and dirs I've made. Render myself useless.
|
||||||
|
|
||||||
|
Do not call further methods on me after calling dispose().
|
||||||
|
|
||||||
|
"""
|
||||||
|
rmtree(self._temp_path)
|
||||||
|
|
||||||
|
def _version(self):
|
||||||
|
"""Deduce the version number of the downloaded package from its filename."""
|
||||||
|
# TODO: Can we delete this method and just print the line from the
|
||||||
|
# reqs file verbatim instead?
|
||||||
|
def version_of_archive(filename, package_name):
|
||||||
|
# Since we know the project_name, we can strip that off the left, strip
|
||||||
|
# any archive extensions off the right, and take the rest as the
|
||||||
|
# version.
|
||||||
|
for ext in ARCHIVE_EXTENSIONS:
|
||||||
|
if filename.endswith(ext):
|
||||||
|
filename = filename[:-len(ext)]
|
||||||
|
break
|
||||||
|
# Handle github sha tarball downloads.
|
||||||
|
if is_git_sha(filename):
|
||||||
|
filename = package_name + '-' + filename
|
||||||
|
if not filename.lower().replace('_', '-').startswith(package_name.lower()):
|
||||||
|
# TODO: Should we replace runs of [^a-zA-Z0-9.], not just _, with -?
|
||||||
|
give_up(filename, package_name)
|
||||||
|
return filename[len(package_name) + 1:] # Strip off '-' before version.
|
||||||
|
|
||||||
|
def version_of_wheel(filename, package_name):
|
||||||
|
# For Wheel files (http://legacy.python.org/dev/peps/pep-0427/#file-
|
||||||
|
# name-convention) we know the format bits are '-' separated.
|
||||||
|
whl_package_name, version, _rest = filename.split('-', 2)
|
||||||
|
# Do the alteration to package_name from PEP 427:
|
||||||
|
our_package_name = re.sub(r'[^\w\d.]+', '_', package_name, re.UNICODE)
|
||||||
|
if whl_package_name != our_package_name:
|
||||||
|
give_up(filename, whl_package_name)
|
||||||
|
return version
|
||||||
|
|
||||||
|
def give_up(filename, package_name):
|
||||||
|
raise RuntimeError("The archive '%s' didn't start with the package name "
|
||||||
|
"'%s', so I couldn't figure out the version number. "
|
||||||
|
"My bad; improve me." %
|
||||||
|
(filename, package_name))
|
||||||
|
|
||||||
|
get_version = (version_of_wheel
|
||||||
|
if self._downloaded_filename().endswith('.whl')
|
||||||
|
else version_of_archive)
|
||||||
|
return get_version(self._downloaded_filename(), self._project_name())
|
||||||
|
|
||||||
|
def _is_always_unsatisfied(self):
|
||||||
|
"""Returns whether this requirement is always unsatisfied
|
||||||
|
|
||||||
|
This would happen in cases where we can't determine the version
|
||||||
|
from the filename.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# If this is a github sha tarball, then it is always unsatisfied
|
||||||
|
# because the url has a commit sha in it and not the version
|
||||||
|
# number.
|
||||||
|
url = self._url()
|
||||||
|
if url:
|
||||||
|
filename = filename_from_url(url)
|
||||||
|
if filename.endswith(ARCHIVE_EXTENSIONS):
|
||||||
|
filename, ext = splitext(filename)
|
||||||
|
if is_git_sha(filename):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@memoize # Avoid hitting the file[cache] over and over.
|
||||||
|
def _expected_hashes(self):
|
||||||
|
"""Return a list of known-good hashes for this package."""
|
||||||
|
return hashes_above(*path_and_line(self._req))
|
||||||
|
|
||||||
|
def _download(self, link):
|
||||||
|
"""Download a file, and return its name within my temp dir.
|
||||||
|
|
||||||
|
This does no verification of HTTPS certs, but our checking hashes
|
||||||
|
makes that largely unimportant. It would be nice to be able to use the
|
||||||
|
requests lib, which can verify certs, but it is guaranteed to be
|
||||||
|
available only in pip >= 1.5.
|
||||||
|
|
||||||
|
This also drops support for proxies and basic auth, though those could
|
||||||
|
be added back in.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Based on pip 1.4.1's URLOpener but with cert verification removed
|
||||||
|
def opener(is_https):
|
||||||
|
if is_https:
|
||||||
|
opener = build_opener(HTTPSHandler())
|
||||||
|
# Strip out HTTPHandler to prevent MITM spoof:
|
||||||
|
for handler in opener.handlers:
|
||||||
|
if isinstance(handler, HTTPHandler):
|
||||||
|
opener.handlers.remove(handler)
|
||||||
|
else:
|
||||||
|
opener = build_opener()
|
||||||
|
return opener
|
||||||
|
|
||||||
|
# Descended from unpack_http_url() in pip 1.4.1
|
||||||
|
def best_filename(link, response):
|
||||||
|
"""Return the most informative possible filename for a download,
|
||||||
|
ideally with a proper extension.
|
||||||
|
|
||||||
|
"""
|
||||||
|
content_type = response.info().get('content-type', '')
|
||||||
|
filename = link.filename # fallback
|
||||||
|
# Have a look at the Content-Disposition header for a better guess:
|
||||||
|
content_disposition = response.info().get('content-disposition')
|
||||||
|
if content_disposition:
|
||||||
|
type, params = cgi.parse_header(content_disposition)
|
||||||
|
# We use ``or`` here because we don't want to use an "empty" value
|
||||||
|
# from the filename param:
|
||||||
|
filename = params.get('filename') or filename
|
||||||
|
ext = splitext(filename)[1]
|
||||||
|
if not ext:
|
||||||
|
ext = mimetypes.guess_extension(content_type)
|
||||||
|
if ext:
|
||||||
|
filename += ext
|
||||||
|
if not ext and link.url != response.geturl():
|
||||||
|
ext = splitext(response.geturl())[1]
|
||||||
|
if ext:
|
||||||
|
filename += ext
|
||||||
|
return filename
|
||||||
|
|
||||||
|
# Descended from _download_url() in pip 1.4.1
|
||||||
|
def pipe_to_file(response, path, size=0):
|
||||||
|
"""Pull the data off an HTTP response, shove it in a new file, and
|
||||||
|
show progress.
|
||||||
|
|
||||||
|
:arg response: A file-like object to read from
|
||||||
|
:arg path: The path of the new file
|
||||||
|
:arg size: The expected size, in bytes, of the download. 0 for
|
||||||
|
unknown or to suppress progress indication (as for cached
|
||||||
|
downloads)
|
||||||
|
|
||||||
|
"""
|
||||||
|
def response_chunks(chunk_size):
|
||||||
|
while True:
|
||||||
|
chunk = response.read(chunk_size)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
print('Downloading %s%s...' % (
|
||||||
|
self._req.req,
|
||||||
|
(' (%sK)' % (size / 1000)) if size > 1000 else ''))
|
||||||
|
progress_indicator = (DownloadProgressBar(max=size).iter if size
|
||||||
|
else DownloadProgressSpinner().iter)
|
||||||
|
with open(path, 'wb') as file:
|
||||||
|
for chunk in progress_indicator(response_chunks(4096), 4096):
|
||||||
|
file.write(chunk)
|
||||||
|
|
||||||
|
url = link.url.split('#', 1)[0]
|
||||||
|
try:
|
||||||
|
response = opener(urlparse(url).scheme != 'http').open(url)
|
||||||
|
except (HTTPError, IOError) as exc:
|
||||||
|
raise DownloadError(link, exc)
|
||||||
|
filename = best_filename(link, response)
|
||||||
|
try:
|
||||||
|
size = int(response.headers['content-length'])
|
||||||
|
except (ValueError, KeyError, TypeError):
|
||||||
|
size = 0
|
||||||
|
pipe_to_file(response, join(self._temp_path, filename), size=size)
|
||||||
|
return filename
|
||||||
|
|
||||||
|
# Based on req_set.prepare_files() in pip bb2a8428d4aebc8d313d05d590f386fa3f0bbd0f
|
||||||
|
@memoize # Avoid re-downloading.
|
||||||
|
def _downloaded_filename(self):
|
||||||
|
"""Download the package's archive if necessary, and return its
|
||||||
|
filename.
|
||||||
|
|
||||||
|
--no-deps is implied, as we have reimplemented the bits that would
|
||||||
|
ordinarily do dependency resolution.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Peep doesn't support requirements that don't come down as a single
|
||||||
|
# file, because it can't hash them. Thus, it doesn't support editable
|
||||||
|
# requirements, because pip itself doesn't support editable
|
||||||
|
# requirements except for "local projects or a VCS url". Nor does it
|
||||||
|
# support VCS requirements yet, because we haven't yet come up with a
|
||||||
|
# portable, deterministic way to hash them. In summary, all we support
|
||||||
|
# is == requirements and tarballs/zips/etc.
|
||||||
|
|
||||||
|
# TODO: Stop on reqs that are editable or aren't ==.
|
||||||
|
|
||||||
|
# If the requirement isn't already specified as a URL, get a URL
|
||||||
|
# from an index:
|
||||||
|
link = self._link() or self._finder.find_requirement(self._req, upgrade=False)
|
||||||
|
|
||||||
|
if link:
|
||||||
|
lower_scheme = link.scheme.lower() # pip lower()s it for some reason.
|
||||||
|
if lower_scheme == 'http' or lower_scheme == 'https':
|
||||||
|
file_path = self._download(link)
|
||||||
|
return basename(file_path)
|
||||||
|
elif lower_scheme == 'file':
|
||||||
|
# The following is inspired by pip's unpack_file_url():
|
||||||
|
link_path = url_to_path(link.url_without_fragment)
|
||||||
|
if isdir(link_path):
|
||||||
|
raise UnsupportedRequirementError(
|
||||||
|
"%s: %s is a directory. So that it can compute "
|
||||||
|
"a hash, peep supports only filesystem paths which "
|
||||||
|
"point to files" %
|
||||||
|
(self._req, link.url_without_fragment))
|
||||||
|
else:
|
||||||
|
copy(link_path, self._temp_path)
|
||||||
|
return basename(link_path)
|
||||||
|
else:
|
||||||
|
raise UnsupportedRequirementError(
|
||||||
|
"%s: The download link, %s, would not result in a file "
|
||||||
|
"that can be hashed. Peep supports only == requirements, "
|
||||||
|
"file:// URLs pointing to files (not folders), and "
|
||||||
|
"http:// and https:// URLs pointing to tarballs, zips, "
|
||||||
|
"etc." % (self._req, link.url))
|
||||||
|
else:
|
||||||
|
raise UnsupportedRequirementError(
|
||||||
|
"%s: couldn't determine where to download this requirement from."
|
||||||
|
% (self._req,))
|
||||||
|
|
||||||
|
def install(self):
|
||||||
|
"""Install the package I represent, without dependencies.
|
||||||
|
|
||||||
|
Obey typical pip-install options passed in on the command line.
|
||||||
|
|
||||||
|
"""
|
||||||
|
other_args = list(requirement_args(self._argv, want_other=True))
|
||||||
|
archive_path = join(self._temp_path, self._downloaded_filename())
|
||||||
|
# -U so it installs whether pip deems the requirement "satisfied" or
|
||||||
|
# not. This is necessary for GitHub-sourced zips, which change without
|
||||||
|
# their version numbers changing.
|
||||||
|
run_pip(['install'] + other_args + ['--no-deps', '-U', archive_path])
|
||||||
|
|
||||||
|
@memoize
|
||||||
|
def _actual_hash(self):
|
||||||
|
"""Download the package's archive if necessary, and return its hash."""
|
||||||
|
return hash_of_file(join(self._temp_path, self._downloaded_filename()))
|
||||||
|
|
||||||
|
def _project_name(self):
|
||||||
|
"""Return the inner Requirement's "unsafe name".
|
||||||
|
|
||||||
|
Raise ValueError if there is no name.
|
||||||
|
|
||||||
|
"""
|
||||||
|
name = getattr(self._req.req, 'project_name', '')
|
||||||
|
if name:
|
||||||
|
return name
|
||||||
|
raise ValueError('Requirement has no project_name.')
|
||||||
|
|
||||||
|
def _name(self):
|
||||||
|
return self._req.name
|
||||||
|
|
||||||
|
def _link(self):
|
||||||
|
try:
|
||||||
|
return self._req.link
|
||||||
|
except AttributeError:
|
||||||
|
# The link attribute isn't available prior to pip 6.1.0, so fall
|
||||||
|
# back to the now deprecated 'url' attribute.
|
||||||
|
return Link(self._req.url) if self._req.url else None
|
||||||
|
|
||||||
|
def _url(self):
|
||||||
|
link = self._link()
|
||||||
|
return link.url if link else None
|
||||||
|
|
||||||
|
@memoize # Avoid re-running expensive check_if_exists().
|
||||||
|
def _is_satisfied(self):
|
||||||
|
self._req.check_if_exists()
|
||||||
|
return (self._req.satisfied_by and
|
||||||
|
not self._is_always_unsatisfied())
|
||||||
|
|
||||||
|
def _class(self):
|
||||||
|
"""Return the class I should be, spanning a continuum of goodness."""
|
||||||
|
try:
|
||||||
|
self._project_name()
|
||||||
|
except ValueError:
|
||||||
|
return MalformedReq
|
||||||
|
if self._is_satisfied():
|
||||||
|
return SatisfiedReq
|
||||||
|
if not self._expected_hashes():
|
||||||
|
return MissingReq
|
||||||
|
if self._actual_hash() not in self._expected_hashes():
|
||||||
|
return MismatchedReq
|
||||||
|
return InstallableReq
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def foot(cls):
|
||||||
|
"""Return the text to be printed once, after all of the errors from
|
||||||
|
classes of my type are printed.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
class MalformedReq(DownloadedReq):
|
||||||
|
"""A requirement whose package name could not be determined"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def head(cls):
|
||||||
|
return 'The following requirements could not be processed:\n'
|
||||||
|
|
||||||
|
def error(self):
|
||||||
|
return '* Unable to determine package name from URL %s; add #egg=' % self._url()
|
||||||
|
|
||||||
|
|
||||||
|
class MissingReq(DownloadedReq):
|
||||||
|
"""A requirement for which no hashes were specified in the requirements file"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def head(cls):
|
||||||
|
return ('The following packages had no hashes specified in the requirements file, which\n'
|
||||||
|
'leaves them open to tampering. Vet these packages to your satisfaction, then\n'
|
||||||
|
'add these "sha256" lines like so:\n\n')
|
||||||
|
|
||||||
|
def error(self):
|
||||||
|
if self._url():
|
||||||
|
# _url() always contains an #egg= part, or this would be a
|
||||||
|
# MalformedRequest.
|
||||||
|
line = self._url()
|
||||||
|
else:
|
||||||
|
line = '%s==%s' % (self._name(), self._version())
|
||||||
|
return '# sha256: %s\n%s\n' % (self._actual_hash(), line)
|
||||||
|
|
||||||
|
|
||||||
|
class MismatchedReq(DownloadedReq):
|
||||||
|
"""A requirement for which the downloaded file didn't match any of my hashes."""
|
||||||
|
@classmethod
|
||||||
|
def head(cls):
|
||||||
|
return ("THE FOLLOWING PACKAGES DIDN'T MATCH THE HASHES SPECIFIED IN THE REQUIREMENTS\n"
|
||||||
|
"FILE. If you have updated the package versions, update the hashes. If not,\n"
|
||||||
|
"freak out, because someone has tampered with the packages.\n\n")
|
||||||
|
|
||||||
|
def error(self):
|
||||||
|
preamble = ' %s: expected' % self._project_name()
|
||||||
|
if len(self._expected_hashes()) > 1:
|
||||||
|
preamble += ' one of'
|
||||||
|
padding = '\n' + ' ' * (len(preamble) + 1)
|
||||||
|
return '%s %s\n%s got %s' % (preamble,
|
||||||
|
padding.join(self._expected_hashes()),
|
||||||
|
' ' * (len(preamble) - 4),
|
||||||
|
self._actual_hash())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def foot(cls):
|
||||||
|
return '\n'
|
||||||
|
|
||||||
|
|
||||||
|
class SatisfiedReq(DownloadedReq):
|
||||||
|
"""A requirement which turned out to be already installed"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def head(cls):
|
||||||
|
return ("These packages were already installed, so we didn't need to download or build\n"
|
||||||
|
"them again. If you installed them with peep in the first place, you should be\n"
|
||||||
|
"safe. If not, uninstall them, then re-attempt your install with peep.\n")
|
||||||
|
|
||||||
|
def error(self):
|
||||||
|
return ' %s' % (self._req,)
|
||||||
|
|
||||||
|
|
||||||
|
class InstallableReq(DownloadedReq):
|
||||||
|
"""A requirement whose hash matched and can be safely installed"""
|
||||||
|
|
||||||
|
|
||||||
|
# DownloadedReq subclasses that indicate an error that should keep us from
|
||||||
|
# going forward with installation, in the order in which their errors should
|
||||||
|
# be reported:
|
||||||
|
ERROR_CLASSES = [MismatchedReq, MissingReq, MalformedReq]
|
||||||
|
|
||||||
|
|
||||||
|
def bucket(things, key):
|
||||||
|
"""Return a map of key -> list of things."""
|
||||||
|
ret = defaultdict(list)
|
||||||
|
for thing in things:
|
||||||
|
ret[key(thing)].append(thing)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def first_every_last(iterable, first, every, last):
|
||||||
|
"""Execute something before the first item of iter, something else for each
|
||||||
|
item, and a third thing after the last.
|
||||||
|
|
||||||
|
If there are no items in the iterable, don't execute anything.
|
||||||
|
|
||||||
|
"""
|
||||||
|
did_first = False
|
||||||
|
for item in iterable:
|
||||||
|
if not did_first:
|
||||||
|
did_first = True
|
||||||
|
first(item)
|
||||||
|
every(item)
|
||||||
|
if did_first:
|
||||||
|
last(item)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_requirements(path, finder):
|
||||||
|
try:
|
||||||
|
# list() so the generator that is parse_requirements() actually runs
|
||||||
|
# far enough to report a TypeError
|
||||||
|
return list(parse_requirements(
|
||||||
|
path, options=EmptyOptions(), finder=finder))
|
||||||
|
except TypeError:
|
||||||
|
# session is a required kwarg as of pip 6.0 and will raise
|
||||||
|
# a TypeError if missing. It needs to be a PipSession instance,
|
||||||
|
# but in older versions we can't import it from pip.download
|
||||||
|
# (nor do we need it at all) so we only import it in this except block
|
||||||
|
from pip.download import PipSession
|
||||||
|
return list(parse_requirements(
|
||||||
|
path, options=EmptyOptions(), session=PipSession(), finder=finder))
|
||||||
|
|
||||||
|
|
||||||
|
def downloaded_reqs_from_path(path, argv):
|
||||||
|
"""Return a list of DownloadedReqs representing the requirements parsed
|
||||||
|
out of a given requirements file.
|
||||||
|
|
||||||
|
:arg path: The path to the requirements file
|
||||||
|
:arg argv: The commandline args, starting after the subcommand
|
||||||
|
|
||||||
|
"""
|
||||||
|
finder = package_finder(argv)
|
||||||
|
return [DownloadedReq(req, argv, finder) for req in
|
||||||
|
_parse_requirements(path, finder)]
|
||||||
|
|
||||||
|
|
||||||
|
def peep_install(argv):
|
||||||
|
"""Perform the ``peep install`` subcommand, returning a shell status code
|
||||||
|
or raising a PipException.
|
||||||
|
|
||||||
|
:arg argv: The commandline args, starting after the subcommand
|
||||||
|
|
||||||
|
"""
|
||||||
|
output = []
|
||||||
|
out = output.append
|
||||||
|
reqs = []
|
||||||
|
try:
|
||||||
|
req_paths = list(requirement_args(argv, want_paths=True))
|
||||||
|
if not req_paths:
|
||||||
|
out("You have to specify one or more requirements files with the -r option, because\n"
|
||||||
|
"otherwise there's nowhere for peep to look up the hashes.\n")
|
||||||
|
return COMMAND_LINE_ERROR
|
||||||
|
|
||||||
|
# We're a "peep install" command, and we have some requirement paths.
|
||||||
|
reqs = list(chain.from_iterable(
|
||||||
|
downloaded_reqs_from_path(path, argv)
|
||||||
|
for path in req_paths))
|
||||||
|
buckets = bucket(reqs, lambda r: r.__class__)
|
||||||
|
|
||||||
|
# Skip a line after pip's "Cleaning up..." so the important stuff
|
||||||
|
# stands out:
|
||||||
|
if any(buckets[b] for b in ERROR_CLASSES):
|
||||||
|
out('\n')
|
||||||
|
|
||||||
|
printers = (lambda r: out(r.head()),
|
||||||
|
lambda r: out(r.error() + '\n'),
|
||||||
|
lambda r: out(r.foot()))
|
||||||
|
for c in ERROR_CLASSES:
|
||||||
|
first_every_last(buckets[c], *printers)
|
||||||
|
|
||||||
|
if any(buckets[b] for b in ERROR_CLASSES):
|
||||||
|
out('-------------------------------\n'
|
||||||
|
'Not proceeding to installation.\n')
|
||||||
|
return SOMETHING_WENT_WRONG
|
||||||
|
else:
|
||||||
|
for req in buckets[InstallableReq]:
|
||||||
|
req.install()
|
||||||
|
|
||||||
|
first_every_last(buckets[SatisfiedReq], *printers)
|
||||||
|
|
||||||
|
return ITS_FINE_ITS_FINE
|
||||||
|
except (UnsupportedRequirementError, DownloadError) as exc:
|
||||||
|
out(str(exc))
|
||||||
|
return SOMETHING_WENT_WRONG
|
||||||
|
finally:
|
||||||
|
for req in reqs:
|
||||||
|
req.dispose()
|
||||||
|
print(''.join(output))
|
||||||
|
|
||||||
|
|
||||||
|
def peep_port(paths):
|
||||||
|
"""Convert a peep requirements file to one compatble with pip-8 hashing.
|
||||||
|
|
||||||
|
Loses comments and tromps on URLs, so the result will need a little manual
|
||||||
|
massaging, but the hard part--the hash conversion--is done for you.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not paths:
|
||||||
|
print('Please specify one or more requirements files so I have '
|
||||||
|
'something to port.\n')
|
||||||
|
return COMMAND_LINE_ERROR
|
||||||
|
for req in chain.from_iterable(
|
||||||
|
_parse_requirements(path, package_finder(argv)) for path in paths):
|
||||||
|
hashes = [hexlify(urlsafe_b64decode((hash + '=').encode('ascii'))).decode('ascii')
|
||||||
|
for hash in hashes_above(*path_and_line(req))]
|
||||||
|
if not hashes:
|
||||||
|
print(req.req)
|
||||||
|
elif len(hashes) == 1:
|
||||||
|
print('%s --hash=sha256:%s' % (req.req, hashes[0]))
|
||||||
|
else:
|
||||||
|
print('%s' % req.req, end='')
|
||||||
|
for hash in hashes:
|
||||||
|
print(' \\')
|
||||||
|
print(' --hash=sha256:%s' % hash, end='')
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Be the top-level entrypoint. Return a shell status code."""
|
||||||
|
commands = {'hash': peep_hash,
|
||||||
|
'install': peep_install,
|
||||||
|
'port': peep_port}
|
||||||
|
try:
|
||||||
|
if len(argv) >= 2 and argv[1] in commands:
|
||||||
|
return commands[argv[1]](argv[2:])
|
||||||
|
else:
|
||||||
|
# Fall through to top-level pip main() for everything else:
|
||||||
|
return pip.main()
|
||||||
|
except PipException as exc:
|
||||||
|
return exc.error_code
|
||||||
|
|
||||||
|
|
||||||
|
def exception_handler(exc_type, exc_value, exc_tb):
|
||||||
|
print('Oh no! Peep had a problem while trying to do stuff. Please write up a bug report')
|
||||||
|
print('with the specifics so we can fix it:')
|
||||||
|
print()
|
||||||
|
print('https://github.com/erikrose/peep/issues/new')
|
||||||
|
print()
|
||||||
|
print('Here are some particulars you can copy and paste into the bug report:')
|
||||||
|
print()
|
||||||
|
print('---')
|
||||||
|
print('peep:', repr(__version__))
|
||||||
|
print('python:', repr(sys.version))
|
||||||
|
print('pip:', repr(getattr(pip, '__version__', 'no __version__ attr')))
|
||||||
|
print('Command line: ', repr(sys.argv))
|
||||||
|
print(
|
||||||
|
''.join(traceback.format_exception(exc_type, exc_value, exc_tb)))
|
||||||
|
print('---')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
exit(main())
|
||||||
|
except Exception:
|
||||||
|
exception_handler(*sys.exc_info())
|
||||||
|
exit(SOMETHING_WENT_WRONG)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
"""Tests for letsencrypt-auto
|
||||||
|
|
||||||
|
Run these locally by saying... ::
|
||||||
|
|
||||||
|
./build.py && docker build -t lea . && docker run --rm -t -i lea
|
||||||
|
|
||||||
|
"""
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
"""Tests for letsencrypt-auto"""
|
||||||
|
|
||||||
|
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from functools import partial
|
||||||
|
from json import dumps
|
||||||
|
from os import chmod, environ
|
||||||
|
from os.path import abspath, dirname, join
|
||||||
|
import re
|
||||||
|
from shutil import copy, rmtree
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
from stat import S_IRUSR, S_IXUSR
|
||||||
|
from subprocess import CalledProcessError, check_output, Popen, PIPE
|
||||||
|
import sys
|
||||||
|
from tempfile import mkdtemp
|
||||||
|
from threading import Thread
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from nose.tools import eq_, nottest, ok_
|
||||||
|
|
||||||
|
|
||||||
|
@nottest
|
||||||
|
def tests_dir():
|
||||||
|
"""Return a path to the "tests" directory."""
|
||||||
|
return dirname(abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
sys.path.insert(0, dirname(tests_dir()))
|
||||||
|
from build import build as build_le_auto
|
||||||
|
|
||||||
|
|
||||||
|
class RequestHandler(BaseHTTPRequestHandler):
|
||||||
|
"""An HTTPS request handler which is quiet and serves a specific folder."""
|
||||||
|
|
||||||
|
def __init__(self, resources, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
:arg resources: A dict of resource paths pointing to content bytes
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.resources = resources
|
||||||
|
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
"""Don't log each request to the terminal."""
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
"""Serve a GET request."""
|
||||||
|
content = self.send_head()
|
||||||
|
if content is not None:
|
||||||
|
self.wfile.write(content)
|
||||||
|
|
||||||
|
def send_head(self):
|
||||||
|
"""Common code for GET and HEAD commands
|
||||||
|
|
||||||
|
This sends the response code and MIME headers and returns either a
|
||||||
|
bytestring of content or, if none is found, None.
|
||||||
|
|
||||||
|
"""
|
||||||
|
path = self.path[1:] # Strip leading slash.
|
||||||
|
content = self.resources.get(path)
|
||||||
|
if content is None:
|
||||||
|
self.send_error(404, 'Path "%s" not found in self.resources' % path)
|
||||||
|
else:
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'text/plain')
|
||||||
|
self.send_header('Content-Length', str(len(content)))
|
||||||
|
self.end_headers()
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def server_and_port(resources):
|
||||||
|
"""Return an unstarted HTTPS server and the port it will use."""
|
||||||
|
# Find a port, and bind to it. I can't get the OS to close the socket
|
||||||
|
# promptly after we shut down the server, so we typically need to try
|
||||||
|
# a couple ports after the first test case. Setting
|
||||||
|
# TCPServer.allow_reuse_address = True seems to have nothing to do
|
||||||
|
# with this behavior.
|
||||||
|
worked = False
|
||||||
|
for port in xrange(4443, 4543):
|
||||||
|
try:
|
||||||
|
server = HTTPServer(('localhost', port),
|
||||||
|
partial(RequestHandler, resources))
|
||||||
|
except socket.error:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
worked = True
|
||||||
|
server.socket = ssl.wrap_socket(
|
||||||
|
server.socket,
|
||||||
|
certfile=join(tests_dir(), 'certs', 'localhost', 'server.pem'),
|
||||||
|
server_side=True)
|
||||||
|
break
|
||||||
|
if not worked:
|
||||||
|
raise RuntimeError("Couldn't find an unused socket for the testing HTTPS server.")
|
||||||
|
return server, port
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def serving(resources):
|
||||||
|
"""Spin up a local HTTPS server, and yield its base URL.
|
||||||
|
|
||||||
|
Use a self-signed cert generated as outlined by
|
||||||
|
https://coolaj86.com/articles/create-your-own-certificate-authority-for-
|
||||||
|
testing/.
|
||||||
|
|
||||||
|
"""
|
||||||
|
server, port = server_and_port(resources)
|
||||||
|
thread = Thread(target=server.serve_forever)
|
||||||
|
try:
|
||||||
|
thread.start()
|
||||||
|
yield 'https://localhost:{port}/'.format(port=port)
|
||||||
|
finally:
|
||||||
|
server.shutdown()
|
||||||
|
thread.join()
|
||||||
|
|
||||||
|
|
||||||
|
LE_AUTO_PATH = join(dirname(tests_dir()), 'letsencrypt-auto')
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def ephemeral_dir():
|
||||||
|
dir = mkdtemp(prefix='le-test-')
|
||||||
|
try:
|
||||||
|
yield dir
|
||||||
|
finally:
|
||||||
|
rmtree(dir)
|
||||||
|
|
||||||
|
|
||||||
|
def out_and_err(command, input=None, shell=False, env=None):
|
||||||
|
"""Run a shell command, and return stderr and stdout as string.
|
||||||
|
|
||||||
|
If the command returns nonzero, raise CalledProcessError.
|
||||||
|
|
||||||
|
:arg command: A list of commandline args
|
||||||
|
:arg input: Data to pipe to stdin. Omit for none.
|
||||||
|
|
||||||
|
Remaining args have the same meaning as for Popen.
|
||||||
|
|
||||||
|
"""
|
||||||
|
process = Popen(command,
|
||||||
|
stdout=PIPE,
|
||||||
|
stdin=PIPE,
|
||||||
|
stderr=PIPE,
|
||||||
|
shell=shell,
|
||||||
|
env=env)
|
||||||
|
out, err = process.communicate(input=input)
|
||||||
|
status = process.poll() # same as in check_output(), though wait() sounds better
|
||||||
|
if status:
|
||||||
|
raise CalledProcessError(status, command, output=out)
|
||||||
|
return out, err
|
||||||
|
|
||||||
|
|
||||||
|
def signed(content, private_key_name='signing.key'):
|
||||||
|
"""Return the signed SHA-256 hash of ``content``, using the given key file."""
|
||||||
|
command = ['openssl', 'dgst', '-sha256', '-sign',
|
||||||
|
join(tests_dir(), private_key_name)]
|
||||||
|
out, err = out_and_err(command, input=content)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def install_le_auto(contents, venv_dir):
|
||||||
|
"""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
|
||||||
|
|
||||||
|
"""
|
||||||
|
venv_le_auto_path = join(venv_dir, 'letsencrypt-auto')
|
||||||
|
with open(venv_le_auto_path, 'w') as le_auto:
|
||||||
|
le_auto.write(contents)
|
||||||
|
chmod(venv_le_auto_path, S_IRUSR | S_IXUSR)
|
||||||
|
|
||||||
|
|
||||||
|
def run_le_auto(venv_dir, base_url, **kwargs):
|
||||||
|
"""Run the prebuilt version of letsencrypt-auto, returning stdout and
|
||||||
|
stderr strings.
|
||||||
|
|
||||||
|
If the command returns other than 0, raise CalledProcessError.
|
||||||
|
|
||||||
|
"""
|
||||||
|
env = environ.copy()
|
||||||
|
d = dict(XDG_DATA_HOME=venv_dir,
|
||||||
|
# URL to PyPI-style JSON that tell us the latest released version
|
||||||
|
# of LE:
|
||||||
|
LE_AUTO_JSON_URL=base_url + 'letsencrypt/json',
|
||||||
|
# URL to dir containing letsencrypt-auto and letsencrypt-auto.sig:
|
||||||
|
LE_AUTO_DIR_TEMPLATE=base_url + '%s/',
|
||||||
|
# The public key corresponding to signing.key:
|
||||||
|
LE_AUTO_PUBLIC_KEY="""-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsMoSzLYQ7E1sdSOkwelg
|
||||||
|
tzKIh2qi3bpXuYtcfFC0XrvWig071NwIj+dZiT0OLZ2hPispEH0B7ISuuWg1ll7G
|
||||||
|
hFW0VdbxL6JdGzS2ShNWkX9hE9z+j8VqwDPOBn3ZHm03qwpYkBDwQib3KqOdYbTT
|
||||||
|
uUtJmmGcuk3a9Aq/sCT6DdfmTSdP5asdQYwIcaQreDrOosaS84DTWI3IU+UYJVgl
|
||||||
|
LsIVPBuy9IcgHidUQ96hJnoPsDCWsHwX62495QKEarauyKQrJzFes0EY95orDM47
|
||||||
|
Z5o/NDiQB11m91yNB0MmPYY9QSbnOA9j7IaaC97AwRLuwXY+/R2ablTcxurWou68
|
||||||
|
iQIDAQAB
|
||||||
|
-----END PUBLIC KEY-----""",
|
||||||
|
**kwargs)
|
||||||
|
env.update(d)
|
||||||
|
return out_and_err(
|
||||||
|
join(venv_dir, 'letsencrypt-auto') + ' --version',
|
||||||
|
shell=True,
|
||||||
|
env=env)
|
||||||
|
|
||||||
|
|
||||||
|
def set_le_script_version(venv_dir, version):
|
||||||
|
"""Tell the letsencrypt script to report a certain version.
|
||||||
|
|
||||||
|
We actually replace the script with a dummy version that knows only how to
|
||||||
|
print its version.
|
||||||
|
|
||||||
|
"""
|
||||||
|
with open(join(venv_dir, 'letsencrypt', 'bin', 'letsencrypt'), 'w') as script:
|
||||||
|
script.write("#!/usr/bin/env python\n"
|
||||||
|
"from sys import stderr\n"
|
||||||
|
"stderr.write('letsencrypt %s\\n')" % version)
|
||||||
|
|
||||||
|
|
||||||
|
class AutoTests(TestCase):
|
||||||
|
"""Test the major branch points of letsencrypt-auto:
|
||||||
|
|
||||||
|
* An le-auto upgrade is needed.
|
||||||
|
* An le-auto upgrade is not needed.
|
||||||
|
* There was an out-of-date LE script installed.
|
||||||
|
* There was a current LE script installed.
|
||||||
|
* There was no LE script installed (less important).
|
||||||
|
* Peep verification passes.
|
||||||
|
* Peep has a hash mismatch.
|
||||||
|
* The OpenSSL sig matches.
|
||||||
|
* 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
|
||||||
|
test suites.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def test_successes(self):
|
||||||
|
"""Exercise most branches of letsencrypt-auto.
|
||||||
|
|
||||||
|
They just happen to be the branches in which everything goes well.
|
||||||
|
|
||||||
|
I violate my usual rule of having small, decoupled tests, because...
|
||||||
|
|
||||||
|
1. We shouldn't need to run a Cartesian product of the branches: the
|
||||||
|
phases run in separate shell processes, containing state leakage
|
||||||
|
pretty effectively. The only shared state is FS state, and it's
|
||||||
|
limited to a temp dir, assuming (if we dare) all functions properly.
|
||||||
|
2. One combination of branches happens to set us up nicely for testing
|
||||||
|
the next, saving code.
|
||||||
|
|
||||||
|
"""
|
||||||
|
NEW_LE_AUTO = build_le_auto(
|
||||||
|
version='99.9.9',
|
||||||
|
requirements='# sha256: HMFNYatCTN7kRvUeUPESP4SC7HQFh_54YmyTO7ooc6A\n'
|
||||||
|
'letsencrypt==99.9.9')
|
||||||
|
NEW_LE_AUTO_SIG = signed(NEW_LE_AUTO)
|
||||||
|
|
||||||
|
with ephemeral_dir() as 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 = {'letsencrypt/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}
|
||||||
|
with serving(resources) as base_url:
|
||||||
|
run_letsencrypt_auto = partial(
|
||||||
|
run_le_auto,
|
||||||
|
venv_dir,
|
||||||
|
base_url,
|
||||||
|
PIP_FIND_LINKS=join(tests_dir(),
|
||||||
|
'fake-letsencrypt',
|
||||||
|
'dist'))
|
||||||
|
|
||||||
|
# Test when a phase-1 upgrade is needed, there's no LE binary
|
||||||
|
# installed, and peep verifies:
|
||||||
|
install_le_auto(build_le_auto(version='50.0.0'), venv_dir)
|
||||||
|
out, err = run_letsencrypt_auto()
|
||||||
|
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('Creating virtual environment...', out)
|
||||||
|
|
||||||
|
# Now we have le-auto 99.9.9 and LE 99.9.9 installed. This
|
||||||
|
# conveniently sets us up to test the next 2 cases.
|
||||||
|
|
||||||
|
# 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('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.assertIn('Creating virtual environment...', out)
|
||||||
|
|
||||||
|
def test_openssl_failure(self):
|
||||||
|
"""Make sure we stop if the openssl signature check fails."""
|
||||||
|
with ephemeral_dir() as venv_dir:
|
||||||
|
# Serve an unrelated hash signed with the good key (easier than
|
||||||
|
# making a bad key, and a mismatch is a mismatch):
|
||||||
|
resources = {'': '<a href="letsencrypt/">letsencrypt/</a>',
|
||||||
|
'letsencrypt/json': dumps({'releases': {'99.9.9': None}}),
|
||||||
|
'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)
|
||||||
|
try:
|
||||||
|
out, err = run_le_auto(venv_dir, base_url)
|
||||||
|
except CalledProcessError as exc:
|
||||||
|
eq_(exc.returncode, 1)
|
||||||
|
self.assertIn("Couldn't verify signature of downloaded "
|
||||||
|
"letsencrypt-auto.",
|
||||||
|
exc.output)
|
||||||
|
else:
|
||||||
|
self.fail('Signature check on letsencrypt-auto erroneously passed.')
|
||||||
|
|
||||||
|
def test_peep_failure(self):
|
||||||
|
"""Make sure peep stops us if there is a hash mismatch."""
|
||||||
|
with ephemeral_dir() as venv_dir:
|
||||||
|
resources = {'': '<a href="letsencrypt/">letsencrypt/</a>',
|
||||||
|
'letsencrypt/json': dumps({'releases': {'99.9.9': None}})}
|
||||||
|
with serving(resources) as base_url:
|
||||||
|
# Build a le-auto script embedding a bad requirements file:
|
||||||
|
install_le_auto(
|
||||||
|
build_le_auto(
|
||||||
|
version='99.9.9',
|
||||||
|
requirements='# sha256: badbadbadbadbadbadbadbadbadbadbadbadbadbadb\n'
|
||||||
|
'configobj==5.0.6'),
|
||||||
|
venv_dir)
|
||||||
|
try:
|
||||||
|
out, err = run_le_auto(venv_dir, base_url)
|
||||||
|
except CalledProcessError as exc:
|
||||||
|
eq_(exc.returncode, 1)
|
||||||
|
self.assertIn("THE FOLLOWING PACKAGES DIDN'T MATCH THE "
|
||||||
|
"HASHES SPECIFIED IN THE REQUIREMENTS",
|
||||||
|
exc.output)
|
||||||
|
else:
|
||||||
|
self.fail("Peep didn't detect a bad hash and stop the "
|
||||||
|
"installation.")
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIID5jCCAs6gAwIBAgIJAI1Qkfyw88REMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV
|
||||||
|
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMRswGQYDVQQKExJNeSBCb2d1cyBS
|
||||||
|
b290IENlcnQxFDASBgNVBAMTC2V4YW1wbGUuY29tMB4XDTE1MTIwNDIwNTIxNVoX
|
||||||
|
DTQwMTIwMzIwNTIxNVowVTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3Rh
|
||||||
|
dGUxGzAZBgNVBAoTEk15IEJvZ3VzIFJvb3QgQ2VydDEUMBIGA1UEAxMLZXhhbXBs
|
||||||
|
ZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQVQpQ2EH4gTJB
|
||||||
|
NJP6+ocT3xJwT8mSXYUnvzjj6iv+JxZiXRGzAPziNzrrSRKY0yDHF+UiJwuOerLa
|
||||||
|
n8laZkLb1Ogqzs2u64rKeb0xWv90Qp+eXG0J/1xb4dw+GExqe5QFo1JUJzO/eK7m
|
||||||
|
1S04SeFkN1qV9mD5yJUy7DGiTUzDHgCxM2tXMLusXYqkxsQQ9+2EJ7BEOK4YJGEx
|
||||||
|
Sign5FuSxb64PiNow6OA97CaLl7tV4INP4w195ueDRIaS4poeOep4s8U7IAdMjIZ
|
||||||
|
EryJgKNCij50xK92vPBBJSj0NOitltBlwoEqkOZpQCOZamFd6nvt78LQ6W8Am+l6
|
||||||
|
y6oCON5JAgMBAAGjgbgwgbUwHQYDVR0OBBYEFAlrdStDhaayLLj89Whe3Gc+HE8y
|
||||||
|
MIGFBgNVHSMEfjB8gBQJa3UrQ4Wmsiy4/PVoXtxnPhxPMqFZpFcwVTELMAkGA1UE
|
||||||
|
BhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxGzAZBgNVBAoTEk15IEJvZ3VzIFJv
|
||||||
|
b3QgQ2VydDEUMBIGA1UEAxMLZXhhbXBsZS5jb22CCQCNUJH8sPPERDAMBgNVHRME
|
||||||
|
BTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQC7KAQfDTiNM3QO8Ic3x21CAPJUavkH
|
||||||
|
zshifN+Ei0+nmseHDTCTgsGfGDOToLUpUEZ4PuiHnz08UwRfd9wotc3SgY9ZaXMe
|
||||||
|
vRs8KUAF9EoyTvESzPyv2b6cS9NNMpj5y7KyXSyP17VoGbNavtiGQ4dwgEH6VgNl
|
||||||
|
0RtBvcSBv/tqxIIx1tWzL74tVEm0Kbd9BAZsYpQNKL8e6WXP35/j0PvCCvtofGrA
|
||||||
|
E8LTqMz4kCwnX+QaJIMJhBophRCsjXdAkvFbFxX0DGPztQtzIwBPcdMjsft7AFeE
|
||||||
|
0XchhDDXxw8YsbpvPfCvrD8XiiVuBycbnB1zt0LLVwB/QsCzUW9ImpLC
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEA0FUKUNhB+IEyQTST+vqHE98ScE/Jkl2FJ7844+or/icWYl0R
|
||||||
|
swD84jc660kSmNMgxxflIicLjnqy2p/JWmZC29ToKs7NruuKynm9MVr/dEKfnlxt
|
||||||
|
Cf9cW+HcPhhManuUBaNSVCczv3iu5tUtOEnhZDdalfZg+ciVMuwxok1Mwx4AsTNr
|
||||||
|
VzC7rF2KpMbEEPfthCewRDiuGCRhMUooJ+RbksW+uD4jaMOjgPewmi5e7VeCDT+M
|
||||||
|
Nfebng0SGkuKaHjnqeLPFOyAHTIyGRK8iYCjQoo+dMSvdrzwQSUo9DTorZbQZcKB
|
||||||
|
KpDmaUAjmWphXep77e/C0OlvAJvpesuqAjjeSQIDAQABAoIBAH+qbVzneV3wxjwh
|
||||||
|
HUHi/p3VyHXc3xh7iNq3mwRH/1eK2nPCttLsGwwBbnC64dOXJfH7maWZKcLRPAMv
|
||||||
|
gfOM0RHn4bJB8tdrbizv91lke0DihvBDkWpb+1wvB4lh2Io0Wpwt3ojFUTfXm87G
|
||||||
|
+iQRWjbQmQlm5zyKh6uiBDSCjDTQdb9omZEBMAwlGPTZwt8TRUEtWd8QgW8FCHoB
|
||||||
|
iLER2WBwXdvn3PBtocI3VE6IYDSeZ81Xv+d7925RtVintT8Suk4toYwX+jfSz+wZ
|
||||||
|
sgHd5V6PSv9a7GUlWoUihD99D9wqDZE8IvMDZ5ofSAUd1KfICDtmsEyugY7u2yYZ
|
||||||
|
tYt49AECgYEA73f7ITMHg8JsUipqb6eG10gCRtRhkqrrO1g/TNeTBh3CTrQGb56e
|
||||||
|
y6kmUivn5gK46t3T2N4Ht4IR8fpLcJcbPYPQNulSjmWm5y6WduafXW/VCW1NA9Lc
|
||||||
|
FyGPkMxFCIVJTLFxfLFepBVvtUzLLDKGGtQxru/GNbBzjdtmVfDPIoECgYEA3rbM
|
||||||
|
cTfvj+jWrV1YsRbphyjy+k3OJEIVx6KA4s5d7Tp12UfYQp/B3HPhXXm5wqeo1Nos
|
||||||
|
UAEWZIMi1VoE8iu6jjeJ6uERtbKKQVed25Us/ff0jUPbxlXgiBOtRcllq9d9Srjm
|
||||||
|
ybHUgfjLsZ2/xpIcOl+oI5pDM9JvD8Sq4ZCFR8kCgYBK/H0tFjeiML2OtS2DLShy
|
||||||
|
PWBJIbQ0I0Vp3eZkf5TQc30m/ASP61G6YItZa9pAElYpZbEy1cQA2MAZz9DTvt2O
|
||||||
|
07ndmA57/KTY+6OuM+Vvctd5DjrxmZPFwoKcSvrLAkHDvETXUQtbwkKquRNeEawg
|
||||||
|
tpWgPAELSufEYhGXk8KpAQKBgBDCqPgMQZcO6rj5QWdyVfi5+C8mE9Fet8ziSdjH
|
||||||
|
twHXWG8VnQzGgQxaHCewtW4Ut/vsv1D2A/1kcQalU6H18IArZdGrRm3qFcV9FoAj
|
||||||
|
5dLnChxncu6mH9Odx3htA52/BcrNx3B+VYPCeXHQcVI8RKuP71NelJgdygXhwwpe
|
||||||
|
mekhAoGBAOUovnqylciYa9HRqo+xZk59eyX+ehhnlV8SeJ2K0PwaQkzQ0KYtCmE7
|
||||||
|
kdSdhcv8h/IQKGaFfc/LyFMM/a26PfAeY5bj41UjkT0K5hQrYuL/52xaT401YLcb
|
||||||
|
Xo+bZz9K0hrdP7TdZFuTY/WxojXgjsVAuAN1NwnJumqxhzPh+hfl
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
D613482D0EF95DD0
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDKjCCAhICCQDWE0gtDvld0DANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJB
|
||||||
|
VTETMBEGA1UECBMKU29tZS1TdGF0ZTEbMBkGA1UEChMSTXkgQm9ndXMgUm9vdCBD
|
||||||
|
ZXJ0MRQwEgYDVQQDEwtleGFtcGxlLmNvbTAeFw0xNTEyMDQyMDU0MzFaFw00MDEy
|
||||||
|
MDMyMDU0MzFaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEw
|
||||||
|
HwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2Fs
|
||||||
|
aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK2WIIi86Mis4UQH
|
||||||
|
a5PrFbX2PBtQHbI3t3ekN1CewRsgQ/2X3lCeWhKmr3CJYXVgA7q/23PORQAiuV6y
|
||||||
|
DG2dQIrjeahWCXaCptTi49ljfVRTW2IxrHke/iA8TkDuZbWGzVLb8TB83ipBOD41
|
||||||
|
SjuomoN4A/ktnIfbNqRqgjjHs2wwJHDfxPiCQlwyOayjHmdlh8cqfVE8rWEm5/3T
|
||||||
|
Iu0X1J53SammR1SbUmsLJNofxFYMK1ogHb0CaFEG9QuuUDPJl5K74Rr6InMQZKPn
|
||||||
|
ne4W3cGoALxPHAca7yicpSMSmdsmd6pqylc2Fdua7o/wf0SwShxS4A1DqA/HWLEM
|
||||||
|
V6MSEF8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAz5sMAFG6W/ZEULZITkBTCU6P
|
||||||
|
NttpGiKufnqyBW5HyNylaczfnHnClvQjr8f/84xvKVcfC3xP0lz+92aIQqo+5L/n
|
||||||
|
v7gLhBFR4Vr2XwMt2qz2FpkaxmVwnhVAHaaC05WIKQ6W2gDwWT0u1K8YdTh+7mvN
|
||||||
|
AT9FW4vDgtNZWq4W/PePh9QCiOOQhGOuBYj/7zqLtz4XPifhi66ILIRDHiu0kond
|
||||||
|
3YMFcECIAf4MPT9vT0iNcWX+c8CfAixPt8nMD6bzOo3oTcfuZh/2enfgLbMqOlOi
|
||||||
|
uk72FM5VVPXTWAckJvL/vVjqsvDuJQKqbr0oUc3bdWbS36xtWZUycp4IQLguAQ==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-----BEGIN CERTIFICATE REQUEST-----
|
||||||
|
MIICnjCCAYYCAQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUx
|
||||||
|
ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAxMJbG9j
|
||||||
|
YWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArZYgiLzoyKzh
|
||||||
|
RAdrk+sVtfY8G1Adsje3d6Q3UJ7BGyBD/ZfeUJ5aEqavcIlhdWADur/bc85FACK5
|
||||||
|
XrIMbZ1AiuN5qFYJdoKm1OLj2WN9VFNbYjGseR7+IDxOQO5ltYbNUtvxMHzeKkE4
|
||||||
|
PjVKO6iag3gD+S2ch9s2pGqCOMezbDAkcN/E+IJCXDI5rKMeZ2WHxyp9UTytYSbn
|
||||||
|
/dMi7RfUnndJqaZHVJtSawsk2h/EVgwrWiAdvQJoUQb1C65QM8mXkrvhGvoicxBk
|
||||||
|
o+ed7hbdwagAvE8cBxrvKJylIxKZ2yZ3qmrKVzYV25ruj/B/RLBKHFLgDUOoD8dY
|
||||||
|
sQxXoxIQXwIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBAFbg3WrAokoPx7iAYG6z
|
||||||
|
PqeDd4/XanXjeL4Ryxv6LoGhu69mmBAd3N5ILPyQJjnkWpIjEmJDzEcPMzhQjRh5
|
||||||
|
GlWTyvKWO4zClYU840KZk7crVkpzNZ+HP0YeM/Agz6sab00ffRcq5m1wEF9MCvDE
|
||||||
|
8FUXk1HBHRAb/6t9QV/7axsPOkGT8SjQ1v2SCaiB0HQL3sYChYLi5zu4dfmQNPGq
|
||||||
|
ar9Xm5a0YqOQIFfmy8RSwxk0Q/ipNFTGN1uvlIRkgbT9zPnodxjWZsSI9BF+q5Af
|
||||||
|
uiE/oAk7MxfJ0LyLfhOWB+T98bKIOVtFT3wMLS1IIgMogwqCEXFf30Q9p2iTEzqT
|
||||||
|
6UE=
|
||||||
|
-----END CERTIFICATE REQUEST-----
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEArZYgiLzoyKzhRAdrk+sVtfY8G1Adsje3d6Q3UJ7BGyBD/Zfe
|
||||||
|
UJ5aEqavcIlhdWADur/bc85FACK5XrIMbZ1AiuN5qFYJdoKm1OLj2WN9VFNbYjGs
|
||||||
|
eR7+IDxOQO5ltYbNUtvxMHzeKkE4PjVKO6iag3gD+S2ch9s2pGqCOMezbDAkcN/E
|
||||||
|
+IJCXDI5rKMeZ2WHxyp9UTytYSbn/dMi7RfUnndJqaZHVJtSawsk2h/EVgwrWiAd
|
||||||
|
vQJoUQb1C65QM8mXkrvhGvoicxBko+ed7hbdwagAvE8cBxrvKJylIxKZ2yZ3qmrK
|
||||||
|
VzYV25ruj/B/RLBKHFLgDUOoD8dYsQxXoxIQXwIDAQABAoIBAG8bVJ+xKt6nqVg9
|
||||||
|
16HKKw9ZGIfy888K0qgFuFImCzwtntdGycmYUdb2Uf0aMgNK/ZgfDXxGXuwDTdtK
|
||||||
|
46GVsaY0i74vs8bjQZ2pzGVsxN+gqzFi0h6Es+w2LXBqJzfVnL6YgPykMB+jtzg6
|
||||||
|
K9Wbyaq0uvZXN4XNzl/WvJtTV4i7Cff1MOd5EhKFdqxrZvB/SRBCr/SMMafRtB9P
|
||||||
|
EvMneNKzhmlrutHAxuyxEKZR32Kkx7ydAdTjGgn+rE+NL5BweXfeWhLU4Bv14bn9
|
||||||
|
Mkneu3w5o1ryJfE2YnVajUP//jeopUT0nTQ3MpEusBQCLBlvFXjjM9uCaFX+5+MP
|
||||||
|
0H4xVcECgYEA1Q+wR3GHbk37vIGSlbENyUsri5WlMt8IVAHsDsTOpxAjYB0yyo+x
|
||||||
|
h9RS+RJZQECJlA6H72peUl3GM7RgdWIcKOT3nZ12XqYKG57rr/N5zlUuxbdS8KBk
|
||||||
|
JhyZeJdYjq/Jrno1ZP+OSmc7VvBLcM7irY7LHlvK0o8W1W0TNJ8jrZkCgYEA0JHX
|
||||||
|
lJd+fiezcUS7g4moHtzJp0JKquQiXLX+c2urmpyhb3ZrTuQ8OUjSy6DlwHlgDx8K
|
||||||
|
Hg2sdx/ZCuDaGjR4IY/Qs5RFt9WUqlK9gi9V3nYVrzBOQkdFOf/Ad3j4pQ8/aeCX
|
||||||
|
nP6snHXz1WqPpbCXG6l6GzFGbQU473GfuKsDuLcCgYAWQaNKc0OQdDj9whNL68ji
|
||||||
|
5CVSWXl+TOoTzHeaO1jS/s6TNbmei1AiPj3EovQL0DIO802j5tqfhAg2UntZB7yl
|
||||||
|
UPXE0zQQQwv/QqSgJrDsqt1N7g6N8FNF3+rwO+8WSKqqvT1ipYd5ojsCo+tdh18K
|
||||||
|
fkYdj70qLaRW+yPsdUtG0QKBgEYc8NqbvsML94+ZKmwCh4iwcf2PFGi0PjTqXTpR
|
||||||
|
tKNKCh7dMR+ZLAGZ0HrxgKqeYsNSjOUjdZmqFB1LDyaGAuhNXzwvGOy+mLZVEC3G
|
||||||
|
Wdhp28pDs9sl+EiSCBJhkTxzjr656F23YzFJmYlhxB5P6cw7wbeIbgNSIRylFqtO
|
||||||
|
mfarAoGBAICsAEWypOctxtmtOcjxgJ7jMbOA7rrsGlXpiy1/WlwIwRGF5LMvIIFX
|
||||||
|
qFAfiPcZn05ZgdAGzaFYowdjmQB10FW0jZbDf+nIHfOF5YmfmfWjsaweEGALJmqB
|
||||||
|
okGu/lGNGf3XoYzy0/hC3WAqk3znSZtQLUq8jEWF7dLNUizUeUow
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEArZYgiLzoyKzhRAdrk+sVtfY8G1Adsje3d6Q3UJ7BGyBD/Zfe
|
||||||
|
UJ5aEqavcIlhdWADur/bc85FACK5XrIMbZ1AiuN5qFYJdoKm1OLj2WN9VFNbYjGs
|
||||||
|
eR7+IDxOQO5ltYbNUtvxMHzeKkE4PjVKO6iag3gD+S2ch9s2pGqCOMezbDAkcN/E
|
||||||
|
+IJCXDI5rKMeZ2WHxyp9UTytYSbn/dMi7RfUnndJqaZHVJtSawsk2h/EVgwrWiAd
|
||||||
|
vQJoUQb1C65QM8mXkrvhGvoicxBko+ed7hbdwagAvE8cBxrvKJylIxKZ2yZ3qmrK
|
||||||
|
VzYV25ruj/B/RLBKHFLgDUOoD8dYsQxXoxIQXwIDAQABAoIBAG8bVJ+xKt6nqVg9
|
||||||
|
16HKKw9ZGIfy888K0qgFuFImCzwtntdGycmYUdb2Uf0aMgNK/ZgfDXxGXuwDTdtK
|
||||||
|
46GVsaY0i74vs8bjQZ2pzGVsxN+gqzFi0h6Es+w2LXBqJzfVnL6YgPykMB+jtzg6
|
||||||
|
K9Wbyaq0uvZXN4XNzl/WvJtTV4i7Cff1MOd5EhKFdqxrZvB/SRBCr/SMMafRtB9P
|
||||||
|
EvMneNKzhmlrutHAxuyxEKZR32Kkx7ydAdTjGgn+rE+NL5BweXfeWhLU4Bv14bn9
|
||||||
|
Mkneu3w5o1ryJfE2YnVajUP//jeopUT0nTQ3MpEusBQCLBlvFXjjM9uCaFX+5+MP
|
||||||
|
0H4xVcECgYEA1Q+wR3GHbk37vIGSlbENyUsri5WlMt8IVAHsDsTOpxAjYB0yyo+x
|
||||||
|
h9RS+RJZQECJlA6H72peUl3GM7RgdWIcKOT3nZ12XqYKG57rr/N5zlUuxbdS8KBk
|
||||||
|
JhyZeJdYjq/Jrno1ZP+OSmc7VvBLcM7irY7LHlvK0o8W1W0TNJ8jrZkCgYEA0JHX
|
||||||
|
lJd+fiezcUS7g4moHtzJp0JKquQiXLX+c2urmpyhb3ZrTuQ8OUjSy6DlwHlgDx8K
|
||||||
|
Hg2sdx/ZCuDaGjR4IY/Qs5RFt9WUqlK9gi9V3nYVrzBOQkdFOf/Ad3j4pQ8/aeCX
|
||||||
|
nP6snHXz1WqPpbCXG6l6GzFGbQU473GfuKsDuLcCgYAWQaNKc0OQdDj9whNL68ji
|
||||||
|
5CVSWXl+TOoTzHeaO1jS/s6TNbmei1AiPj3EovQL0DIO802j5tqfhAg2UntZB7yl
|
||||||
|
UPXE0zQQQwv/QqSgJrDsqt1N7g6N8FNF3+rwO+8WSKqqvT1ipYd5ojsCo+tdh18K
|
||||||
|
fkYdj70qLaRW+yPsdUtG0QKBgEYc8NqbvsML94+ZKmwCh4iwcf2PFGi0PjTqXTpR
|
||||||
|
tKNKCh7dMR+ZLAGZ0HrxgKqeYsNSjOUjdZmqFB1LDyaGAuhNXzwvGOy+mLZVEC3G
|
||||||
|
Wdhp28pDs9sl+EiSCBJhkTxzjr656F23YzFJmYlhxB5P6cw7wbeIbgNSIRylFqtO
|
||||||
|
mfarAoGBAICsAEWypOctxtmtOcjxgJ7jMbOA7rrsGlXpiy1/WlwIwRGF5LMvIIFX
|
||||||
|
qFAfiPcZn05ZgdAGzaFYowdjmQB10FW0jZbDf+nIHfOF5YmfmfWjsaweEGALJmqB
|
||||||
|
okGu/lGNGf3XoYzy0/hC3WAqk3znSZtQLUq8jEWF7dLNUizUeUow
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDKjCCAhICCQDWE0gtDvld0DANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJB
|
||||||
|
VTETMBEGA1UECBMKU29tZS1TdGF0ZTEbMBkGA1UEChMSTXkgQm9ndXMgUm9vdCBD
|
||||||
|
ZXJ0MRQwEgYDVQQDEwtleGFtcGxlLmNvbTAeFw0xNTEyMDQyMDU0MzFaFw00MDEy
|
||||||
|
MDMyMDU0MzFaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEw
|
||||||
|
HwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2Fs
|
||||||
|
aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK2WIIi86Mis4UQH
|
||||||
|
a5PrFbX2PBtQHbI3t3ekN1CewRsgQ/2X3lCeWhKmr3CJYXVgA7q/23PORQAiuV6y
|
||||||
|
DG2dQIrjeahWCXaCptTi49ljfVRTW2IxrHke/iA8TkDuZbWGzVLb8TB83ipBOD41
|
||||||
|
SjuomoN4A/ktnIfbNqRqgjjHs2wwJHDfxPiCQlwyOayjHmdlh8cqfVE8rWEm5/3T
|
||||||
|
Iu0X1J53SammR1SbUmsLJNofxFYMK1ogHb0CaFEG9QuuUDPJl5K74Rr6InMQZKPn
|
||||||
|
ne4W3cGoALxPHAca7yicpSMSmdsmd6pqylc2Fdua7o/wf0SwShxS4A1DqA/HWLEM
|
||||||
|
V6MSEF8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAz5sMAFG6W/ZEULZITkBTCU6P
|
||||||
|
NttpGiKufnqyBW5HyNylaczfnHnClvQjr8f/84xvKVcfC3xP0lz+92aIQqo+5L/n
|
||||||
|
v7gLhBFR4Vr2XwMt2qz2FpkaxmVwnhVAHaaC05WIKQ6W2gDwWT0u1K8YdTh+7mvN
|
||||||
|
AT9FW4vDgtNZWq4W/PePh9QCiOOQhGOuBYj/7zqLtz4XPifhi66ILIRDHiu0kond
|
||||||
|
3YMFcECIAf4MPT9vT0iNcWX+c8CfAixPt8nMD6bzOo3oTcfuZh/2enfgLbMqOlOi
|
||||||
|
uk72FM5VVPXTWAckJvL/vVjqsvDuJQKqbr0oUc3bdWbS36xtWZUycp4IQLguAQ==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
BIN
Binary file not shown.
@@ -0,0 +1,8 @@
|
|||||||
|
from sys import argv, stderr
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Act like letsencrypt --version insofar as printing the version number to
|
||||||
|
stderr."""
|
||||||
|
if '--version' in argv:
|
||||||
|
stderr.write('letsencrypt 99.9.9\n')
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='letsencrypt',
|
||||||
|
version='99.9.9',
|
||||||
|
description='A mock version of letsencrypt that just prints its version',
|
||||||
|
py_modules=['letsencrypt'],
|
||||||
|
entry_points={
|
||||||
|
'console_scripts': ['letsencrypt = letsencrypt:main']
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEpQIBAAKCAQEAsMoSzLYQ7E1sdSOkwelgtzKIh2qi3bpXuYtcfFC0XrvWig07
|
||||||
|
1NwIj+dZiT0OLZ2hPispEH0B7ISuuWg1ll7GhFW0VdbxL6JdGzS2ShNWkX9hE9z+
|
||||||
|
j8VqwDPOBn3ZHm03qwpYkBDwQib3KqOdYbTTuUtJmmGcuk3a9Aq/sCT6DdfmTSdP
|
||||||
|
5asdQYwIcaQreDrOosaS84DTWI3IU+UYJVglLsIVPBuy9IcgHidUQ96hJnoPsDCW
|
||||||
|
sHwX62495QKEarauyKQrJzFes0EY95orDM47Z5o/NDiQB11m91yNB0MmPYY9QSbn
|
||||||
|
OA9j7IaaC97AwRLuwXY+/R2ablTcxurWou68iQIDAQABAoIBAQCJE3W2Mqk2f+XL
|
||||||
|
geKa1BjAkzcXQJCduYGRhUQlw/HGzoBPtGki56Tf53MeHTAkIGfIq3CAr1zRhiNv
|
||||||
|
8SQzvrLQIx/buvhxhcQJdzqsfwgNcqXT3/OliF34P3LMx8GUfPy/6xq2Qdv4fvwA
|
||||||
|
nLJH8wyDTKP6RxtdvUY7GSZ+Ln2QQv/3Nco7tax4GHNGom8iSgeH/YKTDnvitdqh
|
||||||
|
a0fr930QzU39TfOftLmasdmKUOIg8G2wr4Sy6Kn060+OUoQr1fZF5mnLvvQeILCK
|
||||||
|
uav91JkIeMLggzk+t88IJUFWdOoxv5hWTnNzHyt+/GYfovyRz2fKQMwzdh1F8iM5
|
||||||
|
+867rEb9AoGBANn1ncemJBedDshStdCBUH0+2ExPrawveaXOZKnx8/VGFXNi0hAf
|
||||||
|
KzkntMWd5g5kB077FtKO9CYTBvK4pZBWIFLcJEqAz88JeXME6dfUbRucDr72ko+l
|
||||||
|
rcLHXj7F0IDVzj/9CphMGAhC9J/4YW9SPcSbMw6dQ6xOk73f1Vowve0DAoGBAM+k
|
||||||
|
/F+hVqCS3f22Bg9KuDtx+zCydaZxC842DgIkV1SO2iFhNHjnpQ5EIR0WrSYeV2n+
|
||||||
|
rD7kVs5OH1HvnGScHaQKtAVqZClSwF14jzE+Aj8XDwxiHLSOhJgKlzfVX7h1ymMh
|
||||||
|
7fsslDl6xNGQ+40gubhkCLT5qABFKy1mrZ8b+3yDAoGAGLGUI6d2FVrM7vM3+Bx+
|
||||||
|
gwIYvWSVl5l1XcypaPupmRNMoNsEU6FEY2BVQcJm6yB4F4GpD0f0709ejSdQUq7/
|
||||||
|
UIPydKJtaNZ49QgMelBt4B/pJ8eFyVKLAjNWQSRmQAJ5MJS5m5Gbc2wqjOk2GMen
|
||||||
|
idvPiAtXPHFWmb9/S42UJwMCgYEAjymAe2qgcGtyNNfIC8kHhqzKdEPGi/ALJKzu
|
||||||
|
MZnewEURrcv4QpfrnA9rCUQ2Mz7eJA1bsqz6EJmaTIK4wEFGynA6uDUnQ7pzOL7D
|
||||||
|
cz7+i4MZc/89LVvJnY5Hvk4WBfboiDq/etq8g3jatGaSmTYD9la6DhTHORB3eYD+
|
||||||
|
meHQHYMCgYEA18y9hnx2k4vNeBei4YXF4pAvKdwKLQD+CcP9ljb3VT+kXktjRA1C
|
||||||
|
aWj3HhMwvcxtttfkQzEnwwGRAkTEtNewJ8KFxhmc9nYElZTNZ+SuHD5Dkv8xqoj8
|
||||||
|
NvG8rU1eiEyPwE2wQxpM5JLqbo7IWtR0dmptjKoF1gRxn6Wh4TwEiHA=
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
@@ -63,4 +63,5 @@ setup(
|
|||||||
'nginx = letsencrypt_nginx.configurator:NginxConfigurator',
|
'nginx = letsencrypt_nginx.configurator:NginxConfigurator',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
test_suite='letsencrypt_nginx',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -998,6 +998,13 @@ def prepare_and_parse_args(plugins, args):
|
|||||||
"automation", "--duplicate", dest="duplicate", action="store_true",
|
"automation", "--duplicate", dest="duplicate", action="store_true",
|
||||||
help="Allow making a certificate lineage that duplicates an existing one "
|
help="Allow making a certificate lineage that duplicates an existing one "
|
||||||
"(both can be renewed in parallel)")
|
"(both can be renewed in parallel)")
|
||||||
|
helpful.add(
|
||||||
|
"automation", "--os-packages-only", action="store_true",
|
||||||
|
help="(letsencrypt-auto only) install OS package dependencies and then stop")
|
||||||
|
helpful.add(
|
||||||
|
"automation", "--no-self-upgrade", action="store_true",
|
||||||
|
help="(letsencrypt-auto only) prevent the letsencrypt-auto script from"
|
||||||
|
" upgrading itself to newer released versions")
|
||||||
|
|
||||||
helpful.add_group(
|
helpful.add_group(
|
||||||
"testing", description="The following flags are meant for "
|
"testing", description="The following flags are meant for "
|
||||||
|
|||||||
@@ -55,4 +55,5 @@ setup(
|
|||||||
'letshelp-letsencrypt-apache = letshelp_letsencrypt.apache:main',
|
'letshelp-letsencrypt-apache = letshelp_letsencrypt.apache:main',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
test_suite='letshelp_letsencrypt',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ install_requires = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# env markers in extras_require cause problems with older pip: #517
|
# env markers in extras_require cause problems with older pip: #517
|
||||||
|
# Keep in sync with conditional_requirements.py.
|
||||||
if sys.version_info < (2, 7):
|
if sys.version_info < (2, 7):
|
||||||
install_requires.extend([
|
install_requires.extend([
|
||||||
# only some distros recognize stdlib argparse as already satisfying
|
# only some distros recognize stdlib argparse as already satisfying
|
||||||
@@ -56,9 +57,7 @@ if sys.version_info < (2, 7):
|
|||||||
'mock<1.1.0',
|
'mock<1.1.0',
|
||||||
])
|
])
|
||||||
else:
|
else:
|
||||||
install_requires.extend([
|
install_requires.append('mock')
|
||||||
'mock',
|
|
||||||
])
|
|
||||||
|
|
||||||
dev_extras = [
|
dev_extras = [
|
||||||
# Pin astroid==1.3.5, pylint==1.4.2 as a workaround for #289
|
# Pin astroid==1.3.5, pylint==1.4.2 as a workaround for #289
|
||||||
@@ -121,7 +120,6 @@ setup(
|
|||||||
'testing': testing_extras,
|
'testing': testing_extras,
|
||||||
},
|
},
|
||||||
|
|
||||||
tests_require=install_requires,
|
|
||||||
# to test all packages run "python setup.py test -s
|
# to test all packages run "python setup.py test -s
|
||||||
# {acme,letsencrypt_apache,letsencrypt_nginx}"
|
# {acme,letsencrypt_apache,letsencrypt_nginx}"
|
||||||
test_suite='letsencrypt',
|
test_suite='letsencrypt',
|
||||||
|
|||||||
+9
-6
@@ -9,18 +9,21 @@
|
|||||||
// This program can be used to perform RSA public key signatures given only
|
// This program can be used to perform RSA public key signatures given only
|
||||||
// the hash of the file to be signed as input.
|
// the hash of the file to be signed as input.
|
||||||
|
|
||||||
// Sign with SHA1
|
// To compile:
|
||||||
#define HASH_SIZE 20
|
// gcc half-sign.c -lssl -lcrypto -o half-sign
|
||||||
|
|
||||||
|
// Sign with SHA256
|
||||||
|
#define HASH_SIZE 32
|
||||||
|
|
||||||
void usage() {
|
void usage() {
|
||||||
printf("half-sign <private key file> [binary hash file]\n");
|
printf("half-sign <private key file> [binary hash file]\n");
|
||||||
printf("\n");
|
printf("\n");
|
||||||
printf(" Computes and prints a binary RSA signature over data given the SHA1 hash of\n");
|
printf(" Computes and prints a binary RSA signature over data given the SHA256 hash of\n");
|
||||||
printf(" the data as input.\n");
|
printf(" the data as input.\n");
|
||||||
printf("\n");
|
printf("\n");
|
||||||
printf(" <private key file> should be PEM encoded.\n");
|
printf(" <private key file> should be PEM encoded.\n");
|
||||||
printf("\n");
|
printf("\n");
|
||||||
printf(" The input SHA1 hash should be %d bytes in length. If no binary hash file is\n", HASH_SIZE);
|
printf(" The input SHA256 hash should be %d bytes in length. If no binary hash file is\n", HASH_SIZE);
|
||||||
printf(" specified, it will be read from stdin.\n");
|
printf(" specified, it will be read from stdin.\n");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
@@ -38,7 +41,7 @@ void sign_hashed_data(EVP_PKEY *signing_key, unsigned char *md, size_t mdlen) {
|
|||||||
if ((!ctx)
|
if ((!ctx)
|
||||||
|| (EVP_PKEY_sign_init(ctx) <= 0)
|
|| (EVP_PKEY_sign_init(ctx) <= 0)
|
||||||
|| (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) <= 0)
|
|| (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) <= 0)
|
||||||
|| (EVP_PKEY_CTX_set_signature_md(ctx, EVP_sha1()) <= 0)) {
|
|| (EVP_PKEY_CTX_set_signature_md(ctx, EVP_sha256()) <= 0)) {
|
||||||
fprintf(stderr, "Failure establishing ctx for signature\n");
|
fprintf(stderr, "Failure establishing ctx for signature\n");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
@@ -105,7 +108,7 @@ int main(int argc, char *argv[]) {
|
|||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
if (fread(buffer, HASH_SIZE, 1, input) != 1) {
|
if (fread(buffer, HASH_SIZE, 1, input) != 1) {
|
||||||
perror("half-sign: Failed to read SHA1 from input\n");
|
perror("half-sign: Failed to read SHA256 from input\n");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Executable
+51
@@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
|
||||||
|
if ! `which festival > /dev/null` ; then
|
||||||
|
echo Please install \'festival\'!
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
function sayhash { # $1 <-- HASH ; $2 <---SIGFILEBALL
|
||||||
|
while read -p "Press Enter to read the hash aloud or type 'done': " INP && [ "$INP" = "" ] ; do
|
||||||
|
cat $1 | (echo "(Parameter.set 'Duration_Stretch 1.5)"; \
|
||||||
|
echo -n '(SayText "'; \
|
||||||
|
sha1sum | cut -c1-40 | fold -1 | sed 's/^a$/alpha/; s/^b$/bravo/; s/^c$/charlie/; s/^d$/delta/; s/^e$/echo/; s/^f$/foxtrot/'; \
|
||||||
|
echo '")' ) | festival
|
||||||
|
done
|
||||||
|
|
||||||
|
echo 'Paste in the data from the QR code, then type Ctrl-D:'
|
||||||
|
cat > $2
|
||||||
|
}
|
||||||
|
|
||||||
|
function offlinesign { # $1 <-- INPFILE ; $2 <---SIGFILE
|
||||||
|
echo HASH FOR SIGNING:
|
||||||
|
SIGFILEBALL="$2.lzma.base64"
|
||||||
|
#echo "(place the resulting raw binary signature in $SIGFILEBALL)"
|
||||||
|
sha1sum $1
|
||||||
|
echo metahash for confirmation only $(sha1sum $1 |cut -d' ' -f1 | tr -d '\n' | sha1sum | cut -c1-6) ...
|
||||||
|
echo
|
||||||
|
sayhash $1 $SIGFILEBALL
|
||||||
|
}
|
||||||
|
|
||||||
|
function oncesigned { # $1 <-- INPFILE ; $2 <--SIGFILE
|
||||||
|
SIGFILEBALL="$2.lzma.base64"
|
||||||
|
cat $SIGFILEBALL | tr -d '\r' | base64 -d | unlzma -c > $2 || exit 1
|
||||||
|
if ! [ -f $2 ] ; then
|
||||||
|
echo "Failed to find $2"'!'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if file $2 | grep -qv " data" ; then
|
||||||
|
echo "WARNING WARNING $2 does not look like a binary signature:"
|
||||||
|
echo `file $2`
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
HERE=`dirname $0`
|
||||||
|
LEAUTO="`realpath $HERE`/../letsencrypt-auto-source/letsencrypt-auto"
|
||||||
|
SIGFILE="$LEAUTO".sig
|
||||||
|
offlinesign $LEAUTO $SIGFILE
|
||||||
|
oncesigned $LEAUTO $SIGFILE
|
||||||
@@ -34,6 +34,9 @@ else
|
|||||||
echo Releasing developer version "$version"...
|
echo Releasing developer version "$version"...
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$RELEASE_OPENSSL_PUBKEY" = "" ] ; then
|
||||||
|
RELEASE_OPENSSL_PUBKEY="`realpath \`dirname $0\``/eff-pubkey.pem"
|
||||||
|
fi
|
||||||
RELEASE_GPG_KEY=${RELEASE_GPG_KEY:-A2CFB51FA275A7286234E7B24D17C995CD9775F2}
|
RELEASE_GPG_KEY=${RELEASE_GPG_KEY:-A2CFB51FA275A7286234E7B24D17C995CD9775F2}
|
||||||
# Needed to fix problems with git signatures and pinentry
|
# Needed to fix problems with git signatures and pinentry
|
||||||
export GPG_TTY=$(tty)
|
export GPG_TTY=$(tty)
|
||||||
@@ -78,6 +81,21 @@ if [ "$RELEASE_BRANCH" != "candidate-$version" ] ; then
|
|||||||
fi
|
fi
|
||||||
git checkout "$RELEASE_BRANCH"
|
git checkout "$RELEASE_BRANCH"
|
||||||
|
|
||||||
|
# ensure we have the latest built version of leauto
|
||||||
|
letsencrypt-auto-source/build.py
|
||||||
|
|
||||||
|
# and that it's signed correctly
|
||||||
|
if ! openssl dgst -sha256 -verify $RELEASE_OPENSSL_PUBKEY -signature \
|
||||||
|
letsencrypt-auto-source/letsencrypt-auto.sig \
|
||||||
|
letsencrypt-auto-source/letsencrypt-auto ; then
|
||||||
|
echo Failed letsencrypt-auto signature check on "$RELEASE_BRANCH"
|
||||||
|
echo please fix that and re-run
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo Signature check on letsencrypt-auto successful
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
SetVersion() {
|
SetVersion() {
|
||||||
ver="$1"
|
ver="$1"
|
||||||
for pkg_dir in $SUBPKGS letsencrypt-compatibility-test
|
for pkg_dir in $SUBPKGS letsencrypt-compatibility-test
|
||||||
@@ -115,6 +133,7 @@ do
|
|||||||
cd -
|
cd -
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
||||||
mkdir "dist.$version"
|
mkdir "dist.$version"
|
||||||
mv dist "dist.$version/letsencrypt"
|
mv dist "dist.$version/letsencrypt"
|
||||||
for pkg_dir in $SUBPKGS
|
for pkg_dir in $SUBPKGS
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ envlist = py{26,27,33,34,35},py{26,27}-oldest,cover,lint
|
|||||||
# loops, especially on Travis
|
# loops, especially on Travis
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
# packages installed separately to ensure that dowstream deps problems
|
# packages installed separately to ensure that downstream deps problems
|
||||||
# are detected, c.f. #1002
|
# are detected, c.f. #1002
|
||||||
commands =
|
commands =
|
||||||
pip install -e acme[testing]
|
pip install -e acme[testing]
|
||||||
@@ -82,3 +82,13 @@ setenv =
|
|||||||
commands =
|
commands =
|
||||||
pip install -e acme -e .[dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt
|
pip install -e acme -e .[dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt
|
||||||
sudo ./letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/apache-conf-test --debian-modules
|
sudo ./letsencrypt-apache/letsencrypt_apache/tests/apache-conf-files/apache-conf-test --debian-modules
|
||||||
|
|
||||||
|
[testenv:le_auto]
|
||||||
|
# At the moment, this tests under Python 2.7 only, as only that version is
|
||||||
|
# readily available on the Trusty Docker image.
|
||||||
|
commands =
|
||||||
|
docker build -t lea letsencrypt-auto-source
|
||||||
|
docker run --rm -t -i lea
|
||||||
|
whitelist_externals =
|
||||||
|
docker
|
||||||
|
passenv = DOCKER_*
|
||||||
Reference in New Issue
Block a user