diff --git a/README.rst b/README.rst index 3be86af..d00025e 100644 --- a/README.rst +++ b/README.rst @@ -270,6 +270,10 @@ Running ``pypi-server -h`` will print a detailed usage message:: -o, --overwrite allow overwriting existing package files + --hash-algo ALGO + any `hashlib` available algo used as fragments on package links. + Set one of (0, no, off, false) to disabled it. (default: md5) + --welcome HTML_FILE uses the ASCII contents of HTML_FILE as welcome message response. diff --git a/pypiserver/__init__.py b/pypiserver/__init__.py index 30c7d9c..1588556 100644 --- a/pypiserver/__init__.py +++ b/pypiserver/__init__.py @@ -43,6 +43,7 @@ def default_config(): authenticated = ['update'], password_file = None, overwrite = False, + hash_algo = 'md5', verbosity = 1, log_file = None, log_frmt = "%(asctime)s|%(levelname)s|%(thread)d|%(message)s", @@ -70,15 +71,15 @@ def app(**kwds): return _app.app +def str2bool(s, default): + if s is not None and s != '': + return s.lower() not in ("no", "off", "0", "false") + return default def paste_app_factory(global_config, **local_conf): import os def upd_bool_attr_from_dict_str_item(conf, attr, sdict): - def str2bool(s, default): - if s is not None and s != '': - return s.lower() not in ("no", "off", "0", "false") - return default setattr(conf, attr, str2bool(sdict.pop(attr, None), getattr(conf, attr))) def _make_root(root): diff --git a/pypiserver/__main__.py b/pypiserver/__main__.py index cf71a8e..3e75708 100644 --- a/pypiserver/__main__.py +++ b/pypiserver/__main__.py @@ -83,6 +83,10 @@ def usage(): -o, --overwrite allow overwriting existing package files + --hash-algo ALGO + any `hashlib` available algo used as fragments on package links. + Set one of (0, no, off, false) to disabled it. (default: md5) + --welcome HTML_FILE uses the ASCII contents of HTML_FILE as welcome message response. @@ -167,6 +171,7 @@ def main(argv=None): "fallback-url=", "disable-fallback", "overwrite", + "hash-algo=", "log-file=", "log-frmt=", "log-req-frmt=", @@ -226,6 +231,8 @@ def main(argv=None): c.password_file = v elif k in ("-o", "--overwrite"): c.overwrite = True + elif k in ("--hash-algo"): + c.hash_algo = None if not pypiserver.str2bool(v, c.hash_algo) else v elif k == "--log-file": c.log_file = v elif k == "--log-frmt": diff --git a/pypiserver/_app.py b/pypiserver/_app.py index f28cd5b..29c61ba 100644 --- a/pypiserver/_app.py +++ b/pypiserver/_app.py @@ -192,7 +192,9 @@ def simple(prefix=""): return HTTPError(404) links = [(os.path.basename(f.relfn), - urljoin(fp, "../../packages/%s#%s" % (f.relfn_unix(), f.hash()))) + urljoin(fp, "../../packages/%s#%s" % (f.relfn_unix(), + + f.hash(config.hash_algo)))) for f in files] tmpl = """\ @@ -222,7 +224,8 @@ def list_packages(): key=lambda x: (os.path.dirname(x.relfn), x.pkgname, x.parsed_version)) - links = [(f.relfn_unix(), '%s#%s' % (urljoin(fp, f.relfn), f.hash())) + links = [(f.relfn_unix(), '%s#%s' % (urljoin(fp, f.relfn), + f.hash(config.hash_algo))) for f in files] tmpl = """\ diff --git a/pypiserver/core.py b/pypiserver/core.py index 4a0adeb..68acd96 100644 --- a/pypiserver/core.py +++ b/pypiserver/core.py @@ -22,6 +22,7 @@ def configure(root=None, authenticated=None, password_file=None, overwrite=False, + hash_algo='md5', log_file=None, log_frmt=None, log_req_frmt=None, @@ -73,29 +74,15 @@ def configure(root=None, :return: a 2-tuple (Configure, package-list) """ - log.info("+++Pypiserver invoked with: %s", Configuration( - root=root, - redirect_to_fallback=redirect_to_fallback, - fallback_url=fallback_url, - authenticated=authenticated, - password_file=password_file, - overwrite=overwrite, - welcome_file=welcome_file, - log_file=log_file, - log_frmt=log_frmt, - log_req_frmt=log_req_frmt, - log_res_frmt=log_res_frmt, - log_err_frmt=log_err_frmt, - cache_control=cache_control, - auther=auther, - host=host, port=port, server=server, - verbosity=verbosity, VERSION=VERSION - )) + return _configure(**locals()) +def _configure(**kwds): + c = Configuration(**kwds) + log.info("+++Pypiserver invoked with: %s", c) - if root is None: - root = os.path.expanduser("~/packages") - roots = root if isinstance(root, (list, tuple)) else [root] + if c.root is None: + c. root = os.path.expanduser("~/packages") + roots = c.root if isinstance(c.root, (list, tuple)) else [c.root] roots = [os.path.abspath(r) for r in roots] for r in roots: try: @@ -107,56 +94,46 @@ def configure(root=None, packages = lambda: itertools.chain(*[listdir(r) for r in roots]) packages.root = roots[0] - authenticated = authenticated or [] - if not callable(auther): - if password_file and password_file != '.': + if not c.authenticated: + c.authenticated = [] + if not callable(c.auther): + if c.password_file and c.password_file != '.': from passlib.apache import HtpasswdFile - htPsswdFile = HtpasswdFile(password_file) + htPsswdFile = HtpasswdFile(c.password_file) else: - password_file = htPsswdFile = None - auther = functools.partial(auth_by_htpasswd_file, htPsswdFile) + c.password_file = htPsswdFile = None + c.auther = functools.partial(auth_by_htpasswd_file, htPsswdFile) # Read welcome-msg from external file, # or failback to the embedded-msg (ie. in standalone mode). # try: - if not welcome_file: - welcome_file = "welcome.html" - welcome_msg = pkg_resources.resource_string( # @UndefinedVariable + if not c.welcome_file: + c.welcome_file = "welcome.html" + c.welcome_msg = pkg_resources.resource_string( # @UndefinedVariable __name__, "welcome.html").decode("utf-8") # @UndefinedVariable else: - welcome_file = welcome_file - with io.open(welcome_file, 'r', encoding='utf-8') as fd: - welcome_msg = fd.read() + with io.open(c.welcome_file, 'r', encoding='utf-8') as fd: + c.welcome_msg = fd.read() except Exception: log.warning( - "Could not load welcome-file(%s)!", welcome_file, exc_info=1) + "Could not load welcome-file(%s)!", c.welcome_file, exc_info=1) - if fallback_url is None: - fallback_url = "http://pypi.python.org/simple" + if c.fallback_url is None: + c.fallback_url = "http://pypi.python.org/simple" - log_req_frmt = log_req_frmt - log_res_frmt = log_res_frmt - log_err_frmt = log_err_frmt + if c.hash_algo: + try: + halgos = hashlib.algorithms_available + except AttributeError: + halgos = ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'] - config = Configuration( - root=root, - redirect_to_fallback=redirect_to_fallback, - fallback_url=fallback_url, - authenticated=authenticated, - password_file=password_file, - overwrite=overwrite, - welcome_file=welcome_file, - welcome_msg=welcome_msg, - log_req_frmt=log_req_frmt, - log_res_frmt=log_res_frmt, - log_err_frmt=log_err_frmt, - cache_control=cache_control, - auther=auther - ) - log.info("+++Pypiserver started with: %s", config) + if c.hash_algo not in halgos: + sys.exit('Hash-algorithm %s not one of: %s' % (c.hash_algo, halgos)) - return config, packages + log.info("+++Pypiserver started with: %s", c) + + return c, packages def auth_by_htpasswd_file(htPsswdFile, username, password): @@ -268,7 +245,7 @@ class PkgFile(object): def relfn_unix(self): return self.relfn.replace("\\", "/") - def hash(self, hash_algo='md5'): + def hash(self, hash_algo): return '%s=%.32s' % (hash_algo, digest_file(self.fn, hash_algo)) diff --git a/tests/test_main.py b/tests/test_main.py index 34baa74..2c4896c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -89,11 +89,34 @@ def test_fallback_url_default(main): assert main.app.module.config.fallback_url == \ "http://pypi.python.org/simple" -@pytest.fixture -def logfile(tmpdir): - return tmpdir.mkdir("logs").join('test.log') -def test_logging(main, logfile): +def test_hash_algo_default(main): + main([]) + assert main.app.module.config.hash_algo == 'md5' + +def test_hash_algo(main): + main(['--hash-algo=sha256']) + assert main.app.module.config.hash_algo == 'sha256' + +def test_hash_algo_off(main): + main(['--hash-algo=off']) + assert main.app.module.config.hash_algo is None + main(['--hash-algo=0']) + assert main.app.module.config.hash_algo is None + main(['--hash-algo=no']) + assert main.app.module.config.hash_algo is None + main(['--hash-algo=false']) + assert main.app.module.config.hash_algo is None + +def test_hash_algo_BAD(main): + with pytest.raises(SystemExit) as excinfo: + main(['--hash-algo BAD']) + #assert excinfo.value.message == 'some info' main(['--hash-algo BAD']) + print(excinfo) + + +def test_logging(main, tmpdir): + logfile = tmpdir.mkdir("logs").join('test.log') main(["-v", "--log-file", logfile.strpath]) assert logfile.check(), logfile @@ -122,14 +145,14 @@ def test_password_without_auth_list(main, monkeypatch): with pytest.raises(ValueError) as ex: main(["-P", "pswd-file", "-a", ""]) assert ex.value.args[0] == 'BINGO' - + with pytest.raises(ValueError) as ex: main(["-a", "."]) assert ex.value.args[0] == 'BINGO' with pytest.raises(ValueError) as ex: main(["-a", ""]) assert ex.value.args[0] == 'BINGO' - + with pytest.raises(ValueError) as ex: main(["-P", "."]) assert ex.value.args[0] == 'BINGO' @@ -143,6 +166,6 @@ def test_password_alone(main, monkeypatch): def test_dot_password_without_auth_list(main, monkeypatch): main(["-P", ".", "-a", ""]) assert main.app.module.config.authenticated == [] - + main(["-P", ".", "-a", "."]) assert main.app.module.config.authenticated == []