diff --git a/Makefile b/Makefile index 77dd310..7c078b4 100644 --- a/Makefile +++ b/Makefile @@ -5,13 +5,13 @@ clean: rm -f ./*.pyo cleantest: clean - mkdir -p gnupg/tests/keys + mkdir -p gnupg/tests/tmp_test touch gnupg/tests/placeholder.log - rm -rf gnupg/tests/keys + rm -rf gnupg/tests/tmp_test rm gnupg/tests/*.log test: cleantest - python gnupg/tests/test_gnupg.py parsers basic genkey sign + python gnupg/tests/test_gnupg.py parsers basic genkey sign listkeys install: python setup.py install --record installed-files.txt diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 62b5747..a8391df 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -107,78 +107,7 @@ from parsers import GenKey, Sign, ListKeys, ListPackets from parsers import _fix_unsafe, _sanitise, _is_allowed, _sanitise_list from util import logger, _conf -import util - - -def _copy_data(instream, outstream): - """Copy data from one stream to another. - - :type instream: :class:`io.BytesIO` or :class:`io.StringIO` or file - :param instream: A byte stream or open file to read from. - :param file outstream: The file descriptor of a tmpfile to write to. - """ - sent = 0 - - try: - #assert (util._is_stream(instream) - # or isinstance(instream, file)), "instream not stream or file" - assert isinstance(outstream, file), "outstream is not a file" - except AssertionError as ae: - logger.exception(ae) - return - - if hasattr(sys.stdin, 'encoding'): - enc = sys.stdin.encoding - else: - enc = 'ascii' - - while True: - data = instream.read(1024) - if len(data) == 0: - break - sent += len(data) - logger.debug("_copy_data(): sending chunk (%d):\n%s" % (sent, data[:256])) - try: - outstream.write(data) - except UnicodeError: - try: - outstream.write(data.encode(enc)) - except IOError: - logger.exception('_copy_data(): Error sending data: Broken pipe') - break - except IOError: - # Can get 'broken pipe' errors even when all data was sent - logger.exception('_copy_data(): Error sending data: Broken pipe') - break - try: - outstream.close() - except IOError: - logger.exception('_copy_data(): Got IOError while closing %s' % outstream) - else: - logger.debug("_copy_data(): Closed output, %d bytes sent." % sent) - -def _threaded_copy_data(instream, outstream): - """Copy data from one stream to another in a separate thread. - - Wraps ``_copy_data()`` in a :class:`threading.Thread`. - - :type instream: :class:`io.BytesIO` or :class:`io.StringIO` - :param instream: A byte stream to read from. - :param file outstream: The file descriptor of a tmpfile to write to. - """ - copy_thread = threading.Thread(target=_copy_data, - args=(instream, outstream)) - copy_thread.setDaemon(True) - logger.debug('_threaded_copy_data(): %r, %r, %r', copy_thread, - instream, outstream) - copy_thread.start() - return copy_thread - -def _write_passphrase(stream, passphrase, encoding): - passphrase = '%s\n' % passphrase - passphrase = passphrase.encode(encoding) - stream.write(passphrase) - logger.debug("_write_passphrase(): Wrote passphrase.") +import util as _util class GPG(object): @@ -227,12 +156,12 @@ class GPG(object): gpghome = _conf self.gpghome = _fix_unsafe(gpghome) if self.gpghome: - util._create_gpghome(self.gpghome) + _util._create_gpghome(self.gpghome) else: message = ("Unsuitable gpg home dir: %s" % gpghome) logger.debug("GPG.__init__(): %s" % message) - self.gpgbinary = util._find_gpgbinary(gpgbinary) + self.gpgbinary = _util._find_gpgbinary(gpgbinary) if keyring is not None: raise DeprecationWarning("Option 'keyring' changing to 'secring'") @@ -242,17 +171,6 @@ class GPG(object): self.secring = os.path.join(self.gpghome, secring) self.pubring = os.path.join(self.gpghome, pubring) - #for ring in [self.secring, self.pubring]: - # if ring and not os.path.isfile(ring): - # with open(ring, 'a+') as ringfile: - # ringfile.write("") - # ringfile.flush() - # try: - # assert util._has_readwrite(ring), \ - # ("Need r+w for %s" % ring) - # except AssertionError as ae: - # logger.debug(ae.message) - self.options = _sanitise(options) if options else None self.encoding = locale.getpreferredencoding() @@ -261,7 +179,7 @@ class GPG(object): try: assert self.gpghome is not None, "Got None for self.gpghome" - assert util._has_readwrite(self.gpghome), ("Home dir %s needs r+w" + assert _util._has_readwrite(self.gpghome), ("Home dir %s needs r+w" % self.gpghome) assert self.gpgbinary, "Could not find gpgbinary %s" % full assert isinstance(verbose, bool), "'verbose' must be boolean" @@ -354,7 +272,7 @@ class GPG(object): break logger.debug("chunk: %r" % data[:256]) chunks.append(data) - if util._py3k: + if _util._py3k: # Join using b'' or '', as appropriate result.data = type(data)().join(chunks) else: @@ -399,8 +317,8 @@ class GPG(object): else: stdin = p.stdin if passphrase: - _write_passphrase(stdin, passphrase, self.encoding) - writer = _threaded_copy_data(file, stdin) + _util._write_passphrase(stdin, passphrase, self.encoding) + writer = _util._threaded_copy_data(file, stdin) self._collect_output(p, result, writer, stdin) return result @@ -411,8 +329,8 @@ class GPG(object): """Create a signature for a message or file.""" if isinstance(message, file): result = self._sign_file(message, **kwargs) - elif not util._is_stream(message): - f = util._make_binary_stream(message, self.encoding) + elif not _util._is_stream(message): + f = _util._make_binary_stream(message, self.encoding) result = self._sign_file(f, **kwargs) f.close() else: @@ -448,8 +366,8 @@ class GPG(object): try: stdin = p.stdin if passphrase: - _write_passphrase(stdin, passphrase, self.encoding) - writer = _threaded_copy_data(file, stdin) + _util._write_passphrase(stdin, passphrase, self.encoding) + writer = _util._threaded_copy_data(file, stdin) except IOError: logger.exception("_sign_file(): Error writing message") writer = None @@ -471,7 +389,7 @@ class GPG(object): >>> assert verify """ - f = util._make_binary_stream(data, self.encoding) + f = _util._make_binary_stream(data, self.encoding) result = self.verify_file(f) f.close() return result @@ -483,7 +401,7 @@ class GPG(object): detached signature should be specified as the ``sig_file``. :param file file: A file descriptor object. Its type will be checked - with :func:`util._is_file`. + with :func:`_util._is_file`. :param str sig_file: A file containing the GPG signature data for ``file``. If given, ``file`` is verified via this detached signature. @@ -496,10 +414,10 @@ class GPG(object): logger.debug("verify_file(): Handling embedded signature") args = ["--verify"] proc = self._open_subprocess(args) - writer = _threaded_copy_data(file, proc.stdin) + writer = _util._threaded_copy_data(file, proc.stdin) self._collect_output(proc, result, writer, stdin=proc.stdin) else: - if not util._is_file(sig_file): + if not _util._is_file(sig_file): logger.debug("verify_file(): '%r' is not a file" % sig_file) return result logger.debug('verify_file(): Handling detached verification') @@ -508,7 +426,7 @@ class GPG(object): sig_fh = open(sig_file) args = ["--verify %s - " % sig_fh.name] proc = self._open_subprocess(args) - writer = _threaded_copy_data(file, proc.stdin) + writer = _util._threaded_copy_data(file, proc.stdin) self._collect_output(proc, result, stdin=proc.stdin) finally: if sig_fh and not sig_fh.closed: @@ -568,7 +486,7 @@ class GPG(object): result = self._result_map['import'](self) logger.debug('import_keys: %r', key_data[:256]) - data = util._make_binary_stream(key_data, self.encoding) + data = _util._make_binary_stream(key_data, self.encoding) self._handle_io(['--import'], data, result, binary=True) logger.debug('import_keys result: %r', result.__dict__) data.close() @@ -587,7 +505,7 @@ class GPG(object): safe_keyserver = _fix_unsafe(keyserver) result = self._result_map['import'](self) - data = util._make_binary_stream("", self.encoding) + data = _util._make_binary_stream("", self.encoding) args = ['--keyserver', keyserver, '--recv-keys'] if keyids: @@ -606,7 +524,7 @@ class GPG(object): which='key' if secret: which='secret-key' - if util._is_list_or_tuple(fingerprints): + if _util._is_list_or_tuple(fingerprints): fingerprints = ' '.join(fingerprints) args = ['--batch --delete-%s "%s"' % (which, fingerprints)] result = self._result_map['delete'](self) @@ -619,7 +537,7 @@ class GPG(object): which='' if secret: which='-secret-key' - if util._is_list_or_tuple(keyids): + if _util._is_list_or_tuple(keyids): keyids = ' '.join(['"%s"' % k for k in keyids]) args = ["--armor --export%s %s" % (which, keyids)] p = self._open_subprocess(args) @@ -650,7 +568,8 @@ class GPG(object): which='public-keys' if secret: which='secret-keys' - args = "--list-%s --fixed-list-mode --fingerprint --with-colons --no-show-photos" % (which,) + args = "--list-%s --fixed-list-mode --fingerprint " % (which,) + args += "--with-colons --list-options no-show-photos" args = [args] p = self._open_subprocess(args) @@ -689,9 +608,8 @@ class GPG(object): raise NotImplemented("Functionality for '--list-sigs' not implemented.") def gen_key(self, input): - """ - Generate a key; you might use gen_key_input() to create the control - input. + """Generate a GnuPG key through batch file key generation. See + :meth:`GPG.gen_key_input()` for creating the control input. >>> gpg = GPG(gpghome="keys") >>> input = gpg.gen_key_input() @@ -700,24 +618,77 @@ class GPG(object): >>> result = gpg.gen_key('foo') >>> assert not result + :param dict input: A dictionary of parameters and values for the new + key. + :returns: The result mapping with details of the new key, which is a + :class:`parsers.GenKey ` object. """ args = ["--gen-key --batch"] key = self._result_map['generate'](self) - f = util._make_binary_stream(input, self.encoding) + f = _util._make_binary_stream(input, self.encoding) self._handle_io(args, f, key, binary=True) f.close() return key def gen_key_input(self, **kwargs): - """Generate GnuPG key(s) through batch file key generation. + """Generate a batch file for input to :meth:`GPG.gen_key()`. The GnuPG batch file key generation feature allows unattended key generation by creating a file with special syntax and then providing it - to: gpg --gen-key --batch + to: ``gpg --gen-key --batch``: + + Key-Type: RSA + Key-Length: 4096 + Name-Real: Autogenerated Key + Name-Email: %s@%s + Expire-Date: 2014-04-01 + %pubring foo.gpg + %secring sec.gpg + %commit + + Key-Type: DSA + Key-Length: 1024 + Subkey-Type: ELG-E + Subkey-Length: 1024 + Name-Real: Joe Tester + Name-Comment: with stupid passphrase + Name-Email: joe@foo.bar + Expire-Date: 0 + Passphrase: abc + %pubring foo.pub + %secring foo.sec + %commit see http://www.gnupg.org/documentation/manuals/gnupg-devel/Unattended-GPG-key-generation.html#Unattended-GPG-key-generation for more details. + + >>> gpg = GPG(gpghome="keys") + >>> params = {'name_real':'python-gnupg tester', 'name_email':'test@ing'} + >>> key_input = gpg.gen_key_input(**params) + >>> result = gpg.gen_key(input) + >>> assert result + + :param str name_real: The uid name for the generated key. + :param str name_email: The uid email for the generated key. (default: + $USERNAME@$HOSTNAME) + :param str name_comment: The comment in the uid of the generated key. + :param str key_type: One of 'RSA', 'DSA', or 'ELG-E'. (default: 'RSA') + :param int key_length: The length in bytes of the new key. + (default: 4096) + :param str subkey_type: If ``key_type`` is 'RSA', an additional subkey + can be generated, and it's type must also be 'RSA'. If ``key_type`` + is 'DSA', then the only subkey type which can be generated is + 'ELG-E'. + :param int subkey_length: The length in bytes of the new subkey. + :type expire: int or str + :param expire: If an integer, the number of days before the key will + expire; if 0, the key will not expire. Otherwise, this can be given + as a string in the form w or m or y, i.e. "5m" would mean + that the key will expire in five months, "1w" would expire in one + week, and "3y" would expire in three years. (default: "1y") + :param str passphrase: The passphrase for the new key. """ + parms = {} for key, val in list(kwargs.items()): key = key.replace('_','-').title() @@ -726,7 +697,7 @@ class GPG(object): parms.setdefault('Key-Type', 'RSA') parms.setdefault('Key-Length', 4096) parms.setdefault('Name-Real', "Autogenerated Key") - parms.setdefault('Name-Comment', "Generated by python-gnupg") + parms.setdefault('Expire-Date', _util._next_year()) try: logname = os.environ['LOGNAME'] except KeyError: @@ -734,6 +705,7 @@ class GPG(object): hostname = socket.gethostname() parms.setdefault('Name-Email', "%s@%s" % (logname.replace(' ', '_'), hostname)) + out = "Key-Type: %s\n" % parms.pop('Key-Type') for key, val in list(parms.items()): out += "%s: %s\n" % (key, val) @@ -742,28 +714,6 @@ class GPG(object): out += "%commit\n" return out - # Key-Type: RSA - # Key-Length: 1024 - # Name-Real: ISdlink Server on %s - # Name-Comment: Created by %s - # Name-Email: isdlink@%s - # Expire-Date: 0 - # %commit - # - # - # Key-Type: DSA - # Key-Length: 1024 - # Subkey-Type: ELG-E - # Subkey-Length: 1024 - # Name-Real: Joe Tester - # Name-Comment: with stupid passphrase - # Name-Email: joe@foo.bar - # Expire-Date: 0 - # Passphrase: abc - # %pubring foo.pub - # %secring foo.sec - # %commit - # # ENCRYPTION # @@ -805,7 +755,7 @@ class GPG(object): args = ['--symmetric'] else: args = ['--encrypt'] - if not util._is_list_or_tuple(recipients): + if not _util._is_list_or_tuple(recipients): recipients = (recipients,) for recipient in recipients: args.append('--recipient "%s"' % recipient) @@ -861,7 +811,7 @@ class GPG(object): >>> assert result.fingerprint == print1 """ - data = util._make_binary_stream(data, self.encoding) + data = _util._make_binary_stream(data, self.encoding) result = self.encrypt_file(data, recipients, **kwargs) data.close() return result @@ -871,7 +821,7 @@ class GPG(object): :param message: A string or file-like object to decrypt. """ - data = util._make_binary_stream(message, self.encoding) + data = _util._make_binary_stream(message, self.encoding) result = self.decrypt_file(data, **kwargs) data.close() return result @@ -965,7 +915,7 @@ class GPGWrapper(GPG): """Send keys to a keyserver.""" result = self._result_map['list'](self) gnupg.logger.debug('send_keys: %r', keyids) - data = gnupg.util._make_binary_stream("", self.encoding) + data = gnupg._util._make_binary_stream("", self.encoding) args = ['--keyserver', keyserver, '--send-keys'] args.extend(keyids) self._handle_io(args, data, result, binary=True) @@ -985,7 +935,7 @@ class GPGWrapper(GPG): args.append('--cipher-algo %s' % cipher_algo) else: args = ['--encrypt'] - if not util._is_list_or_tuple(recipients): + if not _util._is_list_or_tuple(recipients): recipients = (recipients,) for recipient in recipients: args.append('--recipient "%s"' % recipient) @@ -1008,7 +958,7 @@ class GPGWrapper(GPG): args = ["--list-packets"] result = self._result_map['list-packets'](self) self._handle_io(args, - util._make_binary_stream(raw_data, self.encoding), + _util._make_binary_stream(raw_data, self.encoding), result) return result diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 8d434bb..a79ce70 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -275,7 +275,7 @@ def _is_allowed(input): ['--list-keys', '--list-key', '--fixed-list-mode', '--list-secret-keys', '--list-public-keys', '--list-packets', '--with-colons', - '--no-show-photos', + '--list-options', '--delete-keys', '--delete-secret-keys', '--encrypt', '--encrypt-files', '--decrypt', '--decrypt-files', diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 949ebc7..5388734 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -11,7 +11,6 @@ import argparse import doctest import logging from functools import wraps -import inspect import io import os import shutil @@ -30,24 +29,24 @@ from gnupg import parsers from gnupg import util __author__ = gnupg.__author__ -__date__ = gnupg.__date__ __version__ = gnupg.__version__ -REPO_DIR = os.getcwd() -HOME_DIR = os.path.join(REPO_DIR, 'keys') -tempfile.tempdir = os.path.join(REPO_DIR, 'tmp_test') +logger = logging.getLogger('gnupg') +_here = os.path.join(os.path.join(util._repo, 'gnupg'), 'tests') +_files = os.path.join(_here, 'files') + +tempfile.tempdir = os.path.join(_here, 'tmp_test') if not os.path.isdir(tempfile.gettempdir()): os.mkdir(tempfile.gettempdir()) +HOME_DIR = tempfile.tempdir + @wraps(tempfile.TemporaryFile) def _make_tempfile(*args, **kwargs): return tempfile.TemporaryFile(dir=tempfile.gettempdir(), *args, **kwargs) -logger = logging.getLogger('gnupg') -_here = os.path.join(os.path.join(util._repo, 'gnupg'), 'tests') -_files = os.path.join(_here, 'files') KEYS_TO_IMPORT = """-----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1.4.9 (MingW32) @@ -118,11 +117,10 @@ def compare_keys(k1, k2): class ResultStringIO(io.StringIO): - def __init__(self): - super(self, io.StringIO).__init__() - + def __init__(self, init_string): + super(ResultStringIO, self).__init__(init_string) def write(self, data): - super(self, io.StringIO).write(unicode(data)) + super(ResultStringIO, self).write(unicode(data)) class GPGTestCase(unittest.TestCase): @@ -216,7 +214,7 @@ class GPGTestCase(unittest.TestCase): cmd = self.gpg._make_args(None, False) expected = ['/usr/bin/gpg', '--status-fd 2 --no-tty', - '--homedir "%s"' % os.path.join(os.getcwd(), 'keys'), + '--homedir "%s"' % HOME_DIR, '--no-default-keyring --keyring %s' % self.pubring, '--secret-keyring %s' % self.secring] self.assertListEqual(cmd, expected) @@ -244,16 +242,19 @@ class GPGTestCase(unittest.TestCase): "Empty list expected...got instead: %s" % str(private_keys)) - - def test_copy_data(self): - """ - XXX implement me - XXX add me to a test suite - - Test that _copy_data() is able to duplicate byte streams. - """ - instream = io.BytesIO("This is a string of bytes mapped in memory.") - outstream = str("And this one is just a string.") + def test_copy_data_bytesio(self): + """Test that _copy_data() is able to duplicate byte streams.""" + message = "This is a BytesIO string string in memory." + instream = io.BytesIO(message) + self.assertEqual(unicode(message), instream.getvalue()) + outstream = ResultStringIO(u'result:') + copied = outstream + util._copy_data(instream, outstream) + self.assertTrue(outstream.readable()) + self.assertTrue(outstream.closed) + self.assertFalse(instream.closed) + self.assertTrue(copied.closed) + #self.assertEqual(instream.getvalue()[6:], outstream.getvalue()) def generate_key_input(self, real_name, email_domain, key_length=None, key_type=None, subkey_type=None, passphrase=None): @@ -386,9 +387,9 @@ class GPGTestCase(unittest.TestCase): def test_key_generation_with_empty_value(self): """Test that key generation handles empty values.""" - params = {'name_comment': ' '} + params = {'name_real': ' '} batch = self.gpg.gen_key_input(**params) - self.assertTrue('\nName-Comment: Generated by python-gnupg\n' in batch) + self.assertTrue('\nName-Real: Autogenerated Key\n' in batch) def test_key_generation_override_default_value(self): """Test that overriding a default value in gen_key_input() works.""" @@ -399,8 +400,9 @@ class GPGTestCase(unittest.TestCase): def test_list_keys_after_generation(self): """Test that after key generation, the generated key is available.""" - self.test_list_keys_initial() - self.do_key_generation() + self.test_list_keys_initial_public() + self.test_list_keys_initial_secret() + self.generate_key("Johannes Trithemius", 'iusedcarrierpidgeons@inste.ad') public_keys = self.gpg.list_keys() self.assertTrue(is_list_with_len(public_keys, 1), "1-element list expected") @@ -670,7 +672,8 @@ class GPGTestCase(unittest.TestCase): suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_parsers_is_hex_valid', - 'test_parsers_is_hex_invalid',]), + 'test_parsers_is_hex_invalid', + 'test_copy_data_bytesio',]), 'basic': set(['test_gpghome_creation', 'test_gpg_binary', 'test_gpg_binary_not_abs', @@ -768,11 +771,6 @@ if __name__ == "__main__": suite_names = list() for name, methodset in suites.items(): suite_names.append(name) - this_file = inspect.getfile(inspect.currentframe()).split('.', 1)[0] - #mod = getattr(this_file, '__dict__', None) - #func = getattr(gnupg.__module__, '__setattr__', None) - #if func is not None: - # func(name, list(methodset)) setattr(GPGTestCase, name, list(methodset)) parser = argparse.ArgumentParser(description="Unittests for python-gnupg") diff --git a/gnupg/util.py b/gnupg/util.py index 169d4fd..dd9d1f1 100644 --- a/gnupg/util.py +++ b/gnupg/util.py @@ -30,6 +30,11 @@ from datetime import datetime import logging import os +import time +import random +import string +import sys +import threading try: from io import StringIO @@ -62,6 +67,54 @@ _ugpg = os.path.join(_user, '.gnupg') ## $HOME/.gnupg _conf = os.path.join(os.path.join(_user, '.config'), 'python-gnupg') ## $HOME/.config/python-gnupg + +def _copy_data(instream, outstream): + """Copy data from one stream to another. + + :type instream: :class:`io.BytesIO` or :class:`io.StringIO` or file + :param instream: A byte stream or open file to read from. + :param file outstream: The file descriptor of a tmpfile to write to. + """ + sent = 0 + + #try: + # #assert (util._is_stream(instream) + # # or isinstance(instream, file)), "instream not stream or file" + # assert isinstance(outstream, file), "outstream is not a file" + #except AssertionError as ae: + # logger.exception(ae) + # return + + if hasattr(sys.stdin, 'encoding'): + enc = sys.stdin.encoding + else: + enc = 'ascii' + + while True: + data = instream.read(1024) + if len(data) == 0: + break + sent += len(data) + logger.debug("_copy_data(): sending chunk (%d):\n%s" % (sent, data[:256])) + try: + outstream.write(data) + except UnicodeError: + try: + outstream.write(data.encode(enc)) + except IOError: + logger.exception('_copy_data(): Error sending data: Broken pipe') + break + except IOError: + # Can get 'broken pipe' errors even when all data was sent + logger.exception('_copy_data(): Error sending data: Broken pipe') + break + try: + outstream.close() + except IOError: + logger.exception('_copy_data(): Got IOError while closing %s' % outstream) + else: + logger.debug("_copy_data(): Closed output, %d bytes sent." % sent) + def _create_gpghome(gpghome): """Create the specified GnuPG home directory, if necessary. @@ -181,15 +234,78 @@ def _make_binary_stream(s, encoding): rv = StringIO(s) return rv -## xxx unused function? -def _today(): - """Get the current date. +def _make_passphrase(length=None, save=False, file=None): + """Create a passphrase and write it to a file that only the user can read. + + This is not very secure, and should not be relied upon for actual key + passphrases. + + :param int length: The length in bytes of the string to generate. + + :param file file: The file to save the generated passphrase in. If not + given, defaults to 'passphrase--' in the top-level directory. + """ + if not length: + length = 40 + + passphrase = _make_random_string(length) + + if save: + ruid, euid, suid = os.getresuid() + gid = os.getgid() + now = time.mktime(time.gmtime()) + + if not file: + filename = str('passphrase-%s-%s' % uid, now) + file = os.path.join(_repo, filename) + + with open(file, 'a') as fh: + fh.write(passphrase) + fh.flush() + fh.close() + os.chmod(file, 0600) + os.chown(file, ruid, gid) + + logger.warn("Generated passphrase saved to %s" % file) + return passphrase + +def _make_random_string(length): + """Returns a random lowercase, uppercase, alphanumerical string. + + :param int length: The length in bytes of the string to generate. + """ + chars = string.ascii_lowercase + string.ascii_uppercase + string.digits + return ''.join(random.choice(chars) for x in range(length)) + +def _next_year(): + """Get the date of today plus one year. :rtype: str - :returns: The date, in the format '%Y-%m-%d'. + :returns: The date of this day next year, in the format '%Y-%m-%d'. """ - now_string = datetime.now().__str__() - return now_string.split(' ', 1)[0] + now = datetime.now().__str__() + date = now.split(' ', 1)[0] + year, month, day = date.split('-', 2) + next_year = str(int(year)+1) + return '-'.join((next_year, month, day)) + +def _threaded_copy_data(instream, outstream): + """Copy data from one stream to another in a separate thread. + + Wraps ``_copy_data()`` in a :class:`threading.Thread`. + + :type instream: :class:`io.BytesIO` or :class:`io.StringIO` + :param instream: A byte stream to read from. + :param file outstream: The file descriptor of a tmpfile to write to. + """ + copy_thread = threading.Thread(target=_copy_data, + args=(instream, outstream)) + copy_thread.setDaemon(True) + logger.debug('_threaded_copy_data(): %r, %r, %r', copy_thread, + instream, outstream) + copy_thread.start() + return copy_thread def _which(executable, flags=os.X_OK): """Borrowed from Twisted's :mod:twisted.python.proutils . @@ -228,3 +344,9 @@ def _which(executable, flags=os.X_OK): if os.access(pext, flags): result.append(pext) return result + +def _write_passphrase(stream, passphrase, encoding): + passphrase = '%s\n' % passphrase + passphrase = passphrase.encode(encoding) + stream.write(passphrase) + logger.debug("_write_passphrase(): Wrote passphrase.")