diff --git a/.gitignore b/.gitignore index 341843f98..38c95986c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ dist*/ /venv*/ /kgs/ /.tox/ +/releases/ letsencrypt.log # coverage diff --git a/.travis.yml b/.travis.yml index d5d18737a..6b325e985 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,6 +43,7 @@ matrix: env: TOXENV=le_auto services: docker before_install: + addons: - python: "2.7" env: TOXENV=cover - python: "3.3" diff --git a/acme/setup.py b/acme/setup.py index 5a77f8a67..0843288e6 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -18,7 +18,9 @@ install_requires = [ 'pyrfc3339', 'pytz', 'requests', - 'setuptools', # pkg_resources + # For pkg_resources. >=1.0 so pip resolves it to a version cryptography + # will tolerate; see #2599: + 'setuptools>=1.0', 'six', ] diff --git a/docs/using.rst b/docs/using.rst index fd736c2f9..04b239958 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -81,6 +81,21 @@ manual_ Y N Helps you obtain a cert by giving you instructions to perf nginx_ Y Y Very experimental and not included in letsencrypt-auto_. =========== ==== ==== =============================================================== +There are also a number of third-party plugins for the client, provided by other developers: + +=========== ==== ==== =============================================================== +Plugin Auth Inst Notes +=========== ==== ==== =============================================================== +plesk_ Y Y Integration with the Plesk web hosting tool + https://github.com/plesk/letsencrypt-plesk +haproxy_ Y Y Integration with the HAProxy load balancer + https://code.greenhost.net/open/letsencrypt-haproxy +s3front_ Y Y Integration with Amazon CloudFront distribution of S3 buckets + https://github.com/dlapiduz/letsencrypt-s3front +gandi_ Y Y Integration with Gandi's hosting products and API + https://github.com/Gandi/letsencrypt-gandi +=========== ==== ==== =============================================================== + Future plugins for IMAP servers, SMTP servers, IRC servers, etc, are likely to be installers but not authenticators. diff --git a/letsencrypt-apache/setup.py b/letsencrypt-apache/setup.py index a8e010f0e..46f4da54c 100644 --- a/letsencrypt-apache/setup.py +++ b/letsencrypt-apache/setup.py @@ -11,7 +11,9 @@ install_requires = [ 'acme=={0}'.format(version), 'letsencrypt=={0}'.format(version), 'python-augeas', - 'setuptools', # pkg_resources + # For pkg_resources. >=1.0 so pip resolves it to a version cryptography + # will tolerate; see #2599: + 'setuptools>=1.0', 'zope.component', 'zope.interface', ] diff --git a/letsencrypt-auto b/letsencrypt-auto index 9218bdc52..8dda5f183 100755 --- a/letsencrypt-auto +++ b/letsencrypt-auto @@ -18,25 +18,31 @@ set -e # Work even if somebody does "sh thisscript.sh". 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="0.4.0" +VENV_BIN="$VENV_PATH/bin" +LE_AUTO_VERSION="0.4.2" # 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 + case "$arg" in + --debug) + DEBUG=1;; + --os-packages-only) + OS_PACKAGES_ONLY=1;; + --no-self-upgrade) + # 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;; + --verbose) + VERBOSE=1;; + [!-]*|-*[!v]*|-) + # Anything that isn't -v, -vv, etc.: that is, anything that does not + # start with a -, contains anything that's not a v, or is just "-" + ;; + *) # -v+ remains. + VERBOSE=1;; + esac done # letsencrypt-auto needs root access to bootstrap OS dependencies, and @@ -91,21 +97,18 @@ ExperimentalBootstrap() { } 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!" + for LE_PYTHON in "$LE_PYTHON" python2.7 python27 python2 python; do + # Break (while keeping the LE_PYTHON value) if found. + command -v "$LE_PYTHON" > /dev/null && break + done + if [ "$?" != "0" ]; then + echo "Cannot find any Pythons; please install one!" exit 1 fi + export LE_PYTHON - PYVER=`"$LE_PYTHON" --version 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'` - if [ $PYVER -lt 26 ]; then + PYVER=`"$LE_PYTHON" -V 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'` + if [ "$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 @@ -165,7 +168,7 @@ BootstrapDebCommon() { /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 sh -c "echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/$BACKPORT_NAME.list" $SUDO apt-get update fi fi @@ -304,10 +307,11 @@ BootstrapArchCommon() { pkg-config " - missing=$("$SUDO" pacman -T $deps) + # pacman -T exits with 127 if there are missing dependencies + missing=$($SUDO pacman -T $deps) || true if [ "$missing" ]; then - "$SUDO" pacman -S --needed $missing + $SUDO pacman -S --needed $missing fi } @@ -324,19 +328,19 @@ BootstrapGentooCommon() { case "$PACKAGE_MANAGER" in (paludis) - "$SUDO" cave resolve --keep-targets if-possible $PACKAGES -x + $SUDO cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x ;; (pkgcore) - "$SUDO" pmerge --noreplace $PACKAGES + $SUDO pmerge --noreplace --oneshot $PACKAGES ;; (portage|*) - "$SUDO" emerge --noreplace $PACKAGES + $SUDO emerge --noreplace --oneshot $PACKAGES ;; esac } BootstrapFreeBsd() { - "$SUDO" pkg install -Ay \ + $SUDO pkg install -Ay \ python \ py27-virtualenv \ augeas \ @@ -345,20 +349,27 @@ BootstrapFreeBsd() { BootstrapMac() { if ! hash brew 2>/dev/null; then - echo "Homebrew Not Installed\nDownloading..." + 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 [ -z "$(brew list --versions augeas)" ]; then + echo "augeas not installed.\nInstalling augeas from Homebrew..." + brew install augeas + fi - if ! hash pip 2>/dev/null; then - echo "pip Not Installed\nInstalling python from Homebrew..." + if [ -z "$(brew list --versions dialog)" ]; then + echo "dialog not installed.\nInstalling dialog from Homebrew..." + brew install dialog + fi + + if [ -z "$(brew list --versions python)" ]; then + echo "python not installed.\nInstalling python from Homebrew..." brew install python fi if ! hash virtualenv 2>/dev/null; then - echo "virtualenv Not Installed\nInstalling with pip" + echo "virtualenv not installed.\nInstalling with pip..." pip install virtualenv fi } @@ -410,11 +421,25 @@ TempDir() { mktemp -d 2>/dev/null || mktemp -d -t 'le' # Linux || OS X } +InstallRequirements() { + set +e + PEEP_OUT=`"$VENV_BIN/python" "$TEMP_DIR/peep.py" install -r "$TEMP_DIR/$1"` + PEEP_STATUS=$? + set -e + if [ "$PEEP_STATUS" != 0 ]; then + # Report error. (Otherwise, be quiet.) + echo "Had a problem while downloading and verifying Python packages:" + echo "$PEEP_OUT" + rm -rf "$VENV_PATH" + exit 1 + fi +} -if [ "$NO_SELF_UPGRADE" = 1 ]; then +if [ "$1" = "--le-auto-phase2" ]; then # Phase 2: Create venv, install LE, and run. + shift 1 # the --le-auto-phase2 arg if [ -f "$VENV_BIN/letsencrypt" ]; then INSTALLED_VERSION=$("$VENV_BIN/letsencrypt" --version 2>&1 | cut -d " " -f 2) else @@ -432,7 +457,17 @@ if [ "$NO_SELF_UPGRADE" = 1 ]; then echo "Installing Python packages..." TEMP_DIR=$(TempDir) + trap "rm -rf '$TEMP_DIR'" EXIT # There is no $ interpolation due to quotes on starting heredoc delimiter. + # ------------------------------------------------------------------------- + cat << "UNLIKELY_EOF" > "$TEMP_DIR/setuptools-requirements.txt" +# cryptography requires a more modern version of setuptools. +# sha256: _ANFf7h6utSdwJ-cMTOGNpPn3bbKgrtQpzmnc3nOWpo +# sha256: JPz8FTZKn-CaIg830tztyEl5Xj3j5LOT7piOZqnL2Fo +# sha256: gJaELiTE8ddN_xKr6Qwm0S8F0NmlbtXgb8qm-qHkC2o +setuptools==20.2.2 + +UNLIKELY_EOF # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt" # This is the flattened list of packages letsencrypt-auto installs. To generate @@ -443,6 +478,11 @@ if [ "$NO_SELF_UPGRADE" = 1 ]; then # sha256: YrCJpVvh2JSc0rx-DfC9254Cj678jDIDjMhIYq791uQ argparse==1.4.0 +# This comes before cffi because cffi will otherwise install an unchecked +# version via setup_requires. +# sha256: eVm0p0q9wnsxL-0cIebK-TCc4LKeqGtZH9Lpns3yf3M +pycparser==2.14 + # sha256: U8HJ3bMEMVE-t_PN7wo-BrDxJSGIqqd0SvD1pM1F268 # sha256: pWj0nfyhKo2fNwGHJX78WKOBCeHu5xTZKFYdegGKZPg # sha256: gJxsqM-8ruv71DK0V2ABtA04_yRjdzy1dXfXXhoCC8M @@ -467,28 +507,28 @@ 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: Axk49zpcXrPoCeGP98rraGU1GHFBe-YFDLjIapogK5o +# sha256: oXmjjVD41otJHXoxPbePjKvikIQs7N3dx7NNQI5Z2wo +# sha256: kGyIsqrc-Zz6uyQJgmPRv2WrDIaIrN4Q2uHwnYZZIPE +# sha256: bnBsXGCIdwsdG2NOlZ4hlj4xWwJV9fR3cSWtPVQIKXc +# sha256: 9ev44xxI-HB5Idyg6ZTed4E6nJub8DwRnF3fl73P_nM +# sha256: x7ieQiiMx_vuOBLpnvXHRPIkUuEdaCL2gHr8bWs76D4 +# sha256: hAjSmGWUcQnYto8YN6fN4apNyG4Peco7pYwMRORD1qU +# sha256: x-ds88PZJd0x-iOM-4Bs_7pxjA8IcH13pTh2hHeWmVY +# sha256: fY3jU4DzFwJ1i3dTu1xAcjgyxzAG3tsvkJm_YaN_coc +# sha256: XtvucfrlRp7oP-CjeGa5OYyM46RjJcJPzt-_CXu0ihk +# sha256: WU7a_kgBwTvcHMMF53BKkMGWF-lZNvarRX7k_-AAulA +# sha256: t_2xagp_SBvkLadEv-HqIWMCXeIfkPLGiKMW88NU2pw +# sha256: IHuL8P4JBzNt84tzO0h1Ic-eE4GJq6kjStVP5UXdDbg +# sha256: UJovBThicM94OZPJDUn_77PdYq7kW_HqjOPSzecnHCE +# sha256: rGm2XdGvAXnt5AyfFXiMiPc-Yo6mwFGd44OOJ5uziMY +# sha256: jfb61sauEv1wBOopNX8KK003dOrsp2VlMNCNLZDNQao +# sha256: C4uW3YHMFTOgTzA4LA_iHBly4Yn3lNDEJhoYzsCP2bU +# sha256: yuj8oYg_I8UOp42J3m_k_v20zqgxd3YPRxd1WUFN7ZM +# sha256: GkccpXapzc4bHNnzoisdCe5E1GhiA3VX3heRnA20RCU +# sha256: jsTo49RTs6G2O19Xc3pDTc8e5KLyb2_3xaN8P2eRBNI +# sha256: jrEcd92Oc_SN9rL3p-Fhc_4P6P3-JmIygy6IR34IRU4 +cryptography==1.2.3 # sha256: JHXX_N31lR6S_1RpcnWIAt5SYL9Akxmp8ZNOa7yLHcc # sha256: NZB977D5krdat3iPZf7cHPIP-iJojg5vbxKvwGs-pQE @@ -516,9 +556,9 @@ ndg-httpsclient==0.4.0 # sha256: HDW0rCBs7y0kgWyJ-Jzyid09OM98RJuz-re_bUPwGx8 ordereddict==1.1 -# sha256: OnTxAPkNZZGDFf5kkHca0gi8PxOv0y01_P5OjQs7gSs -# sha256: Paa-K-UG9ZzOMuGeMOIBBT4btNB-JWaJGOAPikmtQKs -parsedatetime==1.5 +# sha256: zp1CIWXPbpY5Bc1fdPJ06_fMmMlBkWFpF475Pw5VeDg +# sha256: F8V4d1UgyZExY04Jz8paBeqeG9KgXNBpZ-vs4Q33ry0 +parsedatetime==2.1 # sha256: Rsjbda51oFa9HMB_ohc0_i5gPRGgeDPswe63TDXHLgw # sha256: 4hJ2JqkebIhduJZol22zECDwry2nKJJLVkgPx8zwlkk @@ -560,9 +600,6 @@ psutil==3.3.0 # sha256: hTys2W0fcB3dZ6oD7MBfUYkBNbcmLpInEBEvEqLtKn8 pyasn1==0.1.9 -# sha256: eVm0p0q9wnsxL-0cIebK-TCc4LKeqGtZH9Lpns3yf3M -pycparser==2.14 - # sha256: iORea7Jd_tJyoe8ucoRh1EtjTCzWiemJtuVqNJxaOuU # sha256: 8KJgcNbbCIHei8x4RpNLfDyTDY-cedRYg-5ImEvA1nI pyOpenSSL==0.15.1 @@ -609,10 +646,6 @@ traceback2==1.4.0 # sha256: IogqDkGMKE4fcYqCKzsCKUTVPS2QjhaQsxmp0-ssBXk unittest2==1.1.0 -# sha256: aUkbUwUVfDxuDwSnAZhNaud_1yn8HJrNJQd_HfOFMms -# sha256: 619wCpv8lkILBVY1r5AC02YuQ9gMP_0x8iTCW8DV9GI -Werkzeug==0.11.3 - # sha256: KCwRK1XdjjyGmjVx-GdnwVCrEoSprOK97CJsWSrK-Bo zope.component==4.2.2 @@ -638,22 +671,25 @@ zope.event==4.1.0 # sha256: sJyMHUezUxxADgGVaX8UFKYyId5u9HhZik8UYPfZo5I zope.interface==4.1.3 -# sha256: ilvjjTWOS86xchl0WBZ0YOAw_0rmqdnjNsxb1hq2RD8 -# sha256: T37KMj0TnsuvHIzCCmoww2fpfpOBTj7cd4NAqucXcpw -acme==0.4.0 - -# sha256: 33BQiANlNLGqGpirTfdCEElTF9YbpaKiYpTbK4zeGD8 -# sha256: lwsV1OdEzzlMeb08C_PRxaCXZ2vOk_1AI2755rZHmPM -letsencrypt==0.4.0 - -# sha256: D3YDaVFjLsMSEfjI5B5D5tn5FeWUtNHYXCObw3ih2tg -# sha256: VTgvsePYGRmI4IOSAnxoYFHd8KciD73bxIuIHtbVFd8 -letsencrypt-apache==0.4.0 - # sha256: uDndLZwRfHAUMMFJlWkYpCOphjtIsJyQ4wpgE-fS9E8 # sha256: j4MIDaoknQNsvM-4rlzG_wB7iNbZN1ITca-r57Gbrbw mock==1.0.1 +# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; +# ADD ALL DEPENDENCIES ABOVE + +# sha256: UMVihR1TbyvQNHzx1CzYiydDitJVGw_mLAGr3-gCGJk +# sha256: ClkIqiGQsLTyyLASRkWYniS9n4CAW6D4GSuBETXFALY +acme==0.4.2 + +# sha256: hbUGND6Eo_q6a97o3o66wwLYJ7koNvwOXh9u5bZNCVI +# sha256: 460kqywseljbDW_Gr_ZU23rWlzNeE-AL4_JwYCRdS-Y +letsencrypt==0.4.2 + +# sha256: KNMAOMrJMr1vLJBDaihGqEmvPbfxgH_dvRk1OFHaM_I +# sha256: SXSg-gIabiV4CBzrfPIyABhfTjKl7YZrKDSVkfE4Vbo +letsencrypt-apache==0.4.2 + UNLIKELY_EOF # ------------------------------------------------------------------------- cat << "UNLIKELY_EOF" > "$TEMP_DIR/peep.py" @@ -745,6 +781,7 @@ except ImportError: 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.exceptions import InstallationError from pip.index import PackageFinder, Link try: from pip.log import logger @@ -763,7 +800,7 @@ except ImportError: DownloadProgressBar = DownloadProgressSpinner = NullProgressBar -__version__ = 3, 0, 0 +__version__ = 3, 1, 1 try: from pip.index import FormatControl # noqa @@ -781,6 +818,7 @@ ITS_FINE_ITS_FINE = 0 SOMETHING_WENT_WRONG = 1 # "Traditional" for command-line errors according to optparse docs: COMMAND_LINE_ERROR = 2 +UNHANDLED_EXCEPTION = 3 ARCHIVE_EXTENSIONS = ('.tar.bz2', '.tar.gz', '.tgz', '.tar', '.zip') @@ -1543,7 +1581,7 @@ def peep_install(argv): first_every_last(buckets[SatisfiedReq], *printers) return ITS_FINE_ITS_FINE - except (UnsupportedRequirementError, DownloadError) as exc: + except (UnsupportedRequirementError, InstallationError, DownloadError) as exc: out(str(exc)) return SOMETHING_WENT_WRONG finally: @@ -1563,16 +1601,23 @@ def peep_port(paths): print('Please specify one or more requirements files so I have ' 'something to port.\n') return COMMAND_LINE_ERROR + + comes_from = None for req in chain.from_iterable( _parse_requirements(path, package_finder(argv)) for path in paths): + req_path, req_line = path_and_line(req) hashes = [hexlify(urlsafe_b64decode((hash + '=').encode('ascii'))).decode('ascii') - for hash in hashes_above(*path_and_line(req))] + for hash in hashes_above(req_path, req_line)] + if req_path != comes_from: + print() + print('# from %s' % req_path) + print() + comes_from = req_path + 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='') + print('%s' % (req.link if getattr(req, 'link', None) else req.req), end='') for hash in hashes: print(' \\') print(' --hash=sha256:%s' % hash, end='') @@ -1617,21 +1662,13 @@ if __name__ == '__main__': exit(main()) except Exception: exception_handler(*sys.exc_info()) - exit(SOMETHING_WENT_WRONG) + exit(UNHANDLED_EXCEPTION) 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 + InstallRequirements "setuptools-requirements.txt" + InstallRequirements "letsencrypt-auto-requirements.txt" + echo "Installation succeeded." fi echo "Requesting root privileges to run letsencrypt..." echo " " $SUDO "$VENV_BIN/letsencrypt" "$@" @@ -1653,10 +1690,11 @@ else exit 0 fi - echo "Checking for new version..." - TEMP_DIR=$(TempDir) - # --------------------------------------------------------------------------- - cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py" + if [ "$NO_SELF_UPGRADE" != 1 ]; then + echo "Checking for new version..." + TEMP_DIR=$(TempDir) + # --------------------------------------------------------------------------- + cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py" """Do downloading and JSON parsing without additional dependencies. :: # Print latest released version of LE to stdout: @@ -1785,25 +1823,36 @@ if __name__ == '__main__': exit(main()) UNLIKELY_EOF - # --------------------------------------------------------------------------- - DeterminePythonVersion - REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` - if [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then - echo "Upgrading letsencrypt-auto $LE_AUTO_VERSION to $REMOTE_VERSION..." + # --------------------------------------------------------------------------- + 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" + # 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 "$@" + # Install new copy of letsencrypt-auto. + # TODO: Deal with quotes in pathnames. + echo "Replacing letsencrypt-auto..." + # Clone permissions with cp. chmod and chown don't have a --reference + # option on OS X or BSD, and stat -c on Linux is stat -f on OS X and BSD: + echo " " $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" + $SUDO cp -p "$0" "$TEMP_DIR/letsencrypt-auto.permission-clone" + echo " " $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone" + $SUDO cp "$TEMP_DIR/letsencrypt-auto" "$TEMP_DIR/letsencrypt-auto.permission-clone" + # Using mv rather than cp leaves the old file descriptor pointing to the + # original copy so the shell can continue to read it unmolested. mv across + # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the + # cp is unlikely to fail (esp. under sudo) if the rm doesn't. + echo " " $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" + $SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0" + # TODO: Clean up temp dir safely, even if it has quotes in its path. + rm -rf "$TEMP_DIR" + fi # A newer version is available. + fi # Self-upgrading is allowed. + + "$0" --le-auto-phase2 "$@" fi diff --git a/letsencrypt-auto-source/letsencrypt-auto b/letsencrypt-auto-source/letsencrypt-auto index 8e9882ffe..0590e5d43 100755 --- a/letsencrypt-auto-source/letsencrypt-auto +++ b/letsencrypt-auto-source/letsencrypt-auto @@ -411,7 +411,7 @@ Bootstrap() { 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 "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" echo "for more info." fi @@ -451,233 +451,207 @@ if [ "$1" = "--le-auto-phase2" ]; then # this, do `pip install --no-cache-dir -e acme -e . -e letsencrypt-apache`, and # then use `hashin` or a more secure method to gather the hashes. -# sha256: wxZH7baf09RlqEfqMVfTe-0flfGXYLEaR6qRwEtmYxQ -# sha256: YrCJpVvh2JSc0rx-DfC9254Cj678jDIDjMhIYq791uQ -argparse==1.4.0 +argparse==1.4.0 \ + --hash=sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314 \ + --hash=sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4 -# 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 +# This comes before cffi because cffi will otherwise install an unchecked +# version via setup_requires. +pycparser==2.14 \ + --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 -# sha256: O1CoPdWBSd_O6Yy2VlJl0QtT6cCivKfu73-19VJIkKc -ConfigArgParse==0.10.0 +cffi==1.4.2 \ + --hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \ + --hash=sha256:a568f49dfca12a8d9f370187257efc58a38109e1eee714d928561d7a018a64f8 \ + --hash=sha256:809c6ca8cfbcaeebfbd432b4576001b40d38ff2463773cb57577d75e1a020bc3 \ + --hash=sha256:86cdca2cd9cba41422230390df17dfeaa9f344a911e3975c8be9da57b35548e9 \ + --hash=sha256:24b13db84aec385ca23c7b8ded83ef8bb4177bc181d14758f9f975be5d020d86 \ + --hash=sha256:969aeffd7c0e097f6be1efd682c156ae226591a0793a94b6c2d5e4293f4c8d4e \ + --hash=sha256:000f358d4b0fa249feaab9c1ce7d5b2fe7e02e7bdf6806c26418505fc685e268 \ + --hash=sha256:a9d86f460bbd8358a2d513ad779e3f3fc878e3b93a00b5002faebf616ffe6b9c \ + --hash=sha256:3127b3ab33eb23ccac071f9a0802748e5cf7c5cbcd02482bb063e35b41dbb0b0 \ + --hash=sha256:e2b2d42236469a40224d39e7b6c60575f388b2f423f354c7ee90a5b7f58c8065 \ + --hash=sha256:8c2dccafee89b1b424b0bec6ad2dd9622c949d2024e929f5da1ed801eac75f1d \ + --hash=sha256:a4de7a4d11aed488bab4fb14f4988587a829bece5a20433f780d6e33b08083cb \ + --hash=sha256:5ca8fe30425265a49274e4b0213a1bc98f4b13449ae5e96f984771e5d83e58c1 \ + --hash=sha256:a4fd38802f59e714eba81a024f62db710b27dbe27a7ea12e911537327aa84d30 \ + --hash=sha256:86cd6912bbc83e9405d4a73cd7f4b4ee8353652d2dbc7c820106ed5b4d1bab3a \ + --hash=sha256:8f1d177d364ea35900415ae24ca3e471be3d5334ed0419294068c49f45913998 +ConfigArgParse==0.10.0 \ + --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 +configobj==5.0.6 \ + --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 +cryptography==1.2.3 \ + --hash=sha256:031938f73a5c5eb3e809e18ff7caeb6865351871417be6050cb8c86a9a202b9a \ + --hash=sha256:a179a38d50f8d68b491d7a313db78f8cabe290842cecddddc7b34d408e59db0a \ + --hash=sha256:906c88b2aadcf99cfabb24098263d1bf65ab0c8688acde10dae1f09d865920f1 \ + --hash=sha256:6e706c5c6088770b1d1b634e959e21963e315b0255f5f4777125ad3d54082977 \ + --hash=sha256:f5ebf8e31c48f8707921dca0e994de77813a9c9b9bf03c119c5ddf97bdcffe73 \ + --hash=sha256:c7b89e42288cc7fbee3812e99ef5c744f22452e11d6822f6807afc6d6b3be83e \ + --hash=sha256:8408d29865947109d8b68f1837a7cde1aa4dc86e0f79ca3ba58c0c44e443d6a5 \ + --hash=sha256:c7e76cf3c3d925dd31fa238cfb806cffba718c0f08707d77a538768477969956 \ + --hash=sha256:7d8de35380f31702758b7753bb5c40723832c73006dedb2f9099bf61a37f7287 \ + --hash=sha256:5edbee71fae5469ee83fe0a37866b9398c8ce3a46325c24fcedfbf097bb48a19 \ + --hash=sha256:594edafe4801c13bdc1cc305e7704a90c19617e95936f6ab457ee4ffe000ba50 \ + --hash=sha256:b7fdb16a0a7f481be42da744bfe1ea2163025de21f90f2c688a316f3c354da9c \ + --hash=sha256:207b8bf0fe0907336df38b733b487521cf9e138189aba9234ad54fe545dd0db8 \ + --hash=sha256:509a2f05386270cf783993c90d49ffefb3dd62aee45bf1ea8ce3d2cde7271c21 \ + --hash=sha256:ac69b65dd1af0179ede40c9f15788c88f73e628ea6c0519de3838e279bb388c6 \ + --hash=sha256:8df6fad6c6ae12fd7004ea29357f0a2b4d3774eaeca7656530d08d2d90cd41aa \ + --hash=sha256:0b8b96dd81cc1533a04f30382c0fe21c1972e189f794d0c4261a18cec08fd9b5 \ + --hash=sha256:cae8fca1883f23c50ea78d89de6fe4fefdb4cea83177760f47177559414ded93 \ + --hash=sha256:1a471ca576a9cdce1b1cd9f3a22b1d09ee44d46862037557de17919c0db44425 \ + --hash=sha256:8ec4e8e3d453b3a1b63b5f57737a434dcf1ee4a2f26f6ff7c5a37c3f679104d2 \ + --hash=sha256:8eb11c77dd8e73f48df6b2f7a7e16173fe0fe8fdfe266232832e88477e08454e +enum34==1.1.2 \ + --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ + --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 +funcsigs==0.4 \ + --hash=sha256:ff5ad9e2f8d9e5d1e8bbfbcf47722ab527cf0d51caeeed9da6d0f40799383fde \ + --hash=sha256:d83ce6df0b0ea6618700fe1db353526391a8a3ada1b7aba52fed7a61da772033 +idna==2.0 \ + --hash=sha256:9b2fc50bd3c4ba306b9651b69411ef22026d4d8335b93afc2214cef1246ce707 \ + --hash=sha256:16199aad938b290f5be1057c0e1efc6546229391c23cea61ca940c115f7d3d3b +ipaddress==1.0.16 \ + --hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \ + --hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0 +linecache2==1.0.0 \ + --hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \ + --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c +ndg-httpsclient==0.4.0 \ + --hash=sha256:e8c155fdebd9c4bcb0810b4ed01ae1987554b1ee034dd7532d7b8fdae38a6274 +ordereddict==1.1 \ + --hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f +parsedatetime==2.1 \ + --hash=sha256:ce9d422165cf6e963905cd5f74f274ebf7cc98c941916169178ef93f0e557838 \ + --hash=sha256:17c578775520c99131634e09cfca5a05ea9e1bd2a05cd06967ebece10df7af2d +pbr==1.8.1 \ + --hash=sha256:46c8db75ae75a056bd1cc07fa21734fe2e603d11a07833ecc1eeb74c35c72e0c \ + --hash=sha256:e2127626a91e6c885db89668976db31020f0af2da728924b56480fc7ccf09649 +psutil==3.3.0 \ + --hash=sha256:584f0b29fcc5d523b433cb8918b2fc74d67e30ee0b44a95baf031528f424619f \ + --hash=sha256:28ca0b6e9d99aa8dc286e8747a4471362b69812a25291de29b6a8d70a1545a0d \ + --hash=sha256:167ad5fff52a672c4ddc1c1a0b25146d6813ebb08a9aab0a3ac45f8a5b669c3b \ + --hash=sha256:e6dea6173a988727bb223d3497349ad5cdef5c0b282eff2d83e5f9065c53f85f \ + --hash=sha256:2af5e0a4aad66049955d0734aa4e3dc8caa17a9eaf8b4c1a27a5f1ee6e40f6fc \ + --hash=sha256:d9884dc0dc2e55e2448e495778dc9899c1c8bf37aeb2f434c1bea74af93c2683 \ + --hash=sha256:e27c2fe6dfcc8738be3d2c5a022f785eb72971057e1a9e1e34fba73bce8a71a6 \ + --hash=sha256:65afd6fecc8f3aed09ee4be63583bc8eb472f06ceaa4fe24c4d1d5a1a3c0e13f \ + --hash=sha256:ba1c558fbfcdf94515c2394b1155c1dc56e2bc2a9c17d30349827c9ed8a67e46 \ + --hash=sha256:ba95ea0022dcb64d36f0c1335c0605fae35bdf3e0fea8d92f5d0f6456a35e55b \ + --hash=sha256:421b6591d16b509aaa8d8c15821d66bb94cb4a8dc4385cad5c51b85d4a096d85 \ + --hash=sha256:326b305cbdb6f94dafbfe2c26b11da88b0ab07b8a07f8188ab9d75ff0c6e841a \ + --hash=sha256:9aede5b2b6fe46b3748ea8e5214443890d1634027bef3d33b7dad16556830278 \ + --hash=sha256:73bed1db894d1aa9c3c7e611d302cdeab7ae8a0dc0eeaf76727878db1ac5cd87 \ + --hash=sha256:935b5dd6d558af512f42501a7c08f41d7aff139af1bb3959daa3abb859234d6c \ + --hash=sha256:4ca0111cf157dcc0f2f69a323c5b5478718d68d45fc9435d84be0ec0f186215b \ + --hash=sha256:b6f13c95398a3fcf0226c4dcfa448560ba5865259cd96ec2810658651e932189 \ + --hash=sha256:ee6be30d1635bbdea4c4325d507dc8a0dbbde7e1c198bd62ddb9f43198b9e214 \ + --hash=sha256:dfa786858c268d7fbbe1b6175e001ec02738d7cfae0a7ce77bf9b651af676729 \ + --hash=sha256:aa77f9de72af9c16cc288cd4a24cf58824388f57d7a81e400c4616457629870e \ + --hash=sha256:f500093357d04da8140d87932cac2e54ef592a54ca8a743abb2850f60c2c22eb +pyasn1==0.1.9 \ + --hash=sha256:61f9d99e3cef65feb1bfe3a2eef7a93eb93819d345bf54bcd42f4e63d5204dae \ + --hash=sha256:1802a6dd32045e472a419db1441aecab469d33e0d2749e192abdec52101724af \ + --hash=sha256:35025cd9422c96504912f04e2f15fe79390a8597b430c2ca5d0534cf9309ffa0 \ + --hash=sha256:2f96ed5a0c329ca16230b326ca12b7461ec8f65e0be3e4f997516f36bf82a345 \ + --hash=sha256:28fee44217991cfad9e6a0b9f7e3f26041e21ebc96629e94e585ccd05d49fa65 \ + --hash=sha256:326e7a854a17fab07691204747695f8f692d674588a355c441fb14f660bf4e68 \ + --hash=sha256:cda5a90485709ca6795c86056c3e5fe7266028b05e53f1d527fdf93a6365a6b8 \ + --hash=sha256:0cb2a14742b543fdd68f931a14ce3829186ed2b1b2267a06787388c96b2dd9be \ + --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ + --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ + --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f +pyOpenSSL==0.15.1 \ + --hash=sha256:88e45e6bb25dfed272a1ef2e728461d44b634c2cd689e989b6e56a349c5a3ae5 \ + --hash=sha256:f0a26070d6db0881de8bcc7846934b7c3c930d8f9c79d45883ee48984bc0d672 +pyRFC3339==1.0 \ + --hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \ + --hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535 +python-augeas==0.5.0 \ + --hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2 +python2-pythondialog==3.3.0 \ + --hash=sha256:04e93f24995c43dd90f338d5d865ca72ce3fb5a5358d4daa4965571db35fc3ec \ + --hash=sha256:3e6f593fead98f8a526bc3e306933533236e33729f552f52896ea504f55313fa +pytz==2015.7 \ + --hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \ + --hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \ + --hash=sha256:ead4aefa7007249e05e51b01095719d5a8dd95760089f5730aac5698b1932918 \ + --hash=sha256:3cca0df08bd0ed98432390494ce3ded003f5e661aa460be7a734bffe35983605 \ + --hash=sha256:3ede470d3d17ba3c07638dfa0d10452bc1b6e5ad326127a65ba77e6aaeb11bec \ + --hash=sha256:68c47964f7186eec306b13629627722b9079cd4447ed9e5ecaecd4eac84ca734 \ + --hash=sha256:dd5d3991950aae40a6c81de1578942e73d629808cefc51d12cd157980e6cfc18 \ + --hash=sha256:a77c52062c07eb7c7b30545dbc73e32995b7e117eea750317b5cb5c7a4618f14 \ + --hash=sha256:81af9aec4bc960a9a0127c488f18772dae4634689233f06f65443e7b11ebeb51 \ + --hash=sha256:e079b1dadc5c06246cc1bb6fe1b23a50b1d1173f2edd5104efd40bb73a28f406 \ + --hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \ + --hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \ + --hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3 +requests==2.9.1 \ + --hash=sha256:113fbba5531a9e34945b7d36b33a084e8ba5d0664b703c81a7c572d91919a5b8 \ + --hash=sha256:c577815dd00f1394203fc44eb979724b098f88264a9ef898ee45b8e5e9cf587f +six==1.10.0 \ + --hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \ + --hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a +traceback2==1.4.0 \ + --hash=sha256:8253cebec4b19094d67cc5ed5af99bf1dba1285292226e98a31929f87a5d6b23 \ + --hash=sha256:05acc67a09980c2ecfedd3423f7ae0104839eccb55fc645773e1caa0951c3030 +unittest2==1.1.0 \ + --hash=sha256:13f77d0875db6d9b435e1d4f41e74ad4cc2eb6e1d5c824996092b3430f088bb8 \ + --hash=sha256:22882a0e418c284e1f718a822b3b022944d53d2d908e1690b319a9d3eb2c0579 +zope.component==4.2.2 \ + --hash=sha256:282c112b55dd8e3c869a3571f86767c150ab1284a9ace2bdec226c592acaf81a +zope.event==4.1.0 \ + --hash=sha256:dc7a59a2fd91730d3793131a5d261b29e93ec4e2a97f1bc487ce8defee2fe786 +zope.interface==4.1.3 \ + --hash=sha256:f07b631f7a601cd8cbd3332d54f43142c7088a83299f859356f08d1d4d4259b3 \ + --hash=sha256:de5cca083b9439d8002fb76bbe6b4998c5a5a721fab25b84298967f002df4c94 \ + --hash=sha256:6788416f7ea7f5b8a97be94825377aa25e8bdc73463e07baaf9858b29e737077 \ + --hash=sha256:6f3230f7254518201e5a3708cbb2de98c848304f06e3ded8bfb39e5825cba2e1 \ + --hash=sha256:5fa575a5240f04200c3088427d0d4b7b737f6e9018818a51d8d0f927a6a2517a \ + --hash=sha256:522194ad6a545735edd75c8a83f48d65d1af064e432a7d320d64f56bafc12e99 \ + --hash=sha256:e8c7b2d40943f71c99148c97f66caa7f5134147f57423f8db5b4825099ce9a09 \ + --hash=sha256:279024f0208601c3caa907c53876e37ad88625f7eaf1cb3842dbe360b2287017 \ + --hash=sha256:2e221a9eec7ccc58889a278ea13dcfed5ef939d80b07819a9a8b3cb1c681484f \ + --hash=sha256:69118965410ec86d44dc6b9017ee3ddbd582e0c0abeef62b3a19dbf6c8ad132b \ + --hash=sha256:d04df8686ec864d0cade8cf199f7f83aecd416109a20834d568f8310ded12dea \ + --hash=sha256:e75a947e15ee97e7e71e02ea302feb2fc62d3a2bb4668bf9dfbed43a506ac7e7 \ + --hash=sha256:4e45d22fb883222a5ab9f282a116fec5ee2e8d1a568ccff6a2d75bbd0eb6bcfc \ + --hash=sha256:bce9339bb3c7a55e0803b63d21c5839e8e479bc85c4adf42ae415b72f94facb2 \ + --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ + --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ + --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 +mock==1.0.1 \ + --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ + --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc -# sha256: ovVlB3DhyH-zNa8Zqbfrc_wFzPIhROto230AzSvLCQI -configobj==5.0.6 +# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -# 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: 54vpwKDfy6xxL-BPv5K5bN2ugLG4QvJCSCFMhJbwBu8 -# sha256: Syb_TnEQ23butvWntkqCYjg51ZXCA47tpmLyott46Xw -linecache2==1.0.0 - -# sha256: 6MFV_evZxLywgQtO0BrhmHVUse4DTddTLXuP2uOKYnQ -ndg-httpsclient==0.4.0 - -# sha256: HDW0rCBs7y0kgWyJ-Jzyid09OM98RJuz-re_bUPwGx8 -ordereddict==1.1 - -# 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: glPOvsSxkJTWfMXtWvmb8duhKFKSIm6Yoxkp-HpdayM -# sha256: BazGegmYDC7P7dNCP3rgEEg57MtV_GRXc-HKoJUcMDA -traceback2==1.4.0 - -# sha256: E_d9CHXbbZtDXh1PQedK1MwutuHVyCSZYJKzQw8Ii7g -# sha256: IogqDkGMKE4fcYqCKzsCKUTVPS2QjhaQsxmp0-ssBXk -unittest2==1.1.0 - -# 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: ilvjjTWOS86xchl0WBZ0YOAw_0rmqdnjNsxb1hq2RD8 -# sha256: T37KMj0TnsuvHIzCCmoww2fpfpOBTj7cd4NAqucXcpw -acme==0.4.0 - -# sha256: 33BQiANlNLGqGpirTfdCEElTF9YbpaKiYpTbK4zeGD8 -# sha256: lwsV1OdEzzlMeb08C_PRxaCXZ2vOk_1AI2755rZHmPM -letsencrypt==0.4.0 - -# sha256: D3YDaVFjLsMSEfjI5B5D5tn5FeWUtNHYXCObw3ih2tg -# sha256: VTgvsePYGRmI4IOSAnxoYFHd8KciD73bxIuIHtbVFd8 -letsencrypt-apache==0.4.0 - -# sha256: uDndLZwRfHAUMMFJlWkYpCOphjtIsJyQ4wpgE-fS9E8 -# sha256: j4MIDaoknQNsvM-4rlzG_wB7iNbZN1ITca-r57Gbrbw -mock==1.0.1 +acme==0.4.2 \ + --hash=sha256:50c562851d536f2bd0347cf1d42cd88b27438ad2551b0fe62c01abdfe8021899 \ + --hash=sha256:0a5908aa2190b0b4f2c8b0124645989e24bd9f80805ba0f8192b811135c500b6 +letsencrypt==0.4.2 \ + --hash=sha256:85b506343e84a3faba6bdee8de8ebac302d827b92836fc0e5e1f6ee5b64d0952 \ + --hash=sha256:e3ad24ab2c2c7a58db0d6fc6aff654db7ad697335e13e00be3f27060245d4be6 +letsencrypt-apache==0.4.2 \ + --hash=sha256:4974a0fa021a6e2578081ceb7cf23200185f4e32a5ed866b28349591f13855ba \ + --hash=sha256:28d30038cac932bd6f2c90436a2846a849af3db7f1807fddbd19353851da33f2 UNLIKELY_EOF # ------------------------------------------------------------------------- - cat << "UNLIKELY_EOF" > "$TEMP_DIR/peep.py" + cat << "UNLIKELY_EOF" > "$TEMP_DIR/pipstrap.py" #!/usr/bin/env python -"""peep ("prudently examine every package") verifies that packages conform to a -trusted, locally stored hash and only then installs them:: +"""A small script that can act as a trust root for installing pip 8 - 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. +Embed this in your project, and your VCS checkout is all you have to trust. In +a post-peep era, this lets you claw your way to a hash-checking version of pip, +with which you can install the rest of your dependencies safely. All it assumes +is Python 2.6 or better and *some* version of pip already installed. If +anything goes wrong, it will exit with a non-zero status code. """ -# This is here so embedded copies of peep.py are MIT-compliant: -# Copyright (c) 2013 Erik Rose +# This is here so embedded copies are MIT-compliant: +# Copyright (c) 2016 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 @@ -689,964 +663,143 @@ hashes in requirements.txt, and you're all set. # 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 +from os.path import join +from pipes import quote +from shutil import rmtree try: - from urllib2 import build_opener, HTTPHandler, HTTPSHandler, HTTPError + from subprocess import check_output +except ImportError: + from subprocess import CalledProcessError, PIPE, Popen + + def check_output(*popenargs, **kwargs): + if 'stdout' in kwargs: + raise ValueError('stdout argument not allowed, it will be ' + 'overridden.') + process = Popen(stdout=PIPE, *popenargs, **kwargs) + output, unused_err = process.communicate() + retcode = process.poll() + if retcode: + cmd = kwargs.get("args") + if cmd is None: + cmd = popenargs[0] + raise CalledProcessError(retcode, cmd, output=output) + return output +from sys import exit, version_info +from tempfile import mkdtemp +try: + from urllib2 import build_opener, HTTPHandler, HTTPSHandler 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.exceptions import InstallationError -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__ = 3, 1, 1 - -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 +__version__ = 1, 1, 0 -ITS_FINE_ITS_FINE = 0 -SOMETHING_WENT_WRONG = 1 -# "Traditional" for command-line errors according to optparse docs: -COMMAND_LINE_ERROR = 2 -UNHANDLED_EXCEPTION = 3 - -ARCHIVE_EXTENSIONS = ('.tar.bz2', '.tar.gz', '.tgz', '.tar', '.zip') - -MARKER = object() +# wheel has a conditional dependency on argparse: +maybe_argparse = ( + [('https://pypi.python.org/packages/source/a/argparse/' + 'argparse-1.4.0.tar.gz', + '62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4')] + if version_info < (2, 7, 0) else []) -class PipException(Exception): - """When I delegated to pip, it exited with an error.""" - - def __init__(self, error_code): - self.error_code = error_code +PACKAGES = maybe_argparse + [ + # Pip has no dependencies, as it vendors everything: + ('https://pypi.python.org/packages/source/p/pip/pip-8.0.3.tar.gz', + '30f98b66f3fe1069c529a491597d34a1c224a68640c82caf2ade5f88aa1405e8'), + # This version of setuptools has only optional dependencies: + ('https://pypi.python.org/packages/source/s/setuptools/' + 'setuptools-20.2.2.tar.gz', + '24fcfc15364a9fe09a220f37d2dcedc849795e3de3e4b393ee988e66a9cbd85a'), + ('https://pypi.python.org/packages/source/w/wheel/wheel-0.29.0.tar.gz', + '1ebb8ad7e26b448e9caa4773d2357849bf80ff9e313964bcaf79cbf0201a1648') +] -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) - +class HashError(Exception): def __str__(self): - return 'Downloading %s failed: %s' % (self.link, self.reason) + url, path, actual, expected = self.args + return ('{url} did not match the expected hash {expected}. Instead, ' + 'it was {actual}. The file (left at {path}) may have been ' + 'tampered with.'.format(**locals())) -def encoded_hash(sha): - """Return a short, 7-bit-safe representation of a hash. +def hashed_download(url, temp, digest): + """Download ``url`` to ``temp``, make sure it has the SHA-256 ``digest``, + and return its path.""" + # Based on pip 1.4.1's URLOpener but with cert verification removed. Python + # >=2.7.9 verifies HTTPS certs itself, and, in any case, the cert + # authenticity has only privacy (not arbitrary code execution) + # implications, since we're checking hashes. + def opener(): + opener = build_opener(HTTPSHandler()) + # Strip out HTTPHandler to prevent MITM spoof: + for handler in opener.handlers: + if isinstance(handler, HTTPHandler): + opener.handlers.remove(handler) + return opener - 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() + def read_chunks(response, chunk_size): while True: - data = archive.read(2 ** 20) - if not data: + chunk = response.read(chunk_size) + if not chunk: 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 '#' - (?Psha256):\s+ # Hash type is hardcoded to be sha256 for now. - (?P[^\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.*))? # 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_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, InstallationError, 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 - - comes_from = None - for req in chain.from_iterable( - _parse_requirements(path, package_finder(argv)) for path in paths): - req_path, req_line = path_and_line(req) - hashes = [hexlify(urlsafe_b64decode((hash + '=').encode('ascii'))).decode('ascii') - for hash in hashes_above(req_path, req_line)] - if req_path != comes_from: - print() - print('# from %s' % req_path) - print() - comes_from = req_path - - if not hashes: - print(req.req) - else: - print('%s' % (req.link if getattr(req, 'link', None) else req.req), end='') - for hash in hashes: - print(' \\') - print(' --hash=sha256:%s' % hash, end='') - print() + yield chunk + + response = opener().open(url) + path = join(temp, urlparse(url).path.split('/')[-1]) + actual_hash = sha256() + with open(path, 'wb') as file: + for chunk in read_chunks(response, 4096): + file.write(chunk) + actual_hash.update(chunk) + + actual_digest = actual_hash.hexdigest() + if actual_digest != digest: + raise HashError(url, path, actual_digest, digest) + return path def main(): - """Be the top-level entrypoint. Return a shell status code.""" - commands = {'hash': peep_hash, - 'install': peep_install, - 'port': peep_port} + temp = mkdtemp(prefix='pipstrap-') 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('---') + downloads = [hashed_download(url, temp, digest) + for url, digest in PACKAGES] + check_output('pip install --no-index --no-deps -U ' + + ' '.join(quote(d) for d in downloads), + shell=True) + except HashError as exc: + print(exc) + except Exception: + rmtree(temp) + raise + else: + rmtree(temp) + return 0 + return 1 if __name__ == '__main__': - try: - exit(main()) - except Exception: - exception_handler(*sys.exc_info()) - exit(UNHANDLED_EXCEPTION) + exit(main()) UNLIKELY_EOF # ------------------------------------------------------------------------- + # Set PATH so pipstrap upgrades the right (v)env: + PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py" set +e - PEEP_OUT=`"$VENV_BIN/python" "$TEMP_DIR/peep.py" install -r "$TEMP_DIR/letsencrypt-auto-requirements.txt"` - PEEP_STATUS=$? + PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` + PIP_STATUS=$? set -e rm -rf "$TEMP_DIR" - if [ "$PEEP_STATUS" != 0 ]; then + if [ "$PIP_STATUS" != 0 ]; then # Report error. (Otherwise, be quiet.) - echo "Had a problem while downloading and verifying Python packages:" - echo "$PEEP_OUT" + echo "Had a problem while installing Python packages:" + echo "$PIP_OUT" rm -rf "$VENV_PATH" exit 1 fi diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig b/letsencrypt-auto-source/letsencrypt-auto.sig index 532a48207..9e2e610c0 100644 Binary files a/letsencrypt-auto-source/letsencrypt-auto.sig and b/letsencrypt-auto-source/letsencrypt-auto.sig differ diff --git a/letsencrypt-auto-source/letsencrypt-auto.sig.lzma.base64 b/letsencrypt-auto-source/letsencrypt-auto.sig.lzma.base64 new file mode 100644 index 000000000..037f2f020 --- /dev/null +++ b/letsencrypt-auto-source/letsencrypt-auto.sig.lzma.base64 @@ -0,0 +1,6 @@ +XQAAAAT//////////wApLArrUzOk5bRHUk0UvMS4xjyZkm3U3qhnKvMbEan7rVeK6yBlbwGeeWFn +Sw4XT1raGAMNq7cwyJvT7ql93Df7TpuRnxNSbPx7q52GojYyb5Oj1IQ2Y22Mvq41Q4K3kCZcVv+1 +YVKW3OazUn+wCnaoGhDdMFmH0EKbEPSGibba6HJqUoFosaDE2hRZmjqYR/VwwPCtW820L0Qz9PZ7 +DEAZ5VdMmj1+u+bYjDEcZD5+DyWKoLWci8tBXcPGiSvPDdZax/IWmR0GGUOd13gC7uX/HM2dHgbM +Izh7Y3PPNEzM8Fu2wdXLoMCaYrQcrPAdKhsnyMCDbjxCVbD9LkS17xCq4LUMkcz/fMu3/CRSMMZ7 +gnn//jNQAA== diff --git a/letsencrypt-auto-source/letsencrypt-auto.template b/letsencrypt-auto-source/letsencrypt-auto.template index ea4d064b7..40edca7fe 100755 --- a/letsencrypt-auto-source/letsencrypt-auto.template +++ b/letsencrypt-auto-source/letsencrypt-auto.template @@ -159,7 +159,7 @@ Bootstrap() { 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 "You will need to bootstrap, configure virtualenv, and run pip install manually." echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites" echo "for more info." fi @@ -198,19 +198,21 @@ if [ "$1" = "--le-auto-phase2" ]; then {{ letsencrypt-auto-requirements.txt }} UNLIKELY_EOF # ------------------------------------------------------------------------- - cat << "UNLIKELY_EOF" > "$TEMP_DIR/peep.py" -{{ peep.py }} + cat << "UNLIKELY_EOF" > "$TEMP_DIR/pipstrap.py" +{{ pipstrap.py }} UNLIKELY_EOF # ------------------------------------------------------------------------- + # Set PATH so pipstrap upgrades the right (v)env: + PATH="$VENV_BIN:$PATH" "$VENV_BIN/python" "$TEMP_DIR/pipstrap.py" set +e - PEEP_OUT=`"$VENV_BIN/python" "$TEMP_DIR/peep.py" install -r "$TEMP_DIR/letsencrypt-auto-requirements.txt"` - PEEP_STATUS=$? + PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1` + PIP_STATUS=$? set -e rm -rf "$TEMP_DIR" - if [ "$PEEP_STATUS" != 0 ]; then + if [ "$PIP_STATUS" != 0 ]; then # Report error. (Otherwise, be quiet.) - echo "Had a problem while downloading and verifying Python packages:" - echo "$PEEP_OUT" + echo "Had a problem while installing Python packages:" + echo "$PIP_OUT" rm -rf "$VENV_PATH" exit 1 fi diff --git a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt index b258fcfad..1e76417b7 100644 --- a/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt +++ b/letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt @@ -2,213 +2,188 @@ # this, do `pip install --no-cache-dir -e acme -e . -e letsencrypt-apache`, and # then use `hashin` or a more secure method to gather the hashes. -# sha256: wxZH7baf09RlqEfqMVfTe-0flfGXYLEaR6qRwEtmYxQ -# sha256: YrCJpVvh2JSc0rx-DfC9254Cj678jDIDjMhIYq791uQ -argparse==1.4.0 +argparse==1.4.0 \ + --hash=sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314 \ + --hash=sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4 -# 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 +# This comes before cffi because cffi will otherwise install an unchecked +# version via setup_requires. +pycparser==2.14 \ + --hash=sha256:7959b4a74abdc27b312fed1c21e6caf9309ce0b29ea86b591fd2e99ecdf27f73 -# sha256: O1CoPdWBSd_O6Yy2VlJl0QtT6cCivKfu73-19VJIkKc -ConfigArgParse==0.10.0 +cffi==1.4.2 \ + --hash=sha256:53c1c9ddb30431513eb7f3cdef0a3e06b0f1252188aaa7744af0f5a4cd45dbaf \ + --hash=sha256:a568f49dfca12a8d9f370187257efc58a38109e1eee714d928561d7a018a64f8 \ + --hash=sha256:809c6ca8cfbcaeebfbd432b4576001b40d38ff2463773cb57577d75e1a020bc3 \ + --hash=sha256:86cdca2cd9cba41422230390df17dfeaa9f344a911e3975c8be9da57b35548e9 \ + --hash=sha256:24b13db84aec385ca23c7b8ded83ef8bb4177bc181d14758f9f975be5d020d86 \ + --hash=sha256:969aeffd7c0e097f6be1efd682c156ae226591a0793a94b6c2d5e4293f4c8d4e \ + --hash=sha256:000f358d4b0fa249feaab9c1ce7d5b2fe7e02e7bdf6806c26418505fc685e268 \ + --hash=sha256:a9d86f460bbd8358a2d513ad779e3f3fc878e3b93a00b5002faebf616ffe6b9c \ + --hash=sha256:3127b3ab33eb23ccac071f9a0802748e5cf7c5cbcd02482bb063e35b41dbb0b0 \ + --hash=sha256:e2b2d42236469a40224d39e7b6c60575f388b2f423f354c7ee90a5b7f58c8065 \ + --hash=sha256:8c2dccafee89b1b424b0bec6ad2dd9622c949d2024e929f5da1ed801eac75f1d \ + --hash=sha256:a4de7a4d11aed488bab4fb14f4988587a829bece5a20433f780d6e33b08083cb \ + --hash=sha256:5ca8fe30425265a49274e4b0213a1bc98f4b13449ae5e96f984771e5d83e58c1 \ + --hash=sha256:a4fd38802f59e714eba81a024f62db710b27dbe27a7ea12e911537327aa84d30 \ + --hash=sha256:86cd6912bbc83e9405d4a73cd7f4b4ee8353652d2dbc7c820106ed5b4d1bab3a \ + --hash=sha256:8f1d177d364ea35900415ae24ca3e471be3d5334ed0419294068c49f45913998 +ConfigArgParse==0.10.0 \ + --hash=sha256:3b50a83dd58149dfcee98cb6565265d10b53e9c0a2bca7eeef7fb5f5524890a7 +configobj==5.0.6 \ + --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902 +cryptography==1.2.3 \ + --hash=sha256:031938f73a5c5eb3e809e18ff7caeb6865351871417be6050cb8c86a9a202b9a \ + --hash=sha256:a179a38d50f8d68b491d7a313db78f8cabe290842cecddddc7b34d408e59db0a \ + --hash=sha256:906c88b2aadcf99cfabb24098263d1bf65ab0c8688acde10dae1f09d865920f1 \ + --hash=sha256:6e706c5c6088770b1d1b634e959e21963e315b0255f5f4777125ad3d54082977 \ + --hash=sha256:f5ebf8e31c48f8707921dca0e994de77813a9c9b9bf03c119c5ddf97bdcffe73 \ + --hash=sha256:c7b89e42288cc7fbee3812e99ef5c744f22452e11d6822f6807afc6d6b3be83e \ + --hash=sha256:8408d29865947109d8b68f1837a7cde1aa4dc86e0f79ca3ba58c0c44e443d6a5 \ + --hash=sha256:c7e76cf3c3d925dd31fa238cfb806cffba718c0f08707d77a538768477969956 \ + --hash=sha256:7d8de35380f31702758b7753bb5c40723832c73006dedb2f9099bf61a37f7287 \ + --hash=sha256:5edbee71fae5469ee83fe0a37866b9398c8ce3a46325c24fcedfbf097bb48a19 \ + --hash=sha256:594edafe4801c13bdc1cc305e7704a90c19617e95936f6ab457ee4ffe000ba50 \ + --hash=sha256:b7fdb16a0a7f481be42da744bfe1ea2163025de21f90f2c688a316f3c354da9c \ + --hash=sha256:207b8bf0fe0907336df38b733b487521cf9e138189aba9234ad54fe545dd0db8 \ + --hash=sha256:509a2f05386270cf783993c90d49ffefb3dd62aee45bf1ea8ce3d2cde7271c21 \ + --hash=sha256:ac69b65dd1af0179ede40c9f15788c88f73e628ea6c0519de3838e279bb388c6 \ + --hash=sha256:8df6fad6c6ae12fd7004ea29357f0a2b4d3774eaeca7656530d08d2d90cd41aa \ + --hash=sha256:0b8b96dd81cc1533a04f30382c0fe21c1972e189f794d0c4261a18cec08fd9b5 \ + --hash=sha256:cae8fca1883f23c50ea78d89de6fe4fefdb4cea83177760f47177559414ded93 \ + --hash=sha256:1a471ca576a9cdce1b1cd9f3a22b1d09ee44d46862037557de17919c0db44425 \ + --hash=sha256:8ec4e8e3d453b3a1b63b5f57737a434dcf1ee4a2f26f6ff7c5a37c3f679104d2 \ + --hash=sha256:8eb11c77dd8e73f48df6b2f7a7e16173fe0fe8fdfe266232832e88477e08454e +enum34==1.1.2 \ + --hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \ + --hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501 +funcsigs==0.4 \ + --hash=sha256:ff5ad9e2f8d9e5d1e8bbfbcf47722ab527cf0d51caeeed9da6d0f40799383fde \ + --hash=sha256:d83ce6df0b0ea6618700fe1db353526391a8a3ada1b7aba52fed7a61da772033 +idna==2.0 \ + --hash=sha256:9b2fc50bd3c4ba306b9651b69411ef22026d4d8335b93afc2214cef1246ce707 \ + --hash=sha256:16199aad938b290f5be1057c0e1efc6546229391c23cea61ca940c115f7d3d3b +ipaddress==1.0.16 \ + --hash=sha256:935712800ce4760701d89ad677666cd52691fd2f6f0b340c8b4239a3c17988a5 \ + --hash=sha256:5a3182b322a706525c46282ca6f064d27a02cffbd449f9f47416f1dc96aa71b0 +linecache2==1.0.0 \ + --hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \ + --hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c +ndg-httpsclient==0.4.0 \ + --hash=sha256:e8c155fdebd9c4bcb0810b4ed01ae1987554b1ee034dd7532d7b8fdae38a6274 +ordereddict==1.1 \ + --hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f +parsedatetime==2.1 \ + --hash=sha256:ce9d422165cf6e963905cd5f74f274ebf7cc98c941916169178ef93f0e557838 \ + --hash=sha256:17c578775520c99131634e09cfca5a05ea9e1bd2a05cd06967ebece10df7af2d +pbr==1.8.1 \ + --hash=sha256:46c8db75ae75a056bd1cc07fa21734fe2e603d11a07833ecc1eeb74c35c72e0c \ + --hash=sha256:e2127626a91e6c885db89668976db31020f0af2da728924b56480fc7ccf09649 +psutil==3.3.0 \ + --hash=sha256:584f0b29fcc5d523b433cb8918b2fc74d67e30ee0b44a95baf031528f424619f \ + --hash=sha256:28ca0b6e9d99aa8dc286e8747a4471362b69812a25291de29b6a8d70a1545a0d \ + --hash=sha256:167ad5fff52a672c4ddc1c1a0b25146d6813ebb08a9aab0a3ac45f8a5b669c3b \ + --hash=sha256:e6dea6173a988727bb223d3497349ad5cdef5c0b282eff2d83e5f9065c53f85f \ + --hash=sha256:2af5e0a4aad66049955d0734aa4e3dc8caa17a9eaf8b4c1a27a5f1ee6e40f6fc \ + --hash=sha256:d9884dc0dc2e55e2448e495778dc9899c1c8bf37aeb2f434c1bea74af93c2683 \ + --hash=sha256:e27c2fe6dfcc8738be3d2c5a022f785eb72971057e1a9e1e34fba73bce8a71a6 \ + --hash=sha256:65afd6fecc8f3aed09ee4be63583bc8eb472f06ceaa4fe24c4d1d5a1a3c0e13f \ + --hash=sha256:ba1c558fbfcdf94515c2394b1155c1dc56e2bc2a9c17d30349827c9ed8a67e46 \ + --hash=sha256:ba95ea0022dcb64d36f0c1335c0605fae35bdf3e0fea8d92f5d0f6456a35e55b \ + --hash=sha256:421b6591d16b509aaa8d8c15821d66bb94cb4a8dc4385cad5c51b85d4a096d85 \ + --hash=sha256:326b305cbdb6f94dafbfe2c26b11da88b0ab07b8a07f8188ab9d75ff0c6e841a \ + --hash=sha256:9aede5b2b6fe46b3748ea8e5214443890d1634027bef3d33b7dad16556830278 \ + --hash=sha256:73bed1db894d1aa9c3c7e611d302cdeab7ae8a0dc0eeaf76727878db1ac5cd87 \ + --hash=sha256:935b5dd6d558af512f42501a7c08f41d7aff139af1bb3959daa3abb859234d6c \ + --hash=sha256:4ca0111cf157dcc0f2f69a323c5b5478718d68d45fc9435d84be0ec0f186215b \ + --hash=sha256:b6f13c95398a3fcf0226c4dcfa448560ba5865259cd96ec2810658651e932189 \ + --hash=sha256:ee6be30d1635bbdea4c4325d507dc8a0dbbde7e1c198bd62ddb9f43198b9e214 \ + --hash=sha256:dfa786858c268d7fbbe1b6175e001ec02738d7cfae0a7ce77bf9b651af676729 \ + --hash=sha256:aa77f9de72af9c16cc288cd4a24cf58824388f57d7a81e400c4616457629870e \ + --hash=sha256:f500093357d04da8140d87932cac2e54ef592a54ca8a743abb2850f60c2c22eb +pyasn1==0.1.9 \ + --hash=sha256:61f9d99e3cef65feb1bfe3a2eef7a93eb93819d345bf54bcd42f4e63d5204dae \ + --hash=sha256:1802a6dd32045e472a419db1441aecab469d33e0d2749e192abdec52101724af \ + --hash=sha256:35025cd9422c96504912f04e2f15fe79390a8597b430c2ca5d0534cf9309ffa0 \ + --hash=sha256:2f96ed5a0c329ca16230b326ca12b7461ec8f65e0be3e4f997516f36bf82a345 \ + --hash=sha256:28fee44217991cfad9e6a0b9f7e3f26041e21ebc96629e94e585ccd05d49fa65 \ + --hash=sha256:326e7a854a17fab07691204747695f8f692d674588a355c441fb14f660bf4e68 \ + --hash=sha256:cda5a90485709ca6795c86056c3e5fe7266028b05e53f1d527fdf93a6365a6b8 \ + --hash=sha256:0cb2a14742b543fdd68f931a14ce3829186ed2b1b2267a06787388c96b2dd9be \ + --hash=sha256:5191ff6b9126d2c039dd87f8ff025bed274baf07fa78afa46f556b1ad7265d6e \ + --hash=sha256:8323e03637b2d072cc7041300bac6ec448c3c28950ab40376036788e9a1af629 \ + --hash=sha256:853cacd96d1f701ddd67aa03ecc05f51890135b7262e922710112f12a2ed2a7f +pyOpenSSL==0.15.1 \ + --hash=sha256:88e45e6bb25dfed272a1ef2e728461d44b634c2cd689e989b6e56a349c5a3ae5 \ + --hash=sha256:f0a26070d6db0881de8bcc7846934b7c3c930d8f9c79d45883ee48984bc0d672 +pyRFC3339==1.0 \ + --hash=sha256:eea31835c56e2096af4363a5745a784878a61d043e247d3a6d6a0a32a9741f56 \ + --hash=sha256:8dfbc6c458b8daba1c0f3620a8c78008b323a268b27b7359e92a4ae41325f535 +python-augeas==0.5.0 \ + --hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2 +python2-pythondialog==3.3.0 \ + --hash=sha256:04e93f24995c43dd90f338d5d865ca72ce3fb5a5358d4daa4965571db35fc3ec \ + --hash=sha256:3e6f593fead98f8a526bc3e306933533236e33729f552f52896ea504f55313fa +pytz==2015.7 \ + --hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \ + --hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \ + --hash=sha256:ead4aefa7007249e05e51b01095719d5a8dd95760089f5730aac5698b1932918 \ + --hash=sha256:3cca0df08bd0ed98432390494ce3ded003f5e661aa460be7a734bffe35983605 \ + --hash=sha256:3ede470d3d17ba3c07638dfa0d10452bc1b6e5ad326127a65ba77e6aaeb11bec \ + --hash=sha256:68c47964f7186eec306b13629627722b9079cd4447ed9e5ecaecd4eac84ca734 \ + --hash=sha256:dd5d3991950aae40a6c81de1578942e73d629808cefc51d12cd157980e6cfc18 \ + --hash=sha256:a77c52062c07eb7c7b30545dbc73e32995b7e117eea750317b5cb5c7a4618f14 \ + --hash=sha256:81af9aec4bc960a9a0127c488f18772dae4634689233f06f65443e7b11ebeb51 \ + --hash=sha256:e079b1dadc5c06246cc1bb6fe1b23a50b1d1173f2edd5104efd40bb73a28f406 \ + --hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \ + --hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \ + --hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3 +requests==2.9.1 \ + --hash=sha256:113fbba5531a9e34945b7d36b33a084e8ba5d0664b703c81a7c572d91919a5b8 \ + --hash=sha256:c577815dd00f1394203fc44eb979724b098f88264a9ef898ee45b8e5e9cf587f +six==1.10.0 \ + --hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \ + --hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a +traceback2==1.4.0 \ + --hash=sha256:8253cebec4b19094d67cc5ed5af99bf1dba1285292226e98a31929f87a5d6b23 \ + --hash=sha256:05acc67a09980c2ecfedd3423f7ae0104839eccb55fc645773e1caa0951c3030 +unittest2==1.1.0 \ + --hash=sha256:13f77d0875db6d9b435e1d4f41e74ad4cc2eb6e1d5c824996092b3430f088bb8 \ + --hash=sha256:22882a0e418c284e1f718a822b3b022944d53d2d908e1690b319a9d3eb2c0579 +zope.component==4.2.2 \ + --hash=sha256:282c112b55dd8e3c869a3571f86767c150ab1284a9ace2bdec226c592acaf81a +zope.event==4.1.0 \ + --hash=sha256:dc7a59a2fd91730d3793131a5d261b29e93ec4e2a97f1bc487ce8defee2fe786 +zope.interface==4.1.3 \ + --hash=sha256:f07b631f7a601cd8cbd3332d54f43142c7088a83299f859356f08d1d4d4259b3 \ + --hash=sha256:de5cca083b9439d8002fb76bbe6b4998c5a5a721fab25b84298967f002df4c94 \ + --hash=sha256:6788416f7ea7f5b8a97be94825377aa25e8bdc73463e07baaf9858b29e737077 \ + --hash=sha256:6f3230f7254518201e5a3708cbb2de98c848304f06e3ded8bfb39e5825cba2e1 \ + --hash=sha256:5fa575a5240f04200c3088427d0d4b7b737f6e9018818a51d8d0f927a6a2517a \ + --hash=sha256:522194ad6a545735edd75c8a83f48d65d1af064e432a7d320d64f56bafc12e99 \ + --hash=sha256:e8c7b2d40943f71c99148c97f66caa7f5134147f57423f8db5b4825099ce9a09 \ + --hash=sha256:279024f0208601c3caa907c53876e37ad88625f7eaf1cb3842dbe360b2287017 \ + --hash=sha256:2e221a9eec7ccc58889a278ea13dcfed5ef939d80b07819a9a8b3cb1c681484f \ + --hash=sha256:69118965410ec86d44dc6b9017ee3ddbd582e0c0abeef62b3a19dbf6c8ad132b \ + --hash=sha256:d04df8686ec864d0cade8cf199f7f83aecd416109a20834d568f8310ded12dea \ + --hash=sha256:e75a947e15ee97e7e71e02ea302feb2fc62d3a2bb4668bf9dfbed43a506ac7e7 \ + --hash=sha256:4e45d22fb883222a5ab9f282a116fec5ee2e8d1a568ccff6a2d75bbd0eb6bcfc \ + --hash=sha256:bce9339bb3c7a55e0803b63d21c5839e8e479bc85c4adf42ae415b72f94facb2 \ + --hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \ + --hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \ + --hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392 +mock==1.0.1 \ + --hash=sha256:b839dd2d9c117c701430c149956918a423a9863b48b09c90e30a6013e7d2f44f \ + --hash=sha256:8f83080daa249d036cbccfb8ae5cc6ff007b88d6d937521371afabe7b19badbc -# sha256: ovVlB3DhyH-zNa8Zqbfrc_wFzPIhROto230AzSvLCQI -configobj==5.0.6 +# THE LINES BELOW ARE EDITED BY THE RELEASE SCRIPT; ADD ALL DEPENDENCIES ABOVE. -# 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: 54vpwKDfy6xxL-BPv5K5bN2ugLG4QvJCSCFMhJbwBu8 -# sha256: Syb_TnEQ23butvWntkqCYjg51ZXCA47tpmLyott46Xw -linecache2==1.0.0 - -# sha256: 6MFV_evZxLywgQtO0BrhmHVUse4DTddTLXuP2uOKYnQ -ndg-httpsclient==0.4.0 - -# sha256: HDW0rCBs7y0kgWyJ-Jzyid09OM98RJuz-re_bUPwGx8 -ordereddict==1.1 - -# 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: glPOvsSxkJTWfMXtWvmb8duhKFKSIm6Yoxkp-HpdayM -# sha256: BazGegmYDC7P7dNCP3rgEEg57MtV_GRXc-HKoJUcMDA -traceback2==1.4.0 - -# sha256: E_d9CHXbbZtDXh1PQedK1MwutuHVyCSZYJKzQw8Ii7g -# sha256: IogqDkGMKE4fcYqCKzsCKUTVPS2QjhaQsxmp0-ssBXk -unittest2==1.1.0 - -# 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: ilvjjTWOS86xchl0WBZ0YOAw_0rmqdnjNsxb1hq2RD8 -# sha256: T37KMj0TnsuvHIzCCmoww2fpfpOBTj7cd4NAqucXcpw -acme==0.4.0 - -# sha256: 33BQiANlNLGqGpirTfdCEElTF9YbpaKiYpTbK4zeGD8 -# sha256: lwsV1OdEzzlMeb08C_PRxaCXZ2vOk_1AI2755rZHmPM -letsencrypt==0.4.0 - -# sha256: D3YDaVFjLsMSEfjI5B5D5tn5FeWUtNHYXCObw3ih2tg -# sha256: VTgvsePYGRmI4IOSAnxoYFHd8KciD73bxIuIHtbVFd8 -letsencrypt-apache==0.4.0 - -# sha256: uDndLZwRfHAUMMFJlWkYpCOphjtIsJyQ4wpgE-fS9E8 -# sha256: j4MIDaoknQNsvM-4rlzG_wB7iNbZN1ITca-r57Gbrbw -mock==1.0.1 +acme==0.4.2 \ + --hash=sha256:50c562851d536f2bd0347cf1d42cd88b27438ad2551b0fe62c01abdfe8021899 \ + --hash=sha256:0a5908aa2190b0b4f2c8b0124645989e24bd9f80805ba0f8192b811135c500b6 +letsencrypt==0.4.2 \ + --hash=sha256:85b506343e84a3faba6bdee8de8ebac302d827b92836fc0e5e1f6ee5b64d0952 \ + --hash=sha256:e3ad24ab2c2c7a58db0d6fc6aff654db7ad697335e13e00be3f27060245d4be6 +letsencrypt-apache==0.4.2 \ + --hash=sha256:4974a0fa021a6e2578081ceb7cf23200185f4e32a5ed866b28349591f13855ba \ + --hash=sha256:28d30038cac932bd6f2c90436a2846a849af3db7f1807fddbd19353851da33f2 diff --git a/letsencrypt-auto-source/pieces/peep.py b/letsencrypt-auto-source/pieces/peep.py deleted file mode 100755 index eee823ff2..000000000 --- a/letsencrypt-auto-source/pieces/peep.py +++ /dev/null @@ -1,970 +0,0 @@ -#!/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.exceptions import InstallationError -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__ = 3, 1, 1 - -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 -UNHANDLED_EXCEPTION = 3 - -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 '#' - (?Psha256):\s+ # Hash type is hardcoded to be sha256 for now. - (?P[^\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.*))? # 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_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, InstallationError, 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 - - comes_from = None - for req in chain.from_iterable( - _parse_requirements(path, package_finder(argv)) for path in paths): - req_path, req_line = path_and_line(req) - hashes = [hexlify(urlsafe_b64decode((hash + '=').encode('ascii'))).decode('ascii') - for hash in hashes_above(req_path, req_line)] - if req_path != comes_from: - print() - print('# from %s' % req_path) - print() - comes_from = req_path - - if not hashes: - print(req.req) - else: - print('%s' % (req.link if getattr(req, 'link', None) else 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(UNHANDLED_EXCEPTION) diff --git a/letsencrypt-auto-source/pieces/pipstrap.py b/letsencrypt-auto-source/pieces/pipstrap.py new file mode 100755 index 000000000..016f7ca13 --- /dev/null +++ b/letsencrypt-auto-source/pieces/pipstrap.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +"""A small script that can act as a trust root for installing pip 8 + +Embed this in your project, and your VCS checkout is all you have to trust. In +a post-peep era, this lets you claw your way to a hash-checking version of pip, +with which you can install the rest of your dependencies safely. All it assumes +is Python 2.6 or better and *some* version of pip already installed. If +anything goes wrong, it will exit with a non-zero status code. + +""" +# This is here so embedded copies are MIT-compliant: +# Copyright (c) 2016 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 +from hashlib import sha256 +from os.path import join +from pipes import quote +from shutil import rmtree +try: + from subprocess import check_output +except ImportError: + from subprocess import CalledProcessError, PIPE, Popen + + def check_output(*popenargs, **kwargs): + if 'stdout' in kwargs: + raise ValueError('stdout argument not allowed, it will be ' + 'overridden.') + process = Popen(stdout=PIPE, *popenargs, **kwargs) + output, unused_err = process.communicate() + retcode = process.poll() + if retcode: + cmd = kwargs.get("args") + if cmd is None: + cmd = popenargs[0] + raise CalledProcessError(retcode, cmd, output=output) + return output +from sys import exit, version_info +from tempfile import mkdtemp +try: + from urllib2 import build_opener, HTTPHandler, HTTPSHandler +except ImportError: + from urllib.request import build_opener, HTTPHandler, HTTPSHandler +try: + from urlparse import urlparse +except ImportError: + from urllib.parse import urlparse # 3.4 + + +__version__ = 1, 1, 0 + + +# wheel has a conditional dependency on argparse: +maybe_argparse = ( + [('https://pypi.python.org/packages/source/a/argparse/' + 'argparse-1.4.0.tar.gz', + '62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4')] + if version_info < (2, 7, 0) else []) + + +PACKAGES = maybe_argparse + [ + # Pip has no dependencies, as it vendors everything: + ('https://pypi.python.org/packages/source/p/pip/pip-8.0.3.tar.gz', + '30f98b66f3fe1069c529a491597d34a1c224a68640c82caf2ade5f88aa1405e8'), + # This version of setuptools has only optional dependencies: + ('https://pypi.python.org/packages/source/s/setuptools/' + 'setuptools-20.2.2.tar.gz', + '24fcfc15364a9fe09a220f37d2dcedc849795e3de3e4b393ee988e66a9cbd85a'), + ('https://pypi.python.org/packages/source/w/wheel/wheel-0.29.0.tar.gz', + '1ebb8ad7e26b448e9caa4773d2357849bf80ff9e313964bcaf79cbf0201a1648') +] + + +class HashError(Exception): + def __str__(self): + url, path, actual, expected = self.args + return ('{url} did not match the expected hash {expected}. Instead, ' + 'it was {actual}. The file (left at {path}) may have been ' + 'tampered with.'.format(**locals())) + + +def hashed_download(url, temp, digest): + """Download ``url`` to ``temp``, make sure it has the SHA-256 ``digest``, + and return its path.""" + # Based on pip 1.4.1's URLOpener but with cert verification removed. Python + # >=2.7.9 verifies HTTPS certs itself, and, in any case, the cert + # authenticity has only privacy (not arbitrary code execution) + # implications, since we're checking hashes. + def opener(): + opener = build_opener(HTTPSHandler()) + # Strip out HTTPHandler to prevent MITM spoof: + for handler in opener.handlers: + if isinstance(handler, HTTPHandler): + opener.handlers.remove(handler) + return opener + + def read_chunks(response, chunk_size): + while True: + chunk = response.read(chunk_size) + if not chunk: + break + yield chunk + + response = opener().open(url) + path = join(temp, urlparse(url).path.split('/')[-1]) + actual_hash = sha256() + with open(path, 'wb') as file: + for chunk in read_chunks(response, 4096): + file.write(chunk) + actual_hash.update(chunk) + + actual_digest = actual_hash.hexdigest() + if actual_digest != digest: + raise HashError(url, path, actual_digest, digest) + return path + + +def main(): + temp = mkdtemp(prefix='pipstrap-') + try: + downloads = [hashed_download(url, temp, digest) + for url, digest in PACKAGES] + check_output('pip install --no-index --no-deps -U ' + + ' '.join(quote(d) for d in downloads), + shell=True) + except HashError as exc: + print(exc) + except Exception: + rmtree(temp) + raise + else: + rmtree(temp) + return 0 + return 1 + + +if __name__ == '__main__': + exit(main()) diff --git a/letsencrypt-auto-source/tests/auto_test.py b/letsencrypt-auto-source/tests/auto_test.py index 90e09f57f..edb5f0c04 100644 --- a/letsencrypt-auto-source/tests/auto_test.py +++ b/letsencrypt-auto-source/tests/auto_test.py @@ -225,8 +225,8 @@ class AutoTests(TestCase): * 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. + * Pip hash-verification passes. + * Pip has a hash mismatch. * The OpenSSL sig matches. * The OpenSSL sig mismatches. @@ -252,8 +252,7 @@ class AutoTests(TestCase): """ NEW_LE_AUTO = build_le_auto( version='99.9.9', - requirements='# sha256: HMFNYatCTN7kRvUeUPESP4SC7HQFh_54YmyTO7ooc6A\n' - 'letsencrypt==99.9.9') + requirements='letsencrypt==99.9.9 --hash=sha256:1cc14d61ab424cdee446f51e50f1123f8482ec740587fe78626c933bba2873a0') NEW_LE_AUTO_SIG = signed(NEW_LE_AUTO) with ephemeral_dir() as venv_dir: @@ -272,7 +271,7 @@ class AutoTests(TestCase): 'dist')) # Test when a phase-1 upgrade is needed, there's no LE binary - # installed, and peep verifies: + # installed, and pip hashes verify: 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+', @@ -318,8 +317,8 @@ class AutoTests(TestCase): 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.""" + def test_pip_failure(self): + """Make sure pip stops us if there is a hash mismatch.""" with ephemeral_dir() as venv_dir: resources = {'': 'letsencrypt/', 'letsencrypt/json': dumps({'releases': {'99.9.9': None}})} @@ -328,15 +327,14 @@ class AutoTests(TestCase): install_le_auto( build_le_auto( version='99.9.9', - requirements='# sha256: badbadbadbadbadbadbadbadbadbadbadbadbadbadb\n' - 'configobj==5.0.6'), + requirements='configobj==5.0.6 --hash=sha256:badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb'), 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", + self.assertIn("THESE PACKAGES DO NOT MATCH THE HASHES " + "FROM THE REQUIREMENTS FILE", exc.output) ok_(not exists(join(venv_dir, 'letsencrypt')), msg="The virtualenv was left around, even though " @@ -345,5 +343,5 @@ class AutoTests(TestCase): "need to recreate the virtualenv, which hinges " "on the presence of $VENV_BIN/letsencrypt.") else: - self.fail("Peep didn't detect a bad hash and stop the " + self.fail("Pip didn't detect a bad hash and stop the " "installation.") diff --git a/letsencrypt-nginx/setup.py b/letsencrypt-nginx/setup.py index 656d6e04f..e53bef059 100644 --- a/letsencrypt-nginx/setup.py +++ b/letsencrypt-nginx/setup.py @@ -12,7 +12,9 @@ install_requires = [ 'letsencrypt=={0}'.format(version), 'PyOpenSSL', 'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary? - 'setuptools', # pkg_resources + # For pkg_resources. >=1.0 so pip resolves it to a version cryptography + # will tolerate; see #2599: + 'setuptools>=1.0', 'zope.interface', ] diff --git a/letsencrypt/achallenges.py b/letsencrypt/achallenges.py index 4d85f5d6a..0cdec06df 100644 --- a/letsencrypt/achallenges.py +++ b/letsencrypt/achallenges.py @@ -59,15 +59,3 @@ class DNS(AnnotatedChallenge): """Client annotated "dns" ACME challenge.""" __slots__ = ('challb', 'domain') acme_type = challenges.DNS - - -class RecoveryContact(AnnotatedChallenge): - """Client annotated "recoveryContact" ACME challenge.""" - __slots__ = ('challb', 'domain') - acme_type = challenges.RecoveryContact - - -class ProofOfPossession(AnnotatedChallenge): - """Client annotated "proofOfPossession" ACME challenge.""" - __slots__ = ('challb', 'domain') - acme_type = challenges.ProofOfPossession diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index ffbd70ced..285a1f3b7 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -9,7 +9,6 @@ from acme import challenges from acme import messages from letsencrypt import achallenges -from letsencrypt import constants from letsencrypt import errors from letsencrypt import error_handler from letsencrypt import interfaces @@ -21,13 +20,9 @@ logger = logging.getLogger(__name__) class AuthHandler(object): """ACME Authorization Handler for a client. - :ivar dv_auth: Authenticator capable of solving - :class:`~acme.challenges.DVChallenge` types - :type dv_auth: :class:`letsencrypt.interfaces.IAuthenticator` - - :ivar cont_auth: Authenticator capable of solving - :class:`~acme.challenges.ContinuityChallenge` types - :type cont_auth: :class:`letsencrypt.interfaces.IAuthenticator` + :ivar auth: Authenticator capable of solving + :class:`~acme.challenges.Challenge` types + :type auth: :class:`letsencrypt.interfaces.IAuthenticator` :ivar acme.client.Client acme: ACME client API. @@ -36,23 +31,19 @@ class AuthHandler(object): :ivar dict authzr: ACME Authorization Resource dict where keys are domains and values are :class:`acme.messages.AuthorizationResource` - :ivar list dv_c: DV challenges in the form of + :ivar list achalls: DV challenges in the form of :class:`letsencrypt.achallenges.AnnotatedChallenge` - :ivar list cont_c: Continuity challenges in the - form of :class:`letsencrypt.achallenges.AnnotatedChallenge` """ - def __init__(self, dv_auth, cont_auth, acme, account): - self.dv_auth = dv_auth - self.cont_auth = cont_auth + def __init__(self, auth, acme, account): + self.auth = auth self.acme = acme self.account = account self.authzr = dict() # List must be used to keep responses straight. - self.dv_c = [] - self.cont_c = [] + self.achalls = [] def get_authorizations(self, domains, best_effort=False): """Retrieve all authorizations for challenges. @@ -76,12 +67,12 @@ class AuthHandler(object): self._choose_challenges(domains) # While there are still challenges remaining... - while self.dv_c or self.cont_c: - cont_resp, dv_resp = self._solve_challenges() + while self.achalls: + resp = self._solve_challenges() logger.info("Waiting for verification...") - # Send all Responses - this modifies dv_c and cont_c - self._respond(cont_resp, dv_resp, best_effort) + # Send all Responses - this modifies achalls + self._respond(resp, best_effort) # Just make sure all decisions are complete. self.verify_authzr_complete() @@ -98,32 +89,27 @@ class AuthHandler(object): self._get_chall_pref(dom), self.authzr[dom].body.combinations) - dom_cont_c, dom_dv_c = self._challenge_factory( + dom_achalls = self._challenge_factory( dom, path) - self.dv_c.extend(dom_dv_c) - self.cont_c.extend(dom_cont_c) + self.achalls.extend(dom_achalls) def _solve_challenges(self): """Get Responses for challenges from authenticators.""" - cont_resp = [] - dv_resp = [] + resp = [] with error_handler.ErrorHandler(self._cleanup_challenges): try: - if self.cont_c: - cont_resp = self.cont_auth.perform(self.cont_c) - if self.dv_c: - dv_resp = self.dv_auth.perform(self.dv_c) + if self.achalls: + resp = self.auth.perform(self.achalls) except errors.AuthorizationError: logger.critical("Failure in setting up challenges.") logger.info("Attempting to clean up outstanding challenges...") raise - assert len(cont_resp) == len(self.cont_c) - assert len(dv_resp) == len(self.dv_c) + assert len(resp) == len(self.achalls) - return cont_resp, dv_resp + return resp - def _respond(self, cont_resp, dv_resp, best_effort): + def _respond(self, resp, best_effort): """Send/Receive confirmation of all challenges. .. note:: This method also cleans up the auth_handler state. @@ -131,17 +117,14 @@ class AuthHandler(object): """ # TODO: chall_update is a dirty hack to get around acme-spec #105 chall_update = dict() - active_achalls = [] - active_achalls.extend( - self._send_responses(self.dv_c, dv_resp, chall_update)) - active_achalls.extend( - self._send_responses(self.cont_c, cont_resp, chall_update)) + active_achalls = self._send_responses(self.achalls, + resp, chall_update) # Check for updated status... try: self._poll_challenges(chall_update, best_effort) finally: - # This removes challenges from self.dv_c and self.cont_c + # This removes challenges from self.achalls self._cleanup_challenges(active_achalls) def _send_responses(self, achalls, resps, chall_update): @@ -255,8 +238,7 @@ class AuthHandler(object): """ # Make sure to make a copy... chall_prefs = [] - chall_prefs.extend(self.cont_auth.get_chall_pref(domain)) - chall_prefs.extend(self.dv_auth.get_chall_pref(domain)) + chall_prefs.extend(self.auth.get_chall_pref(domain)) return chall_prefs def _cleanup_challenges(self, achall_list=None): @@ -268,22 +250,14 @@ class AuthHandler(object): logger.info("Cleaning up challenges") if achall_list is None: - dv_c = self.dv_c - cont_c = self.cont_c + achalls = self.achalls else: - dv_c = [achall for achall in achall_list - if isinstance(achall.chall, challenges.DVChallenge)] - cont_c = [achall for achall in achall_list if isinstance( - achall.chall, challenges.ContinuityChallenge)] + achalls = achall_list - if dv_c: - self.dv_auth.cleanup(dv_c) - for achall in dv_c: - self.dv_c.remove(achall) - if cont_c: - self.cont_auth.cleanup(cont_c) - for achall in cont_c: - self.cont_c.remove(achall) + if achalls: + self.auth.cleanup(achalls) + for achall in achalls: + self.achalls.remove(achall) def verify_authzr_complete(self): """Verifies that all authorizations have been decided. @@ -304,30 +278,20 @@ class AuthHandler(object): :param list path: List of indices from `challenges`. - :returns: dv_chall, list of DVChallenge type + :returns: achalls, list of challenge type :class:`letsencrypt.achallenges.Indexed` - cont_chall, list of ContinuityChallenge type - :class:`letsencrypt.achallenges.Indexed` - :rtype: tuple + :rtype: list :raises .errors.Error: if challenge type is not recognized """ - dv_chall = [] - cont_chall = [] + achalls = [] for index in path: challb = self.authzr[domain].body.challenges[index] - chall = challb.chall + achalls.append(challb_to_achall(challb, self.account.key, domain)) - achall = challb_to_achall(challb, self.account.key, domain) - - if isinstance(chall, challenges.ContinuityChallenge): - cont_chall.append(achall) - elif isinstance(chall, challenges.DVChallenge): - dv_chall.append(achall) - - return cont_chall, dv_chall + return achalls def challb_to_achall(challb, account_key, domain): @@ -349,12 +313,6 @@ def challb_to_achall(challb, account_key, domain): challb=challb, domain=domain, account_key=account_key) elif isinstance(chall, challenges.DNS): return achallenges.DNS(challb=challb, domain=domain) - elif isinstance(chall, challenges.RecoveryContact): - return achallenges.RecoveryContact( - challb=challb, domain=domain) - elif isinstance(chall, challenges.ProofOfPossession): - return achallenges.ProofOfPossession( - challb=challb, domain=domain) else: raise errors.Error( "Received unsupported challenge of type: %s", chall.typ) @@ -424,10 +382,7 @@ def _find_smart_path(challbs, preferences, combinations): combo_total = 0 if not best_combo: - msg = ("Client does not support any combination of challenges that " - "will satisfy the CA.") - logger.fatal(msg) - raise errors.AuthorizationError(msg) + _report_no_chall_path() return best_combo @@ -436,48 +391,29 @@ def _find_dumb_path(challbs, preferences): """Find challenge path without server hints. Should be called if the combinations hint is not included by the - server. This function returns the best path that does not contain - multiple mutually exclusive challenges. + server. This function either returns a path containing all + challenges provided by the CA or raises an exception. """ - assert len(preferences) == len(set(preferences)) - path = [] - satisfied = set() - for pref_c in preferences: - for i, offered_challb in enumerate(challbs): - if (isinstance(offered_challb.chall, pref_c) and - is_preferred(offered_challb, satisfied)): - path.append(i) - satisfied.add(offered_challb) + for i, challb in enumerate(challbs): + # supported is set to True if the challenge type is supported + supported = next((True for pref_c in preferences + if isinstance(challb.chall, pref_c)), False) + if supported: + path.append(i) + else: + _report_no_chall_path() + return path -def mutually_exclusive(obj1, obj2, groups, different=False): - """Are two objects mutually exclusive?""" - for group in groups: - obj1_present = False - obj2_present = False - - for obj_cls in group: - obj1_present |= isinstance(obj1, obj_cls) - obj2_present |= isinstance(obj2, obj_cls) - - if obj1_present and obj2_present and ( - not different or not isinstance(obj1, obj2.__class__)): - return False - return True - - -def is_preferred(offered_challb, satisfied, - exclusive_groups=constants.EXCLUSIVE_CHALLENGES): - """Return whether or not the challenge is preferred in path.""" - for challb in satisfied: - if not mutually_exclusive( - offered_challb.chall, challb.chall, exclusive_groups, - different=True): - return False - return True +def _report_no_chall_path(): + """Logs and raises an error that no satisfiable chall path exists.""" + msg = ("Client with the currently selected authenticator does not support " + "any combination of challenges that will satisfy the CA.") + logger.fatal(msg) + raise errors.AuthorizationError(msg) _ACME_PREFIX = "urn:acme:error:" diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 508eb48ad..dee43eac1 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -12,6 +12,7 @@ import traceback import configargparse import OpenSSL +import six import zope.component import zope.interface.exceptions import zope.interface.verify @@ -334,12 +335,18 @@ def _restore_required_config_elements(config, renewalparams): # int-valued items to add if they're present for config_item in INT_CONFIG_ITEMS: if config_item in renewalparams and not _set_by_cli(config_item): - try: - value = int(renewalparams[config_item]) - setattr(config.namespace, config_item, value) - except ValueError: - raise errors.Error( - "Expected a numeric value for {0}".format(config_item)) + config_value = renewalparams[config_item] + # the default value for http01_port was None during private beta + if config_item == "http01_port" and config_value == "None": + logger.info("updating legacy http01_port value") + int_value = flag_default("http01_port") + else: + try: + int_value = int(config_value) + except ValueError: + raise errors.Error( + "Expected a numeric value for {0}".format(config_item)) + setattr(config.namespace, config_item, int_value) def _restore_plugin_configs(config, renewalparams): @@ -370,7 +377,7 @@ def _restore_plugin_configs(config, renewalparams): if renewalparams.get("installer", None) is not None: plugin_prefixes.append(renewalparams["installer"]) for plugin_prefix in set(plugin_prefixes): - for config_item, config_value in renewalparams.iteritems(): + for config_item, config_value in six.iteritems(renewalparams): if config_item.startswith(plugin_prefix + "_") and not _set_by_cli(config_item): # Values None, True, and False need to be treated specially, # As they don't get parsed correctly based on type @@ -623,14 +630,14 @@ class HelpfulArgumentParser(object): """ def __init__(self, args, plugins, detect_defaults=False): + from letsencrypt import main self.VERBS = main.VERBS - # List of topics for which additional help can be provided HELP_TOPICS = ["all", "security", - "paths", "automation", "testing"] + main.VERBS.keys() + "paths", "automation", "testing"] + list(six.iterkeys(self.VERBS) - plugin_names = [name for name, _p in plugins.iteritems()] + plugin_names = list(six.iterkeys(plugins)) self.help_topics = HELP_TOPICS + plugin_names + [None] usage, short_usage = usage_strings(plugins) self.parser = configargparse.ArgParser( @@ -900,7 +907,7 @@ class HelpfulArgumentParser(object): may or may not be displayed as help topics. """ - for name, plugin_ep in plugins.iteritems(): + for name, plugin_ep in six.iteritems(plugins): parser_or_group = self.add_group(name, description=plugin_ep.description) #print(parser_or_group) plugin_ep.plugin_cls.inject_parser_options(parser_or_group, name) @@ -1295,7 +1302,7 @@ def process_domain(args_or_config, domain_arg, webroot_path=None): class WebrootMapProcessor(argparse.Action): # pylint: disable=missing-docstring def __call__(self, parser, args, webroot_map_arg, option_string=None): webroot_map = json.loads(webroot_map_arg) - for domains, webroot_path in webroot_map.iteritems(): + for domains, webroot_path in six.iteritems(webroot_map): process_domain(args, domains, [webroot_path]) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 9dfa70e8d..6134c4e6e 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -17,7 +17,6 @@ from letsencrypt import account from letsencrypt import auth_handler from letsencrypt import configuration from letsencrypt import constants -from letsencrypt import continuity_auth from letsencrypt import crypto_util from letsencrypt import errors from letsencrypt import error_handler @@ -161,21 +160,21 @@ class Client(object): :ivar .IConfig config: Client configuration. :ivar .Account account: Account registered with `register`. :ivar .AuthHandler auth_handler: Authorizations handler that will - dispatch DV and Continuity challenges to appropriate - authenticators (providing `.IAuthenticator` interface). - :ivar .IAuthenticator dv_auth: Prepared (`.IAuthenticator.prepare`) - authenticator that can solve the `.constants.DV_CHALLENGES`. + dispatch DV challenges to appropriate authenticators + (providing `.IAuthenticator` interface). + :ivar .IAuthenticator auth: Prepared (`.IAuthenticator.prepare`) + authenticator that can solve ACME challenges. :ivar .IInstaller installer: Installer. :ivar acme.client.Client acme: Optional ACME client API handle. You might already have one from `register`. """ - def __init__(self, config, account_, dv_auth, installer, acme=None): + def __init__(self, config, account_, auth, installer, acme=None): """Initialize a client.""" self.config = config self.account = account_ - self.dv_auth = dv_auth + self.auth = auth self.installer = installer # Initialize ACME if account is provided @@ -183,15 +182,9 @@ class Client(object): acme = acme_from_config_key(config, self.account.key) self.acme = acme - # TODO: Check if self.config.enroll_autorenew is None. If - # so, set it based to the default: figure out if dv_auth is - # standalone (then default is False, otherwise default is True) - - if dv_auth is not None: - cont_auth = continuity_auth.ContinuityAuthenticator(config, - installer) + if auth is not None: self.auth_handler = auth_handler.AuthHandler( - dv_auth, cont_auth, self.acme, self.account) + auth, self.acme, self.account) else: self.auth_handler = None diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 402f5e9a1..f8ef1e845 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -44,11 +44,6 @@ RENEWER_DEFAULTS = dict( """Defaults for renewer script.""" -EXCLUSIVE_CHALLENGES = frozenset([frozenset([ - challenges.TLSSNI01, challenges.HTTP01])]) -"""Mutually exclusive challenges.""" - - ENHANCEMENTS = ["redirect", "http-header", "ocsp-stapling", "spdy"] """List of possible :class:`letsencrypt.interfaces.IInstaller` enhancements. diff --git a/letsencrypt/continuity_auth.py b/letsencrypt/continuity_auth.py deleted file mode 100644 index 28612bb17..000000000 --- a/letsencrypt/continuity_auth.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Continuity Authenticator""" -import zope.interface - -from acme import challenges - -from letsencrypt import achallenges -from letsencrypt import errors -from letsencrypt import interfaces -from letsencrypt import proof_of_possession - - -@zope.interface.implementer(interfaces.IAuthenticator) -class ContinuityAuthenticator(object): - """IAuthenticator for - :const:`~acme.challenges.ContinuityChallenge` class challenges. - - :ivar proof_of_pos: Performs "proofOfPossession" challenges. - :type proof_of_pos: - :class:`letsencrypt.proof_of_possession.Proof_of_Possession` - - """ - - # This will have an installer soon for get_key/cert purposes - def __init__(self, config, installer): # pylint: disable=unused-argument - """Initialize Client Authenticator. - - :param config: Configuration. - :type config: :class:`letsencrypt.interfaces.IConfig` - - :param installer: Let's Encrypt Installer. - :type installer: :class:`letsencrypt.interfaces.IInstaller` - - """ - self.proof_of_pos = proof_of_possession.ProofOfPossession(installer) - - def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use - """Return list of challenge preferences.""" - return [challenges.ProofOfPossession] - - def perform(self, achalls): - """Perform client specific challenges for IAuthenticator""" - responses = [] - for achall in achalls: - if isinstance(achall, achallenges.ProofOfPossession): - responses.append(self.proof_of_pos.perform(achall)) - else: - raise errors.ContAuthError("Unexpected Challenge") - return responses - - def cleanup(self, achalls): # pylint: disable=no-self-use - """Cleanup call for IAuthenticator.""" - for achall in achalls: - if not isinstance(achall, achallenges.ProofOfPossession): - raise errors.ContAuthError("Unexpected Challenge") diff --git a/letsencrypt/errors.py b/letsencrypt/errors.py index 99bb29d9d..b2b078f6a 100644 --- a/letsencrypt/errors.py +++ b/letsencrypt/errors.py @@ -48,19 +48,6 @@ class FailedChallenges(AuthorizationError): for achall in self.failed_achalls if achall.error is not None)) -class ContAuthError(AuthorizationError): - """Let's Encrypt Continuity Authenticator error.""" - - -class DvAuthError(AuthorizationError): - """Let's Encrypt DV Authenticator error.""" - - -# Authenticator - Challenge specific errors -class TLSSNI01Error(DvAuthError): - """Let's Encrypt TLSSNI01 error.""" - - # Plugin Errors class PluginError(Error): """Let's Encrypt Plugin error.""" @@ -86,10 +73,6 @@ class NotSupportedError(PluginError): """Let's Encrypt Plugin function not supported error.""" -class RevokerError(Error): - """Let's Encrypt Revoker error.""" - - class StandaloneBindError(Error): """Standalone plugin bind error.""" diff --git a/letsencrypt/plugins/webroot_test.py b/letsencrypt/plugins/webroot_test.py index 7a34b3fcc..8c1427340 100644 --- a/letsencrypt/plugins/webroot_test.py +++ b/letsencrypt/plugins/webroot_test.py @@ -1,4 +1,7 @@ """Tests for letsencrypt.plugins.webroot.""" + +from __future__ import print_function + import errno import os import shutil @@ -74,7 +77,7 @@ class AuthenticatorTest(unittest.TestCase): os.chmod(self.path, 0o000) try: open(permission_canary, "r") - print "Warning, running tests as root skips permissions tests..." + print("Warning, running tests as root skips permissions tests...") except IOError: # ok, permissions work, test away... self.assertRaises(errors.PluginError, self.auth.prepare) diff --git a/letsencrypt/proof_of_possession.py b/letsencrypt/proof_of_possession.py deleted file mode 100644 index 7928c60e7..000000000 --- a/letsencrypt/proof_of_possession.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Proof of Possession Identifier Validation Challenge.""" -import logging -import os - -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -import zope.component - -from acme import challenges -from acme import jose -from acme import other - -from letsencrypt import interfaces -from letsencrypt.display import util as display_util - - -logger = logging.getLogger(__name__) - - -class ProofOfPossession(object): # pylint: disable=too-few-public-methods - """Proof of Possession Identifier Validation Challenge. - - Based on draft-barnes-acme, section 6.5. - - :ivar installer: Installer object - :type installer: :class:`~letsencrypt.interfaces.IInstaller` - - """ - def __init__(self, installer): - self.installer = installer - - def perform(self, achall): - """Perform the Proof of Possession Challenge. - - :param achall: Proof of Possession Challenge - :type achall: :class:`letsencrypt.achallenges.ProofOfPossession` - - :returns: Response or None/False if the challenge cannot be completed - :rtype: :class:`acme.challenges.ProofOfPossessionResponse` - or False - - """ - if (achall.alg in [jose.HS256, jose.HS384, jose.HS512] or - not isinstance(achall.hints.jwk, achall.alg.kty)): - return None - - for cert, key, _ in self.installer.get_all_certs_keys(): - with open(cert) as cert_file: - cert_data = cert_file.read() - try: - cert_obj = x509.load_pem_x509_certificate( - cert_data, default_backend()) - except ValueError: - try: - cert_obj = x509.load_der_x509_certificate( - cert_data, default_backend()) - except ValueError: - logger.warn("Certificate is neither PER nor DER: %s", cert) - - cert_key = achall.alg.kty(key=cert_obj.public_key()) - if cert_key == achall.hints.jwk: - return self._gen_response(achall, key) - - # Is there are different prompt we should give the user? - code, key = zope.component.getUtility( - interfaces.IDisplay).input( - "Path to private key for identifier: %s " % achall.domain) - if code != display_util.CANCEL: - return self._gen_response(achall, key) - - # If we get here, the key wasn't found - return False - - def _gen_response(self, achall, key_path): # pylint: disable=no-self-use - """Create the response to the Proof of Possession Challenge. - - :param achall: Proof of Possession Challenge - :type achall: :class:`letsencrypt.achallenges.ProofOfPossession` - - :param str key_path: Path to the key corresponding to the hinted to - public key. - - :returns: Response or False if the challenge cannot be completed - :rtype: :class:`acme.challenges.ProofOfPossessionResponse` - or False - - """ - if os.path.isfile(key_path): - with open(key_path, 'rb') as key: - try: - # Needs to be changed if JWKES doesn't have a key attribute - jwk = achall.alg.kty.load(key.read()) - sig = other.Signature.from_msg(achall.nonce, jwk.key, - alg=achall.alg) - except (IndexError, ValueError, TypeError, jose.errors.Error): - return False - return challenges.ProofOfPossessionResponse(nonce=achall.nonce, - signature=sig) - return False diff --git a/letsencrypt/reporter.py b/letsencrypt/reporter.py index 81106be34..147928e3c 100644 --- a/letsencrypt/reporter.py +++ b/letsencrypt/reporter.py @@ -4,10 +4,10 @@ from __future__ import print_function import collections import logging import os -import Queue import sys import textwrap +from six.moves import queue # pylint: disable=import-error import zope.interface from letsencrypt import interfaces @@ -21,7 +21,7 @@ logger = logging.getLogger(__name__) class Reporter(object): """Collects and displays information to the user. - :ivar `Queue.PriorityQueue` messages: Messages to be displayed to + :ivar `queue.PriorityQueue` messages: Messages to be displayed to the user. """ @@ -36,7 +36,7 @@ class Reporter(object): _msg_type = collections.namedtuple('ReporterMsg', 'priority text on_crash') def __init__(self): - self.messages = Queue.PriorityQueue() + self.messages = queue.PriorityQueue() def add_message(self, msg, priority, on_crash=True): """Adds msg to the list of messages to be printed. diff --git a/letsencrypt/storage.py b/letsencrypt/storage.py index 6786ac745..cff2d53e1 100644 --- a/letsencrypt/storage.py +++ b/letsencrypt/storage.py @@ -694,7 +694,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes for i in (cli_config.renewal_configs_dir, cli_config.archive_dir, cli_config.live_dir): if not os.path.exists(i): - os.makedirs(i, 0700) + os.makedirs(i, 0o700) logger.debug("Creating directory %s.", i) config_file, config_filename = le_util.unique_lineage_name( cli_config.renewal_configs_dir, lineagename) diff --git a/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py index 6b07b840f..ea5438923 100644 --- a/letsencrypt/tests/acme_util.py +++ b/letsencrypt/tests/acme_util.py @@ -17,51 +17,14 @@ HTTP01 = challenges.HTTP01( TLSSNI01 = challenges.TLSSNI01( token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA")) DNS = challenges.DNS(token="17817c66b60ce2e4012dfad92657527a") -RECOVERY_CONTACT = challenges.RecoveryContact( - activation_url="https://example.ca/sendrecovery/a5bd99383fb0", - success_url="https://example.ca/confirmrecovery/bb1b9928932", - contact="c********n@example.com") -POP = challenges.ProofOfPossession( - alg="RS256", nonce=jose.b64decode("eET5udtV7aoX8Xl8gYiZIA"), - hints=challenges.ProofOfPossession.Hints( - jwk=jose.JWKRSA(key=KEY.public_key()), - cert_fingerprints=( - "93416768eb85e33adc4277f4c9acd63e7418fcfe", - "16d95b7b63f1972b980b14c20291f3c0d1855d95", - "48b46570d9fc6358108af43ad1649484def0debf" - ), - certs=(), # TODO - subject_key_identifiers=("d0083162dcc4c8a23ecb8aecbd86120e56fd24e5"), - serial_numbers=(34234239832, 23993939911, 17), - issuers=( - "C=US, O=SuperT LLC, CN=SuperTrustworthy Public CA", - "O=LessTrustworthy CA Inc, CN=LessTrustworthy But StillSecure", - ), - authorized_for=("www.example.com", "example.net"), - ) -) -CHALLENGES = [HTTP01, TLSSNI01, DNS, RECOVERY_CONTACT, POP] -DV_CHALLENGES = [chall for chall in CHALLENGES - if isinstance(chall, challenges.DVChallenge)] -CONT_CHALLENGES = [chall for chall in CHALLENGES - if isinstance(chall, challenges.ContinuityChallenge)] +CHALLENGES = [HTTP01, TLSSNI01, DNS] def gen_combos(challbs): """Generate natural combinations for challbs.""" - dv_chall = [] - cont_chall = [] - - for i, challb in enumerate(challbs): # pylint: disable=redefined-outer-name - if isinstance(challb.chall, challenges.DVChallenge): - dv_chall.append(i) - else: - cont_chall.append(i) - - # Gen combos for 1 of each type, lowest index first (makes testing easier) - return tuple((i, j) if i < j else (j, i) - for i in dv_chall for j in cont_chall) + # completing a single DV challenge satisfies the CA + return tuple((i,) for i, _ in enumerate(challbs)) def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name @@ -82,16 +45,8 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name TLSSNI01_P = chall_to_challb(TLSSNI01, messages.STATUS_PENDING) HTTP01_P = chall_to_challb(HTTP01, messages.STATUS_PENDING) DNS_P = chall_to_challb(DNS, messages.STATUS_PENDING) -RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages.STATUS_PENDING) -POP_P = chall_to_challb(POP, messages.STATUS_PENDING) -CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS_P, RECOVERY_CONTACT_P, POP_P] -DV_CHALLENGES_P = [challb for challb in CHALLENGES_P - if isinstance(challb.chall, challenges.DVChallenge)] -CONT_CHALLENGES_P = [ - challb for challb in CHALLENGES_P - if isinstance(challb.chall, challenges.ContinuityChallenge) -] +CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS_P] def gen_authzr(authz_status, domain, challs, statuses, combos=True): diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 5a6199ca3..426a269ac 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -23,8 +23,7 @@ class ChallengeFactoryTest(unittest.TestCase): from letsencrypt.auth_handler import AuthHandler # Account is mocked... - self.handler = AuthHandler( - None, None, None, mock.Mock(key="mock_key")) + self.handler = AuthHandler(None, None, mock.Mock(key="mock_key")) self.dom = "test" self.handler.authzr[self.dom] = acme_util.gen_authzr( @@ -32,20 +31,17 @@ class ChallengeFactoryTest(unittest.TestCase): [messages.STATUS_PENDING] * 6, False) def test_all(self): - cont_c, dv_c = self.handler._challenge_factory( + achalls = self.handler._challenge_factory( self.dom, range(0, len(acme_util.CHALLENGES))) self.assertEqual( - [achall.chall for achall in cont_c], acme_util.CONT_CHALLENGES) - self.assertEqual( - [achall.chall for achall in dv_c], acme_util.DV_CHALLENGES) + [achall.chall for achall in achalls], acme_util.CHALLENGES) - def test_one_dv_one_cont(self): - cont_c, dv_c = self.handler._challenge_factory(self.dom, [1, 3]) + def test_one_tls_sni(self): + achalls = self.handler._challenge_factory(self.dom, [1]) self.assertEqual( - [achall.chall for achall in cont_c], [acme_util.RECOVERY_CONTACT]) - self.assertEqual([achall.chall for achall in dv_c], [acme_util.TLSSNI01]) + [achall.chall for achall in achalls], [acme_util.TLSSNI01]) def test_unrecognized(self): self.handler.authzr["failure.com"] = acme_util.gen_authzr( @@ -67,22 +63,17 @@ class GetAuthorizationsTest(unittest.TestCase): def setUp(self): from letsencrypt.auth_handler import AuthHandler - self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator") - self.mock_cont_auth = mock.MagicMock(name="ContinuityAuthenticator") + self.mock_auth = mock.MagicMock(name="ApacheConfigurator") - self.mock_dv_auth.get_chall_pref.return_value = [challenges.TLSSNI01] - self.mock_cont_auth.get_chall_pref.return_value = [ - challenges.RecoveryContact] + self.mock_auth.get_chall_pref.return_value = [challenges.TLSSNI01] - self.mock_cont_auth.perform.side_effect = gen_auth_resp - self.mock_dv_auth.perform.side_effect = gen_auth_resp + self.mock_auth.perform.side_effect = gen_auth_resp self.mock_account = mock.Mock(key=le_util.Key("file_path", "PEM")) self.mock_net = mock.MagicMock(spec=acme_client.Client) self.handler = AuthHandler( - self.mock_dv_auth, self.mock_cont_auth, - self.mock_net, self.mock_account) + self.mock_auth, self.mock_net, self.mock_account) logging.disable(logging.CRITICAL) @@ -92,7 +83,7 @@ class GetAuthorizationsTest(unittest.TestCase): @mock.patch("letsencrypt.auth_handler.AuthHandler._poll_challenges") def test_name1_tls_sni_01_1(self, mock_poll): self.mock_net.request_domain_challenges.side_effect = functools.partial( - gen_dom_authzr, challs=acme_util.DV_CHALLENGES) + gen_dom_authzr, challs=acme_util.CHALLENGES) mock_poll.side_effect = self._validate_all @@ -105,16 +96,40 @@ class GetAuthorizationsTest(unittest.TestCase): self.assertEqual(chall_update.keys(), ["0"]) self.assertEqual(len(chall_update.values()), 1) - self.assertEqual(self.mock_dv_auth.cleanup.call_count, 1) - self.assertEqual(self.mock_cont_auth.cleanup.call_count, 0) + self.assertEqual(self.mock_auth.cleanup.call_count, 1) # Test if list first element is TLSSNI01, use typ because it is an achall self.assertEqual( - self.mock_dv_auth.cleanup.call_args[0][0][0].typ, "tls-sni-01") + self.mock_auth.cleanup.call_args[0][0][0].typ, "tls-sni-01") self.assertEqual(len(authzr), 1) @mock.patch("letsencrypt.auth_handler.AuthHandler._poll_challenges") - def test_name3_tls_sni_01_3_rectok_3(self, mock_poll): + def test_name1_tls_sni_01_1_http_01_1_dns_1(self, mock_poll): + self.mock_net.request_domain_challenges.side_effect = functools.partial( + gen_dom_authzr, challs=acme_util.CHALLENGES, combos=False) + + mock_poll.side_effect = self._validate_all + self.mock_auth.get_chall_pref.return_value.append(challenges.HTTP01) + self.mock_auth.get_chall_pref.return_value.append(challenges.DNS) + + authzr = self.handler.get_authorizations(["0"]) + + self.assertEqual(self.mock_net.answer_challenge.call_count, 3) + + self.assertEqual(mock_poll.call_count, 1) + chall_update = mock_poll.call_args[0][0] + self.assertEqual(chall_update.keys(), ["0"]) + self.assertEqual(len(chall_update.values()), 1) + + self.assertEqual(self.mock_auth.cleanup.call_count, 1) + # Test if list first element is TLSSNI01, use typ because it is an achall + for achall in self.mock_auth.cleanup.call_args[0][0]: + self.assertTrue(achall.typ in ["tls-sni-01", "http-01", "dns"]) + + self.assertEqual(len(authzr), 1) + + @mock.patch("letsencrypt.auth_handler.AuthHandler._poll_challenges") + def test_name3_tls_sni_01_3(self, mock_poll): self.mock_net.request_domain_challenges.side_effect = functools.partial( gen_dom_authzr, challs=acme_util.CHALLENGES) @@ -122,28 +137,27 @@ class GetAuthorizationsTest(unittest.TestCase): authzr = self.handler.get_authorizations(["0", "1", "2"]) - self.assertEqual(self.mock_net.answer_challenge.call_count, 6) + self.assertEqual(self.mock_net.answer_challenge.call_count, 3) # Check poll call self.assertEqual(mock_poll.call_count, 1) chall_update = mock_poll.call_args[0][0] self.assertEqual(len(chall_update.keys()), 3) self.assertTrue("0" in chall_update.keys()) - self.assertEqual(len(chall_update["0"]), 2) + self.assertEqual(len(chall_update["0"]), 1) self.assertTrue("1" in chall_update.keys()) - self.assertEqual(len(chall_update["1"]), 2) + self.assertEqual(len(chall_update["1"]), 1) self.assertTrue("2" in chall_update.keys()) - self.assertEqual(len(chall_update["2"]), 2) + self.assertEqual(len(chall_update["2"]), 1) - self.assertEqual(self.mock_dv_auth.cleanup.call_count, 1) - self.assertEqual(self.mock_cont_auth.cleanup.call_count, 1) + self.assertEqual(self.mock_auth.cleanup.call_count, 1) self.assertEqual(len(authzr), 3) def test_perform_failure(self): self.mock_net.request_domain_challenges.side_effect = functools.partial( gen_dom_authzr, challs=acme_util.CHALLENGES) - self.mock_dv_auth.perform.side_effect = errors.AuthorizationError + self.mock_auth.perform.side_effect = errors.AuthorizationError self.assertRaises( errors.AuthorizationError, self.handler.get_authorizations, ["0"]) @@ -170,20 +184,21 @@ class PollChallengesTest(unittest.TestCase): # Account and network are mocked... self.mock_net = mock.MagicMock() self.handler = AuthHandler( - None, None, self.mock_net, mock.Mock(key="mock_key")) + None, self.mock_net, mock.Mock(key="mock_key")) self.doms = ["0", "1", "2"] self.handler.authzr[self.doms[0]] = acme_util.gen_authzr( messages.STATUS_PENDING, self.doms[0], - acme_util.DV_CHALLENGES, [messages.STATUS_PENDING] * 3, False) + [acme_util.HTTP01, acme_util.TLSSNI01], + [messages.STATUS_PENDING] * 2, False) self.handler.authzr[self.doms[1]] = acme_util.gen_authzr( messages.STATUS_PENDING, self.doms[1], - acme_util.DV_CHALLENGES, [messages.STATUS_PENDING] * 3, False) + acme_util.CHALLENGES, [messages.STATUS_PENDING] * 3, False) self.handler.authzr[self.doms[2]] = acme_util.gen_authzr( messages.STATUS_PENDING, self.doms[2], - acme_util.DV_CHALLENGES, [messages.STATUS_PENDING] * 3, False) + acme_util.CHALLENGES, [messages.STATUS_PENDING] * 3, False) self.chall_update = {} for dom in self.doms: @@ -220,7 +235,7 @@ class PollChallengesTest(unittest.TestCase): from letsencrypt.auth_handler import challb_to_achall self.mock_net.poll.side_effect = self._mock_poll_solve_one_valid self.chall_update[self.doms[0]].append( - challb_to_achall(acme_util.RECOVERY_CONTACT_P, "key", self.doms[0])) + challb_to_achall(acme_util.DNS_P, "key", self.doms[0])) self.assertRaises( errors.AuthorizationError, self.handler._poll_challenges, self.chall_update, False) @@ -311,7 +326,7 @@ class GenChallengePathTest(unittest.TestCase): def test_common_case(self): """Given TLSSNI01 and HTTP01 with appropriate combos.""" challbs = (acme_util.TLSSNI01_P, acme_util.HTTP01_P) - prefs = [challenges.TLSSNI01] + prefs = [challenges.TLSSNI01, challenges.HTTP01] combos = ((0,), (1,)) # Smart then trivial dumb path test @@ -321,111 +336,17 @@ class GenChallengePathTest(unittest.TestCase): self.assertEqual(self._call(challbs[::-1], prefs, combos), (1,)) self.assertTrue(self._call(challbs[::-1], prefs, None)) - def test_common_case_with_continuity(self): - challbs = (acme_util.POP_P, - acme_util.RECOVERY_CONTACT_P, - acme_util.TLSSNI01_P, - acme_util.HTTP01_P) - prefs = [challenges.ProofOfPossession, challenges.TLSSNI01] - combos = acme_util.gen_combos(challbs) - self.assertEqual(self._call(challbs, prefs, combos), (0, 2)) - - # dumb_path() trivial test - self.assertTrue(self._call(challbs, prefs, None)) - - def test_full_cont_server(self): - challbs = (acme_util.RECOVERY_CONTACT_P, - acme_util.POP_P, - acme_util.TLSSNI01_P, - acme_util.HTTP01_P, - acme_util.DNS_P) - # Typical webserver client that can do everything except DNS - # Attempted to make the order realistic - prefs = [challenges.ProofOfPossession, - challenges.HTTP01, - challenges.TLSSNI01, - challenges.RecoveryContact] - combos = acme_util.gen_combos(challbs) - self.assertEqual(self._call(challbs, prefs, combos), (1, 3)) - - # Dumb path trivial test - self.assertTrue(self._call(challbs, prefs, None)) - def test_not_supported(self): - challbs = (acme_util.POP_P, acme_util.TLSSNI01_P) + challbs = (acme_util.DNS_P, acme_util.TLSSNI01_P) prefs = [challenges.TLSSNI01] combos = ((0, 1),) + # smart path fails because no challs in perfs satisfies combos self.assertRaises( errors.AuthorizationError, self._call, challbs, prefs, combos) - - -class MutuallyExclusiveTest(unittest.TestCase): - """Tests for letsencrypt.auth_handler.mutually_exclusive.""" - - # pylint: disable=missing-docstring,too-few-public-methods - class A(object): - pass - - class B(object): - pass - - class C(object): - pass - - class D(C): - pass - - @classmethod - def _call(cls, chall1, chall2, different=False): - from letsencrypt.auth_handler import mutually_exclusive - return mutually_exclusive(chall1, chall2, groups=frozenset([ - frozenset([cls.A, cls.B]), frozenset([cls.A, cls.C]), - ]), different=different) - - def test_group_members(self): - self.assertFalse(self._call(self.A(), self.B())) - self.assertFalse(self._call(self.A(), self.C())) - - def test_cross_group(self): - self.assertTrue(self._call(self.B(), self.C())) - - def test_same_type(self): - self.assertFalse(self._call(self.A(), self.A(), different=False)) - self.assertTrue(self._call(self.A(), self.A(), different=True)) - - # in particular... - obj = self.A() - self.assertFalse(self._call(obj, obj, different=False)) - self.assertTrue(self._call(obj, obj, different=True)) - - def test_subclass(self): - self.assertFalse(self._call(self.A(), self.D())) - self.assertFalse(self._call(self.D(), self.A())) - - -class IsPreferredTest(unittest.TestCase): - """Tests for letsencrypt.auth_handler.is_preferred.""" - - @classmethod - def _call(cls, chall, satisfied): - from letsencrypt.auth_handler import is_preferred - return is_preferred(chall, satisfied, exclusive_groups=frozenset([ - frozenset([challenges.TLSSNI01, challenges.HTTP01]), - frozenset([challenges.DNS, challenges.HTTP01]), - ])) - - def test_empty_satisfied(self): - self.assertTrue(self._call(acme_util.DNS_P, frozenset())) - - def test_mutually_exclusvie(self): - self.assertFalse( - self._call( - acme_util.TLSSNI01_P, frozenset([acme_util.HTTP01_P]))) - - def test_mutually_exclusive_same_type(self): - self.assertTrue( - self._call(acme_util.TLSSNI01_P, frozenset([acme_util.TLSSNI01_P]))) + # dumb path fails because all challbs are not supported + self.assertRaises( + errors.AuthorizationError, self._call, challbs, prefs, None) class ReportFailedChallsTest(unittest.TestCase): @@ -486,11 +407,11 @@ def gen_auth_resp(chall_list): for chall in chall_list] -def gen_dom_authzr(domain, unused_new_authzr_uri, challs): +def gen_dom_authzr(domain, unused_new_authzr_uri, challs, combos=True): """Generates new authzr for domains.""" return acme_util.gen_authzr( messages.STATUS_PENDING, domain, challs, - [messages.STATUS_PENDING] * len(challs)) + [messages.STATUS_PENDING] * len(challs), combos) if __name__ == "__main__": diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 282a0b1d3..c5865206d 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -1,15 +1,18 @@ """Tests for letsencrypt.cli.""" + +from __future__ import print_function + import argparse import functools import itertools import os import shutil -import StringIO import traceback import tempfile import unittest import mock +import six from acme import jose @@ -82,7 +85,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods def _help_output(self, args): "Run a command, and return the ouput string for scrutiny" - output = StringIO.StringIO() + + output = six.StringIO() with mock.patch('letsencrypt.main.sys.stdout', new=output): self.assertRaises(SystemExit, self._call_stdout, args) out = output.getvalue() @@ -581,7 +585,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods try: ret, _, _, _ = self._call(args) if ret: - print "Returned", ret + print("Returned", ret) raise AssertionError(ret) assert not error_expected, "renewal should have errored" except: # pylint: disable=bare-except @@ -629,8 +633,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods def _dump_log(self): with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf: - print "Logs:" - print lf.read() + print("Logs:") + print(lf.read()) def _make_test_renewal_conf(self, testfile): @@ -711,6 +715,12 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods self._test_renew_common(renewalparams=renewalparams, error_expected=True, assert_oc_called=False) + def test_renew_with_nonetype_http01(self): + renewalparams = {'authenticator': 'webroot', + 'http01_port': 'None'} + self._test_renew_common(renewalparams=renewalparams, error_expected=False, + assert_oc_called=True) + def test_renew_with_bad_domain(self): renewalparams = {'authenticator': 'webroot'} names = ['*.example.com'] diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index b1a3fc779..7dd513e18 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -107,7 +107,7 @@ class ClientTest(unittest.TestCase): self.acme = acme.return_value = mock.MagicMock() self.client = Client( config=self.config, account_=self.account, - dv_auth=None, installer=None) + auth=None, installer=None) def test_init_acme_verify_ssl(self): net = self.acme_client.call_args[1]["net"] diff --git a/letsencrypt/tests/colored_logging_test.py b/letsencrypt/tests/colored_logging_test.py index 5b49ec820..4080157fc 100644 --- a/letsencrypt/tests/colored_logging_test.py +++ b/letsencrypt/tests/colored_logging_test.py @@ -1,8 +1,9 @@ """Tests for letsencrypt.colored_logging.""" import logging -import StringIO import unittest +import six + from letsencrypt import le_util @@ -12,7 +13,7 @@ class StreamHandlerTest(unittest.TestCase): def setUp(self): from letsencrypt import colored_logging - self.stream = StringIO.StringIO() + self.stream = six.StringIO() self.stream.isatty = lambda: True self.handler = colored_logging.StreamHandler(self.stream) diff --git a/letsencrypt/tests/continuity_auth_test.py b/letsencrypt/tests/continuity_auth_test.py deleted file mode 100644 index 70287bd01..000000000 --- a/letsencrypt/tests/continuity_auth_test.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Test for letsencrypt.continuity_auth.""" -import unittest - -import mock - -from acme import challenges - -from letsencrypt import achallenges -from letsencrypt import errors - - -class PerformTest(unittest.TestCase): - """Test client perform function.""" - - def setUp(self): - from letsencrypt.continuity_auth import ContinuityAuthenticator - - self.auth = ContinuityAuthenticator( - mock.MagicMock(server="demo_server.org"), None) - self.auth.proof_of_pos.perform = mock.MagicMock( - name="proof_of_pos_perform", side_effect=gen_client_resp) - - def test_pop(self): - achalls = [] - for i in xrange(4): - achalls.append(achallenges.ProofOfPossession( - challb=None, domain=str(i))) - responses = self.auth.perform(achalls) - - self.assertEqual(len(responses), 4) - for i in xrange(4): - self.assertEqual(responses[i], "ProofOfPossession%d" % i) - - def test_unexpected(self): - self.assertRaises( - errors.ContAuthError, self.auth.perform, [ - achallenges.KeyAuthorizationAnnotatedChallenge( - challb=None, domain="0", account_key="invalid_key")]) - - def test_chall_pref(self): - self.assertEqual( - self.auth.get_chall_pref("example.com"), - [challenges.ProofOfPossession]) - - -class CleanupTest(unittest.TestCase): - """Test the Authenticator cleanup function.""" - - def setUp(self): - from letsencrypt.continuity_auth import ContinuityAuthenticator - - self.auth = ContinuityAuthenticator( - mock.MagicMock(server="demo_server.org"), None) - - def test_unexpected(self): - unexpected = achallenges.KeyAuthorizationAnnotatedChallenge( - challb=None, domain="0", account_key="dummy_key") - self.assertRaises(errors.ContAuthError, self.auth.cleanup, [unexpected]) - - -def gen_client_resp(chall): - """Generate a dummy response.""" - return "%s%s" % (chall.__class__.__name__, chall.domain) - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index 87894f837..191b70801 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -4,11 +4,11 @@ import errno import os import shutil import stat -import StringIO import tempfile import unittest import mock +import six from letsencrypt import errors @@ -307,14 +307,14 @@ class AddDeprecatedArgumentTest(unittest.TestCase): self.assertTrue("--old-option is deprecated" in stderr) def _get_argparse_warnings(self, args): - stderr = StringIO.StringIO() + stderr = six.StringIO() with mock.patch("letsencrypt.le_util.sys.stderr", new=stderr): self.parser.parse_args(args) return stderr.getvalue() def test_help(self): self._call("--old-option", 2) - stdout = StringIO.StringIO() + stdout = six.StringIO() with mock.patch("letsencrypt.le_util.sys.stdout", new=stdout): try: self.parser.parse_args(["-h"]) diff --git a/letsencrypt/tests/proof_of_possession_test.py b/letsencrypt/tests/proof_of_possession_test.py deleted file mode 100644 index f2e7b2021..000000000 --- a/letsencrypt/tests/proof_of_possession_test.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Tests for letsencrypt.proof_of_possession.""" -import os -import tempfile -import unittest - -import mock - -from acme import challenges -from acme import jose -from acme import messages - -from letsencrypt import achallenges -from letsencrypt import proof_of_possession -from letsencrypt.display import util as display_util - -from letsencrypt.tests import test_util - - -CERT0_PATH = test_util.vector_path("cert.der") -CERT2_PATH = test_util.vector_path("dsa_cert.pem") -CERT2_KEY_PATH = test_util.vector_path("dsa512_key.pem") -CERT3_PATH = test_util.vector_path("matching_cert.pem") -CERT3_KEY_PATH = test_util.vector_path("rsa512_key_2.pem") -CERT3_KEY = test_util.load_rsa_private_key("rsa512_key_2.pem").public_key() - - -class ProofOfPossessionTest(unittest.TestCase): - def setUp(self): - self.installer = mock.MagicMock() - self.cert1_path = tempfile.mkstemp()[1] - certs = [CERT0_PATH, self.cert1_path, CERT2_PATH, CERT3_PATH] - keys = [None, None, CERT2_KEY_PATH, CERT3_KEY_PATH] - self.installer.get_all_certs_keys.return_value = zip( - certs, keys, 4 * [None]) - self.proof_of_pos = proof_of_possession.ProofOfPossession( - self.installer) - - hints = challenges.ProofOfPossession.Hints( - jwk=jose.JWKRSA(key=CERT3_KEY), cert_fingerprints=(), - certs=(), serial_numbers=(), subject_key_identifiers=(), - issuers=(), authorized_for=()) - chall = challenges.ProofOfPossession( - alg=jose.RS256, nonce='zczv4HMLVe_0kimJ25Juig', hints=hints) - challb = messages.ChallengeBody( - chall=chall, uri="http://example", status=messages.STATUS_PENDING) - self.achall = achallenges.ProofOfPossession( - challb=challb, domain="example.com") - - def tearDown(self): - os.remove(self.cert1_path) - - def test_perform_bad_challenge(self): - hints = challenges.ProofOfPossession.Hints( - jwk=jose.jwk.JWKOct(key="foo"), cert_fingerprints=(), - certs=(), serial_numbers=(), subject_key_identifiers=(), - issuers=(), authorized_for=()) - chall = challenges.ProofOfPossession( - alg=jose.HS512, nonce='zczv4HMLVe_0kimJ25Juig', hints=hints) - challb = messages.ChallengeBody( - chall=chall, uri="http://example", status=messages.STATUS_PENDING) - self.achall = achallenges.ProofOfPossession( - challb=challb, domain="example.com") - self.assertEqual(self.proof_of_pos.perform(self.achall), None) - - def test_perform_no_input(self): - self.assertTrue(self.proof_of_pos.perform(self.achall).verify()) - - @mock.patch("letsencrypt.proof_of_possession.zope.component.getUtility") - def test_perform_with_input(self, mock_input): - # Remove the matching certificate - self.installer.get_all_certs_keys.return_value.pop() - mock_input().input.side_effect = [(display_util.CANCEL, ""), - (display_util.OK, CERT0_PATH), - (display_util.OK, "imaginary_file"), - (display_util.OK, CERT3_KEY_PATH)] - self.assertFalse(self.proof_of_pos.perform(self.achall)) - self.assertFalse(self.proof_of_pos.perform(self.achall)) - self.assertFalse(self.proof_of_pos.perform(self.achall)) - self.assertTrue(self.proof_of_pos.perform(self.achall).verify()) - - -if __name__ == "__main__": - unittest.main() # pragma: no cover diff --git a/letsencrypt/tests/reporter_test.py b/letsencrypt/tests/reporter_test.py index c848b1cab..26a1105c8 100644 --- a/letsencrypt/tests/reporter_test.py +++ b/letsencrypt/tests/reporter_test.py @@ -1,8 +1,9 @@ """Tests for letsencrypt.reporter.""" -import StringIO import sys import unittest +import six + class ReporterTest(unittest.TestCase): """Tests for letsencrypt.reporter.Reporter.""" @@ -12,7 +13,7 @@ class ReporterTest(unittest.TestCase): self.reporter = reporter.Reporter() self.old_stdout = sys.stdout - sys.stdout = StringIO.StringIO() + sys.stdout = six.StringIO() def tearDown(self): sys.stdout = self.old_stdout diff --git a/letsencrypt/tests/testdata/sample-renewal-ancient.conf b/letsencrypt/tests/testdata/sample-renewal-ancient.conf old mode 100755 new mode 100644 diff --git a/letsencrypt/tests/testdata/sample-renewal.conf b/letsencrypt/tests/testdata/sample-renewal.conf old mode 100755 new mode 100644 index 16778303a..d6ebbd845 --- a/letsencrypt/tests/testdata/sample-renewal.conf +++ b/letsencrypt/tests/testdata/sample-renewal.conf @@ -2,7 +2,7 @@ cert = MAGICDIR/live/sample-renewal/cert.pem privkey = MAGICDIR/live/sample-renewal/privkey.pem chain = MAGICDIR/live/sample-renewal/chain.pem fullchain = MAGICDIR/live/sample-renewal/fullchain.pem -renew_before_expiry = 1 year +renew_before_expiry = 4 years # Options and defaults used in the renewal process [renewalparams] diff --git a/letshelp-letsencrypt/letshelp_letsencrypt/apache.py b/letshelp-letsencrypt/letshelp_letsencrypt/apache.py index ac4e9b831..d7cb05b70 100755 --- a/letshelp-letsencrypt/letshelp_letsencrypt/apache.py +++ b/letshelp-letsencrypt/letshelp_letsencrypt/apache.py @@ -1,5 +1,8 @@ #!/usr/bin/env python """Let's Encrypt Apache configuration submission script""" + +from __future__ import print_function + import argparse import atexit import contextlib @@ -48,20 +51,20 @@ def make_and_verify_selection(server_root, temp_dir): """ copied_files, copied_dirs = copy_config(server_root, temp_dir) - print textwrap.fill("A secure copy of the files that have been selected " + print(textwrap.fill("A secure copy of the files that have been selected " "for submission has been created under {0}. All " "comments have been removed and the files are only " "accessible by the current user. A list of the files " "that have been included is shown below. Please make " "sure that this selection does not contain private " "keys, passwords, or any other sensitive " - "information.".format(temp_dir)) - print "\nFiles:" + "information.".format(temp_dir))) + print("\nFiles:") for copied_file in copied_files: - print copied_file - print "Directories (including all contained files):" + print(copied_file) + print("Directories (including all contained files):") for copied_dir in copied_dirs: - print copied_dir + print(copied_dir) sys.stdout.write("\nIs it safe to submit these files? ") while True: diff --git a/setup.py b/setup.py index cbf0ff89d..b187e6fdb 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,9 @@ install_requires = [ 'pyrfc3339', 'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280 'pytz', - 'setuptools', # pkg_resources + # For pkg_resources. >=1.0 so pip resolves it to a version cryptography + # will tolerate; see #2599: + 'setuptools>=1.0', 'six', 'zope.component', 'zope.interface', diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 32c292e90..77e866b52 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -68,7 +68,7 @@ common renew CheckCertCount 2 # This will renew because the expiry is less than 10 years from now -sed -i "4arenew_before_expiry = 10 years" "$root/conf/renewal/le.wtf.conf" +sed -i "4arenew_before_expiry = 4 years" "$root/conf/renewal/le.wtf.conf" common_no_force_renew renew --rsa-key-size 2048 CheckCertCount 3 diff --git a/tools/eff-pubkey.pem b/tools/eff-pubkey.pem new file mode 100644 index 000000000..fe6c2f5bb --- /dev/null +++ b/tools/eff-pubkey.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6MR8W/galdxnpGqBsYbq +OzQb2eyW15YFjDDEMI0ZOzt8f504obNs920lDnpPD2/KqgsfjOgw2K7xWDJIj/18 +xUvWPk3LDkrnokNiRkA3KOx3W6fHycKL+zID7zy+xZYBuh2fLyQtWV1VGQ45iNRp +9+Zo7rH86cdfgkdnWTlNSHyTLW9NbXvyv/E12bppPcEvgCTAQXgnDVJ0/sqmeiij +n9tTFh03aM+R2V/21h8aTraAS24qiPCz6gkmYGC8yr6mglcnNoYbsLNYZ69zF1XH +cXPduCPdPdfLlzVlKK1/U7hkA28eG3BIAMh6uJYBRJTpiGgaGdPd7YekUB8S6cy+ +CQIDAQAB +-----END PUBLIC KEY----- diff --git a/tools/release.sh b/tools/release.sh index 02e3d00b8..7e67d4e4c 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -161,6 +161,22 @@ for module in letsencrypt $subpkgs_modules ; do done deactivate +# pin pip hashes of the things we just built +for pkg in acme letsencrypt letsencrypt-apache ; do + echo $pkg==$version \\ + pip hash dist."$version/$pkg"/*.{whl,gz} | grep "^--hash" | python2 -c 'from sys import stdin; input = stdin.read(); print " ", input.replace("\n--hash", " \\\n --hash"),' +done > /tmp/hashes.$$ + +if ! wc -l /tmp/hashes.$$ | grep -qE "^\s*9 " ; then + echo Unexpected pip hash output + exit 1 +fi + +# perform hideous surgery on requirements.txt... +head -n -9 letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt > /tmp/req.$$ +cat /tmp/hashes.$$ >> /tmp/req.$$ +cp /tmp/req.$$ letsencrypt-auto-source/pieces/letsencrypt-auto-requirements.txt + # ensure we have the latest built version of leauto letsencrypt-auto-source/build.py @@ -199,6 +215,8 @@ echo twine upload "$root/dist.$version/*/*" if [ "$RELEASE_BRANCH" = candidate-"$version" ] ; then SetVersion "$nextversion".dev0 + letsencrypt-auto-source/build.py + git add letsencrypt-auto-source/letsencrypt-auto git diff git commit -m "Bump version to $nextversion" fi diff --git a/tox.ini b/tox.ini index 57359cd86..6af9610e3 100644 --- a/tox.ini +++ b/tox.ini @@ -91,4 +91,4 @@ commands = docker run --rm -t -i lea whitelist_externals = docker -passenv = DOCKER_* \ No newline at end of file +passenv = DOCKER_*