#!/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.7 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 from subprocess import check_output from sys import exit 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 PACKAGES = [ # Pip has no dependencies, as it vendors everything: ('https://pypi.python.org/packages/source/p/pip/pip-8.0.2.tar.gz', '46f4bd0d8dfd51125a554568d646fe4200a3c2c6c36b9f2d06d2212148439521'), # This version of setuptools has only optional dependencies: ('https://pypi.python.org/packages/source/s/setuptools/' 'setuptools-19.4.tar.gz', '214bf29933f47cf25e6faa569f710731728a07a19cae91ea64f826051f68a8cf'), # We require Python 2.7 or later because we don't support wheel's # conditional dep on argparse. This version of wheel has no other # dependencies: ('https://pypi.python.org/packages/source/w/wheel/wheel-0.26.0.tar.gz', 'eaad353805c180a47545a256e6508835b65a8e830ba1093ed8162f19a50a530c') ] 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 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())