diff --git a/Makefile b/Makefile index bb05749..9dfa649 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,11 @@ SHELL=/bin/sh TESTDIR=./gnupg/test TESTHANDLE=$(TESTDIR)/test_gnupg.py FILES=$(SHELL find ./gnupg/ -name "*.py" -printf "%p,") +PKG_NAME=python-gnupg +DOC_DIR=docs +DOC_BUILD_DIR:=$(DOC_DIR)/_build +DOC_HTML_DIR:=$(DOC_BUILD_DIR)/html +DOC_BUILD_ZIP:=$(PKG_NAME)-docs.zip .PHONY=all all: uninstall install test @@ -73,11 +78,15 @@ py3k-uninstall: uninstall reinstall: uninstall install py3k-reinstall: py3k-uninstall py3k-install -cleandocs: - sphinx-apidoc -F -A "Isis Agora Lovecruft" -H "python-gnupg" \ - -o docs gnupg/ tests/ +docs-clean: + -rm -rf $(DOC_BUILD_DIR) -docs: - cd docs && \ - make clean && \ - make html +docs-completely-new: + sphinx-apidoc -F -A "Isis Agora Lovecruft" -H "python-gnupg" -o $(DOC_DIR) gnupg/ tests/ + +docs-html: + cd $(DOC_DIR) && make clean && make html + +docs-zipfile: docs-html + cd $(DOC_HTML_DIR) && { find . -name '*' | zip -@ -v ../$(DOC_BUILD_ZIP) ;}; + @echo "Built documentation in $(DOC_BUILD_DIR)/$(DOC_BUILD_ZIP)" diff --git a/docs/conf.py b/docs/conf.py index 049ed5a..248f450 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,6 +12,7 @@ # serve to show the default. import sys, os +import psutil # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the diff --git a/gnupg/_meta.py b/gnupg/_meta.py index 57a0dfb..5ab7a28 100644 --- a/gnupg/_meta.py +++ b/gnupg/_meta.py @@ -75,7 +75,7 @@ class GPGMeta(type): same effective user ID as that of this program. Otherwise, returns None. """ - identity = os.getresuid() + identity = psutil.Process(os.getpid()).uids for proc in psutil.process_iter(): if (proc.name == "gpg-agent") and proc.is_running: log.debug("Found gpg-agent process with pid %d" % proc.pid) @@ -321,11 +321,11 @@ class GPGBase(object): created. Lastly, the ``direcory`` will be checked that the EUID has read and write permissions for it. - :param str homedir: A relative or absolute path to the directory to use - for storing/accessing GnuPG's files, including + :param str directory: A relative or absolute path to the directory to + use for storing/accessing GnuPG's files, including keyrings and the trustdb. :raises: :exc:`RuntimeError` if unable to find a suitable directory to - use. + use. """ if not directory: log.debug("GPGBase._homedir_setter(): Using default homedir: '%s'" diff --git a/gnupg/_parsers.py b/gnupg/_parsers.py index e409afc..1c99c0d 100644 --- a/gnupg/_parsers.py +++ b/gnupg/_parsers.py @@ -298,7 +298,7 @@ def _sanitise(*args): values = value.split(' ') for v in values: ## these can be handled separately, without _fix_unsafe(), - ## because they are only allowed if the pass the regex + ## because they are only allowed if they pass the regex if (flag in none_options) and (v is None): continue @@ -332,8 +332,11 @@ def _sanitise(*args): if flag in ['--encrypt', '--encrypt-files', '--decrypt', '--decrypt-files', '--import', '--verify']: - if _util._is_file(val): checked += (val + " ") - else: log.debug("%s not file: %s" % (flag, val)) + if _util._is_file(val) or \ + (flag == '--verify' and val == '-'): + checked += (val + " ") + else: + log.debug("%s not file: %s" % (flag, val)) elif flag in ['--cipher-algo', '--personal-cipher-prefs', '--personal-cipher-preferences']: @@ -372,7 +375,8 @@ def _sanitise(*args): groups[last] = str(filo.pop()) ## accept the read-from-stdin arg: if len(filo) >= 1 and filo[len(filo)-1] == '-': - groups[last] += str(' - \'\'') ## gross hack + groups[last] += str(' - ') ## gross hack + filo.pop() else: groups[last] = str() while len(filo) > 1 and not is_flag(filo[len(filo)-1]): diff --git a/gnupg/_util.py b/gnupg/_util.py index dd80632..f5b7ff3 100644 --- a/gnupg/_util.py +++ b/gnupg/_util.py @@ -31,6 +31,7 @@ from time import mktime import codecs import encodings import os +import psutil import threading import random import re @@ -270,6 +271,8 @@ def _find_binary(binary=None): except IndexError as ie: log.info("Could not determine absolute path of binary: '%s'" % binary) + elif os.access(binary, os.X_OK): + found = binary if found is None: try: found = _which('gpg')[0] except IndexError as ie: @@ -393,7 +396,7 @@ def _make_passphrase(length=None, save=False, file=None): passphrase = _make_random_string(length) if save: - ruid, euid, suid = os.getresuid() + ruid, euid, suid = psutil.Process(os.getpid()).uids gid = os.getgid() now = mktime(localtime()) @@ -529,39 +532,40 @@ def _write_passphrase(stream, passphrase, encoding): class InheritableProperty(object): - """Based on the emulation of PyProperty_Type() in Objects/descrobject.c""" + """Based on the emulation of PyProperty_Type() in Objects/descrobject.c""" - def __init__(self, fget=None, fset=None, fdel=None, doc=None): - self.fget = fget - self.fset = fset - self.fdel = fdel - self.__doc__ = doc + def __init__(self, fget=None, fset=None, fdel=None, doc=None): + self.fget = fget + self.fset = fset + self.fdel = fdel + self.__doc__ = doc - def __get__(self, obj, objtype=None): - if obj is None: - return self - if self.fget is None: - raise AttributeError("unreadable attribute") - if self.fget.__name__ == '' or not self.fget.__name__: - return self.fget(obj) - else: - return getattr(obj, self.fget.__name__)() + def __get__(self, obj, objtype=None): + if obj is None: + return self + if self.fget is None: + raise AttributeError("unreadable attribute") + if self.fget.__name__ == '' or not self.fget.__name__: + return self.fget(obj) + else: + return getattr(obj, self.fget.__name__)() - def __set__(self, obj, value): - if self.fset is None: - raise AttributeError("can't set attribute") - if self.fset.__name__ == '' or not self.fset.__name__: - self.fset(obj, value) - else: - getattr(obj, self.fset.__name__)(value) + def __set__(self, obj, value): + if self.fset is None: + raise AttributeError("can't set attribute") + if self.fset.__name__ == '' or not self.fset.__name__: + self.fset(obj, value) + else: + getattr(obj, self.fset.__name__)(value) + + def __delete__(self, obj): + if self.fdel is None: + raise AttributeError("can't delete attribute") + if self.fdel.__name__ == '' or not self.fdel.__name__: + self.fdel(obj) + else: + getattr(obj, self.fdel.__name__)() - def __delete__(self, obj): - if self.fdel is None: - raise AttributeError("can't delete attribute") - if self.fdel.__name__ == '' or not self.fdel.__name__: - self.fdel(obj) - else: - getattr(obj, self.fdel.__name__)() class Storage(dict): """A dictionary where keys are stored as class attributes. diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 0605f68..7d6fc30 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -308,7 +308,7 @@ class GPG(GPGBase): sig_fh = None try: sig_fh = open(sig_file) - args = ["--verify %s - " % sig_fh.name] + args = ["--verify %s -" % sig_fh.name] proc = self._open_subprocess(args) writer = _util._threaded_copy_data(file, proc.stdin) self._collect_output(proc, result, stdin=proc.stdin) @@ -557,18 +557,20 @@ class GPG(GPGBase): fpr = str(key.fingerprint) if len(fpr) == 20: - if self.temp_keyring or self.temp_secring: - if not os.path.exists(self._keys_dir): - os.makedirs(self._keys_dir) - prefix = os.path.join(self._keys_dir, fpr) + for d in map(lambda x: os.path.dirname(x), + [self.temp_keyring, self.temp_secring]): + if not os.path.exists(d): + os.makedirs(d) if self.temp_keyring: if os.path.isfile(self.temp_keyring): + prefix = os.path.join(self.temp_keyring, fpr) try: os.rename(self.temp_keyring, prefix+".pubring") except OSError as ose: log.error(ose.message) if self.temp_secring: if os.path.isfile(self.temp_secring): + prefix = os.path.join(self.temp_secring, fpr) try: os.rename(self.temp_secring, prefix+".secring") except OSError as ose: log.error(ose.message) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index 6bf2c8a..e223dbc 100644 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -173,7 +173,6 @@ class GPGTestCase(unittest.TestCase): self.keyring = self.gpg.keyring self.secring = self.gpg.secring self.insecure_prng = False - self.gpg._keys_dir = os.path.join(_files, 'generated-keys') def tearDown(self): """This is called once per self.test_* method after the test run.""" @@ -523,7 +522,7 @@ class GPGTestCase(unittest.TestCase): self.assertIsNotNone(key) self.assertNotEquals(key, "") self.assertGreater(len(str(key)), 0) - keyfile = os.path.join(self.gpg._keys_dir, 'test_key_3.pub') + keyfile = os.path.join(_files, 'test_key_3.pub') log.debug("Storing downloaded key as %s" % keyfile) with open(keyfile, 'w') as fh: fh.write(str(key)) diff --git a/requirements.txt b/requirements.txt index fc171a9..2b610ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,58 +1,3 @@ -# -# python-gnupg/requirements.txt -# ----------------------------- -# Pip requirements.txt file. This file is also parsed for distribute to use in -# setup.py. -#_____________________________________________________________________________ -# This file is part of python-gnupg, a Python interface to GnuPG. -# Copyright © 2013 Isis Lovecruft, 0xA3ADB67A2CDB8B35 -# © 2013 Andrej B. -# © 2013 LEAP Encryption Access Project -# © 2008-2012 Vinay Sajip -# © 2005 Steve Traugott -# © 2004 A.M. Kuchling -# -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the included LICENSE file for details. -#______________________________________________________________________________ -# -# Force pip upgrade due to security vulnerabilities. -# -# This has actually has little to do with installing python-gnupg, since -# older versions of pip would install everything just fine. except that, in -# my opinion, using GnuPG for privacy is silly when the installation of -# python-gnupg with an older version of pip is trivially exploitable through -# a MITM attack. see https://github.com/pypa/pip/pull/791 -# -# Also, note that SSL package delivery is *not* entirely fixed yet. See -# https://github.com/TheTorProject/ooni-backend/pull/1#discussion_r4084881 -# -#pip>=1.3.1 -# -# NOTE: setuptools is currently (as of 27 May 2013) being merged back into its -# parent project, distribute. By using the included distribute_setup.py -# script, we make sure that we have a recent version of setuptools/distribute, -# which is the *only* Python packaging framework compatible at this point with -# both Python>=2.4 and Python3.x. -# -# A new version of distribute is necessary due to the merging of setuptools -# back into its parent project, distribute. Also, the only way to package for -# both Python 2 and 3 is to use distribute. -# -#distribute>=0.6.45 -# -# Sphinx is only necessary for building documentation, so it is added in -# setup.py under extras_require['docs']. -# -# If you want to build the documentation, uncomment this line: -#Sphinx>=1.1 -# -# And, this one is actually used in the gnupg module code: -# -psutil>=0.5.1 +# sha256: UI5KRMglOjhqD4bZyb1KG0y7L5TojUmhnBUTZTymbEU +psutil==1.2.1 + diff --git a/setup.cfg b/setup.cfg index a906e65..fb18565 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,7 @@ [upload_docs] upload-dir = docs/_build/html +show-response = true +verbose = true [upload] sign = True diff --git a/setup.py b/setup.py index 5def919..3b4154d 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,10 @@ from __future__ import absolute_import from __future__ import print_function import setuptools +import os import versioneer + + versioneer.versionfile_source = 'gnupg/_version.py' versioneer.versionfile_build = 'gnupg/_version.py' versioneer.tag_prefix = '' @@ -34,6 +37,44 @@ __contact__ = 'isis@patternsinthevoid.net' __url__ = 'https://github.com/isislovecruft/python-gnupg' +def get_requirements(): + """Extract the list of requirements from our requirements.txt. + + :rtype: 2-tuple + :returns: Two lists, the first is a list of requirements in the form of + pkgname==version. The second is a list of URIs or VCS checkout strings + which specify the dependency links for obtaining a copy of the + requirement. + """ + requirements_file = os.path.join(os.getcwd(), 'requirements.txt') + requirements = [] + links=[] + try: + with open(requirements_file) as reqfile: + for line in reqfile.readlines(): + line = line.strip() + if line.startswith('#'): + continue + elif line.startswith( + ('https://', 'git://', 'hg://', 'svn://')): + links.append(line) + else: + requirements.append(line) + + except (IOError, OSError) as error: + print(error) + + return requirements, links + + +requires, deplinks = get_requirements() +print('Found requirements:') +[print('\t%s' % name) for name in requires] + +print('Found dependency links:') +[print('\t%s' % uri) for uri in deplinks] + + setuptools.setup( name = "gnupg", description="A Python wrapper for GnuPG", @@ -62,7 +103,8 @@ greater. scripts=['versioneer.py'], test_suite='gnupg.test.test_gnupg', - install_requires=['psutil>=0.5.1'], + install_requires=requires, + dependency_links=deplinks, extras_require={'docs': ["Sphinx>=1.1", "repoze.sphinx"]}, platforms="Linux, BSD, OSX, Windows",