Commit e31d72d5 authored by anarcat's avatar anarcat

Merge branch 'feature/use-logging' into '2.x'

Use python's logging module instead of custom mothods

Closes #43

See merge request !17
parents bf275c27 22bf7f96
......@@ -20,10 +20,13 @@ import locale
import sys
import os
import getpass
import logging
from monkeysign.ui import MonkeysignUi
import monkeysign.translation
logger = logging.getLogger(__name__)
class MonkeysignCli(MonkeysignUi):
"""sign a key in a safe fashion.
......@@ -48,11 +51,11 @@ passwords."""
os.environ['GPG_TTY'] = os.ttyname(sys.stdin.fileno())
except OSError as e:
if e.errno == errno.ENOTTY:
self.warn(_('cannot find your TTY, GPG may freak out if you do not set the GPG_TTY environment'))
logger.warning(_('cannot find your TTY, GPG may freak out if you do not set the GPG_TTY environment'))
else:
raise
else:
self.log(_('reset GPG_TTY to %s') % os.environ['GPG_TTY'])
logger.info(_('reset GPG_TTY to %s'), os.environ['GPG_TTY'])
# 1. fetch the key into a temporary keyring
self.find_key()
......@@ -60,7 +63,7 @@ passwords."""
# 2. copy the signing key secrets into the keyring
self.copy_secrets()
self.warn(_('Preparing to sign with this key\n\n%s') % self.signing_key)
logger.warning(_('Preparing to sign with this key\n\n%s'), self.signing_key)
# 3. for every user id (or all, if -a is specified)
# 3.1. sign the uid, using gpg-agent
......@@ -104,7 +107,7 @@ passwords."""
# workaround http://bugs.python.org/issue7768
pattern = raw_input(prompt.encode(sys.stdout.encoding or locale.getpreferredencoding(True)))
while not (pattern in allowed_uids or (pattern.isdigit() and int(pattern)-1 in range(0,len(allowed_uids)))):
print _('invalid uid')
logger.warning(_('invalid uid'))
pattern = raw_input(prompt.encode(sys.stdout.encoding or locale.getpreferredencoding(True)))
if pattern.isdigit():
pattern = allowed_uids[int(pattern)-1]
......
......@@ -70,11 +70,14 @@ to the user.
import errno
import os, tempfile, shutil, subprocess, re
from datetime import datetime
import logging
from StringIO import StringIO
import monkeysign.translation
logger = logging.getLogger(__name__)
class Context():
"""Python wrapper for GnuPG
......@@ -105,10 +108,6 @@ class Context():
'list-options': 'show-sig-subpackets,show-uid-validity,show-unusable-uids,show-unusable-subkeys,show-keyring,show-sig-expire',
}
# whether to paste output here and there
# if not false, needs to be a file descriptor
debug = False
def __init__(self):
self.options = dict(Context.options) # copy
......@@ -177,15 +176,14 @@ class Context():
we can optionnally watch for a confirmation pattern on the
statusfd.
"""
logger.debug('command: %s', self.build_command(command))
proc = subprocess.Popen(self.build_command(command), # nosec
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(self.stdout, self.stderr) = proc.communicate(stdin)
self.returncode = proc.returncode
if self.debug:
print >>self.debug, 'command:', self.build_command(command)
print >>self.debug, 'ret:', self.returncode, 'stdout:', self.stdout, 'stderr:', self.stderr
logger.debug('ret: %s stdout: %s stderr: %s', self.returncode, self.stdout, self.stderr)
return proc.returncode == 0
def seek_pattern(self, fd, pattern):
......@@ -204,11 +202,11 @@ class Context():
line = fd.readline()
match = re.search(pattern, line)
while line and not match:
if self.debug: print >>self.debug, "skipped:", line,
logger.debug("skipped: %s", line)
line = fd.readline()
match = re.search(pattern, line)
if match:
if self.debug: print >>self.debug, "FOUND:", line,
logger.debug("FOUND: %s", line)
return match
else:
raise GpgProtocolError(self.returncode, _("could not find pattern '%s' in input, last skipped '%s'") % (pattern, line))
......@@ -237,14 +235,16 @@ class Context():
ignored = ('[GNUPG:] KEYEXPIRED', '[GNUPG:] SIGEXPIRED', '[GNUPG:] KEY_CONSIDERED', 'gpg: ')
while line and line.startswith(ignored):
if self.debug: print >>self.debug, "IGNORED:", line,
logger.debug("IGNORED: %s", line)
line = fd.readline()
match = re.search(pattern, line)
if self.debug:
if match: print >>self.debug, "FOUND:", line,
else: print >>self.debug, "SKIPPED:", line,
if match:
logger.debug("FOUND: %s", line)
else:
logger.debug("SKIPPED: %s",line)
if not match:
raise GpgProtocolError(self.returncode, 'expected "%s", found "%s"' % (pattern, line))
return match
......@@ -262,8 +262,7 @@ class Context():
but really, the pipes are often setup outside of here so the
fd is hardcoded here
"""
if self.debug:
print >>self.debug, "WROTE:", message
logger.debug("WROTE: %s", message)
print >>fd, message
def version(self):
......@@ -468,8 +467,7 @@ class Keyring():
def del_uid(self, fingerprint, pattern):
command = self.context.build_command(['edit-key', fingerprint])
if self.context.debug:
print >>self.context.debug, 'command:', command
logger.debug('command: %s', command)
proc = subprocess.Popen(command, # nosec
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
......@@ -515,8 +513,7 @@ class Keyring():
# output of --sign-key
command = self.context.build_command([['sign-key',
'lsign-key'][local], pattern])
if self.context.debug:
print >>self.context.debug, 'command:', command
logger.debug('command: %s', command)
proc = subprocess.Popen(command, # nosec
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
......
......@@ -26,6 +26,8 @@ import StringIO
import stat
import subprocess # nosec
import webbrowser
import logging
import warnings
from glib import GError
import gobject
......@@ -44,6 +46,8 @@ import monkeysign.translation
from monkeysign.msg_exception import errorhandler
import monkeysign
logger = logging.getLogger(__name__)
class MonkeysignScanUi(MonkeysignUi):
"""sign a key in a safe fashion using a webcam to scan for qr-codes
......@@ -88,6 +92,10 @@ passwords.
self.window = MonkeysignScan()
self.window.msui = self
# Add a handler to the root logger that displays warnings and
# errors as dialogs
logging.getLogger().addHandler(GTKLoggingHandler(self.window))
# XXX: this probably belongs lower in the stack,
# because we don't want to create a temporary keyring
# just when we start the graphical UI, but instead
......@@ -129,12 +137,14 @@ passwords.
self.window.resume_capture()
def warn(self, prompt):
"""display the message but let things go"""
md = gtk.MessageDialog(self.window, gtk.DIALOG_DESTROY_WITH_PARENT,
gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, prompt)
with gtk.gdk.lock:
md.run()
md.destroy()
"""display the message but let things go
DEPRECATED: This method has been deprecated. Use Logger.warning instead.
"""
warning = ("This method is deprecated, and will be removed in a future version. "
"As alternative, use Logger.warning()")
warnings.warn(warning, DeprecationWarning)
logger.warning(prompt)
def choose_uid(self, prompt, key):
md = gtk.Dialog(prompt, self.window, gtk.DIALOG_DESTROY_WITH_PARENT,
......@@ -159,11 +169,11 @@ passwords.
label = None
if response == gtk.RESPONSE_ACCEPT:
self.log(_('okay, signing'))
logger.info(_('okay, signing'))
label = [radio for radio in self.uid_radios.get_group()
if radio.get_active()][0].get_label()
else:
self.log(_('user denied signature'))
logger.info(_('user denied signature'))
md.destroy()
return label
......@@ -232,7 +242,7 @@ class MonkeysignScan(gtk.Window):
except GError:
# misconfigured, ignore
# XXX: no access to logging, again
# self.msui.warn("could not find icon: %s" % e)
# logger.warning("could not find icon: %s", e)
pass
except pkg_resources.DistributionNotFound:
# not installed system-wide, ignore
......@@ -469,9 +479,15 @@ class MonkeysignScan(gtk.Window):
if not verified:
raise GpgRuntimeError(0, _('cannot find signature for image file'))
except GpgRuntimeError :
self.msui.warn(_("The image provided cannot be verified using a trusted OpenPGP signature.\n\nMake sure the image comes from a trusted source (e.g. your own camera, which you have never left unsurveilled) before signing this!\n\nDO NOT SIGN UNTRUSTED FINGERPRINTS!\n\nTo get rid of this warning, if you really trust this image, use the following command to sign the file\n\n gpg -s --detach %s\n") % filename)
logger.warning(_("The image provided cannot be verified using a trusted OpenPGP signature.\n\n"
"Make sure the image comes from a trusted source (e.g. your own camera, which "
"you have never left unsurveilled) before signing this!\n\nDO NOT SIGN UNTRUSTED "
"FINGERPRINTS!\n\n"
"To get rid of this warning, if you really trust this image, use the following "
"command to sign the file\n\n"
" gpg -s --detach %s\n"), filename)
else:
self.msui.log(_('image signature verified successfully'))
logger.info(_('image signature verified successfully'))
self.scan_image(filename)
return
......@@ -513,7 +529,7 @@ class MonkeysignScan(gtk.Window):
self.process_scan(symbol.data)
found = True
if not found:
self.msui.warn(_('no data found in image!'))
logger.warning(_('no data found in image!'))
def save_qrcode(self, widget=None):
"""Use a file chooser dialog to enable user to save the current QR
......@@ -521,7 +537,7 @@ class MonkeysignScan(gtk.Window):
if self.active_key is None:
# XXX: without this, warn() freezes, go figure
gtk.gdk.threads_leave()
self.msui.warn(_('No identity selected. Select one from the identiy menu or generate a OpenPGP key if none is available.'))
logger.warning(_('No identity selected. Select one from the identiy menu or generate a OpenPGP key if none is available.'))
return
key = self.active_key
image = self.make_qrcode(key.fpr)
......@@ -547,7 +563,7 @@ class MonkeysignScan(gtk.Window):
if self.active_key is None:
# XXX: without this, warn() freezes, go figure
gtk.gdk.threads_leave()
self.msui.warn(_('No identity selected. Select one from the identiy menu or generate a OpenPGP key if none is available.'))
logger.warning(_('No identity selected. Select one from the identiy menu or generate a OpenPGP key if none is available.'))
return
keyid = self.active_key.subkeys[0].keyid()
print_op = gtk.PrintOperation()
......@@ -647,7 +663,7 @@ class MonkeysignScan(gtk.Window):
# this is actually because the key was
# imported without having to create a dialog
pass
self.msui.log(_('fetching finished'))
logger.info(_('fetching finished'))
if condition == 0:
# 2. copy the signing key secrets into the keyring
self.msui.copy_secrets()
......@@ -697,7 +713,7 @@ class MonkeysignScan(gtk.Window):
def process_scan(self, data):
"""process zbar-scanned data"""
self.msui.log(_('zbar captured a frame, looking for 40 character hexadecimal fingerprint in %s') % data)
logger.info(_('zbar captured a frame, looking for 40 character hexadecimal fingerprint in %s'), data)
m = re.search("((?:[0-9A-F]{4}\s*){10})", data, re.IGNORECASE)
if m is not None:
......@@ -709,8 +725,8 @@ class MonkeysignScan(gtk.Window):
# interactive but that's ugly as hell - find_key() should
# take a callback maybe?
# 1.a) from the local keyring
self.msui.log(_('looking for key %s in your keyring')
% self.msui.pattern)
logger.info(_('looking for key %s in your keyring'),
self.msui.pattern)
self.msui.keyring.context.set_option('export-options',
'export-minimal')
if self.msui.tmpkeyring.import_data(self.msui.keyring.export_data(self.msui.pattern)):
......@@ -728,7 +744,7 @@ class MonkeysignScan(gtk.Window):
if self.msui.options.keyserver is not None:
self.msui.tmpkeyring.context.set_option('keyserver', self.msui.options.keyserver)
command = self.msui.tmpkeyring.context.build_command(['recv-keys', self.msui.pattern])
self.msui.log('cmd: ' + str(command))
logger.info('cmd: ' + str(command))
self.dialog = gtk.Dialog(title=_('Please wait'), parent=None, flags=gtk.DIALOG_MODAL, buttons=None)
self.dialog.add_button('gtk-cancel', gtk.RESPONSE_CANCEL)
message = gtk.Label(_('Retrieving public key from server...'))
......@@ -748,7 +764,7 @@ class MonkeysignScan(gtk.Window):
if self.dialog.run() == gtk.RESPONSE_CANCEL:
proc.kill()
else:
self.msui.warn(_('data found in barcode does not match a OpenPGP fingerprint pattern: %s') % data)
logger.warning(_('data found in barcode does not match a OpenPGP fingerprint pattern: %s'), data)
self.resume_capture()
def resume_capture(self):
......@@ -810,3 +826,28 @@ class MonkeysignScan(gtk.Window):
menu.append(new_menu_item)
menu.popup(None, None, None, event.button, event.time)
class GTKLoggingHandler(logging.Handler):
"""
Handles log messages and displays a dialog with the message.
"""
def __init__(self, window):
self.window = window
super(GTKLoggingHandler, self).__init__()
def emit(self, record):
# Set the dialog type for ERROR, CRRITICAL, and WARNING. Don't
# show any other levels.
if record.levelno is logging.ERROR or record.levelno is logging.CRITICAL:
level = gtk.MESSAGE_ERROR
elif record.levelno is logging.WARNING:
level = gtk.MESSAGE_WARNING
else:
return
md = gtk.MessageDialog(self.window, gtk.DIALOG_DESTROY_WITH_PARENT,
level, gtk.BUTTONS_OK, record.msg)
with gtk.gdk.lock:
md.run()
md.destroy()
......@@ -26,12 +26,15 @@ from StringIO import StringIO
import unittest
import tempfile
import re
import logging
sys.path.insert(0, os.path.dirname(__file__) + '/..')
from monkeysign.gpg import *
from test_lib import find_test_file, skipUnlessUnicodeLocale, skipIfDatePassed
logger = logging.getLogger(__name__)
class TestContext(unittest.TestCase):
"""Tests for the Context class.
......@@ -77,16 +80,6 @@ class TestContext(unittest.TestCase):
"""make sure version() returns something"""
self.assertTrue(self.gpg.version())
def test_seek_debug(self):
"""test if seek actually respects debug"""
self.gpg.debug = True # should yield an attribute error, that's fine
with self.assertRaises(AttributeError):
self.gpg.seek(StringIO('test'), 'test')
# now within a keyring?
k = TempKeyring()
k.context.debug = True
with self.assertRaises(AttributeError):
k.import_data(open(find_test_file('96F47C6A.asc')).read())
class TestTempKeyring(unittest.TestCase):
"""Test the TempKeyring class."""
......@@ -212,9 +205,8 @@ class TestKeyringWithKeys(TestKeyringBase):
@todo we should check the data structure
"""
# just a cute display for now
for fpr, key in self.gpg.get_keys('96F47C6A').iteritems():
print key
logger.debug(key)
def test_sign_key_wrong_user(self):
"""make sure sign_key with a erroneous local-user fails
......@@ -424,8 +416,9 @@ fpr:::::::::C9E1F1230DBE47D57BAB3C60586073B34023702F:
uid:::::::2451063FCBB4D262938687C2D8F6B949B0A3AF01::The Anarcat <anarcat@anarcat.ath.cx>:
ssb::2048:16:C016FF12EB8D47BB:1110320966::::::::::""")
def test_print(self):
print self.key
def test_str(self):
"""Tests that the __str__ call of OpenPGPKey works for secret keys"""
self.assertIn('C9E1 F123 0DBE 47D5 7BAB 3C60 5860 73B3 4023 702F', self.key.__str__())
if __name__ == '__main__':
unittest.main()
......@@ -29,6 +29,8 @@ from pkg_resources import ResolutionError
import sys
import re
from StringIO import StringIO
import logging
import logging.handlers
import tempfile
try:
from unittest.mock import Mock
......@@ -64,6 +66,7 @@ import test_lib
# optimized because called often
from test_lib import find_test_file, skipIfDatePassed, skipUnlessNetwork
logger = logging.getLogger(__name__)
class CliBaseTest(unittest.TestCase):
def setUp(self):
......@@ -230,6 +233,7 @@ class CliTestSpacedFingerprint(CliTestDialog):
sys.argv.pop() # remove the uid from parent class
sys.argv += '8DC9 01CE 6414 6C04 8AD5 0FBB 7921 5252 7B75 921E'.split()
class BaseTestCase(unittest.TestCase):
pattern = None
args = []
......@@ -244,6 +248,7 @@ class BaseTestCase(unittest.TestCase):
self.ui.keyring = TempKeyring()
self.ui.prepare() # needed because we changed the base keyring
class BasicTests(BaseTestCase):
pattern = '7B75921E'
......@@ -263,11 +268,26 @@ class SigningTests(BaseTestCase):
def setUp(self):
"""setup a basic keyring capable of signing a local key"""
BaseTestCase.setUp(self)
if not self.ui.options.debug:
# Save the logging handlers on the root logger to a variable
# and replace them with a NullHandler (the null handler
# prevents logging calls from trying to add a stream handler
# back). This stops output from the tested functions from
# printing to the console and cluttering things up.
self.logging_handlers = logging.getLogger().handlers
logging.getLogger().handlers = [logging.NullHandler()]
self.assertTrue(self.ui.keyring.import_data(open(find_test_file('7B75921E.asc')).read()))
self.assertTrue(self.ui.tmpkeyring.import_data(open(find_test_file('96F47C6A.asc')).read()))
self.assertTrue(self.ui.keyring.import_data(open(find_test_file('96F47C6A.asc')).read()))
self.assertTrue(self.ui.keyring.import_data(open(find_test_file('96F47C6A-secret.asc')).read()))
def tearDown(self):
if not self.ui.options.debug:
# Restore the logging handlers we removed in the setup function
logging.getLogger().handlers = self.logging_handlers
def test_find_key(self):
"""test if we can extract the key locally
......@@ -356,42 +376,64 @@ this duplicates tests from the gpg code, but is necessary to test later function
def test_export_key(self):
"""see if we export a properly encrypted key set"""
messages = []
# collect messages instead of warning the user
self.ui.warn = messages.append
self.test_sign_key()
self.ui.export_key()
self.assertIsNone(self.ui.export_key(), 'sends mail?')
paste = messages.pop()
self.assertNotIn('BEGIN PGP PUBLIC KEY BLOCK', paste,
'message not encrypted')
self.assertIn('BEGIN PGP MESSAGE', paste, 'message not encrypted')
self.assertNotIn('MIME-Version', paste,
'message to paste has weird MIME headers')
# Create a handler to store messages being logged
memory_handler = logging.handlers.MemoryHandler(0)
# Add this handler to the monkeysign.ui logger
monkeysign_ui_logger = logging.getLogger('monkeysign.ui')
monkeysign_ui_logger.addHandler(memory_handler)
try:
self.test_sign_key()
self.ui.export_key()
self.assertIsNone(self.ui.export_key(), 'sends mail?')
contains_exported_key = False
for record in memory_handler.buffer:
if (record.levelname == 'WARNING' and
"here's the encrypted signed public key you can paste in your email client" in record.getMessage()):
contains_exported_key = True
self.assertNotIn('BEGIN PGP PUBLIC KEY BLOCK', record.getMessage(), 'message not encrypted')
self.assertNotIn('MIME-Version', record.getMessage(), 'message to paste has weird MIME headers')
self.assertIn('BEGIN PGP MESSAGE', record.getMessage(), 'message not encrypted')
break
self.assertTrue(contains_exported_key, 'no exported key logged')
finally:
# Remove the handler when we're done.
monkeysign_ui_logger.removeHandler(memory_handler)
def test_sendmail(self):
"""see if we can generate a proper commandline to send email"""
self.test_sign_key()
messages = []
# collect messages instead of warning the user
self.ui.warn = messages.append
self.ui.options.nomail = False
self.ui.options.user = 'unittests@localhost'
self.ui.options.to = 'devnull@localhost'
self.ui.options.mta = "dd status=none of='" + \
self.ui.keyring.homedir + "/%(to)s'"
self.assertTrue(self.ui.export_key(), 'fails to send mail')
filename = '%s/%s' % (self.ui.keyring.homedir, self.ui.options.to)
self.assertGreater(os.path.getsize(filename), 0,
'mail properly created')
self.assertIn('sent message to %s with dd' % self.ui.options.to,
messages.pop(),
'missing information to user')
self.ui.options.to = 'devnull; touch bad'
self.assertTrue(self.ui.export_key(),
'fails to send email to weird address')
self.assertFalse(os.path.exists('bad'),
'vulnerable to command injection')
# Create a handler to store messages being logged
memory_handler = logging.handlers.MemoryHandler(0)
# Add this handler to the monkeysign.ui logger
monkeysign_ui_logger = logging.getLogger('monkeysign.ui')
monkeysign_ui_logger.addHandler(memory_handler)
try:
self.ui.options.nomail = False
self.ui.options.user = 'unittests@localhost'
self.ui.options.to = 'devnull@localhost'
self.ui.options.mta = "dd status=none of='" + \
self.ui.keyring.homedir + "/%(to)s'"
self.assertTrue(self.ui.export_key(), 'fails to send mail')
filename = '%s/%s' % (self.ui.keyring.homedir, self.ui.options.to)
self.assertGreater(os.path.getsize(filename), 0,
'mail properly created')
contains_information_for_user = False
for record in memory_handler.buffer:
if (record.levelname == 'WARNING' and
'sent message to %s with dd' % self.ui.options.to in record.getMessage()):
contains_information_for_user = True
self.assertTrue(contains_information_for_user, 'missing information to user')
self.ui.options.to = 'devnull; touch bad'
self.assertTrue(self.ui.export_key(),
'fails to send email to weird address')
self.assertFalse(os.path.exists('bad'),
'vulnerable to command injection')
finally:
# Remove the handler when we're done.
monkeysign_ui_logger.removeHandler(memory_handler)
def test_mua(self):
self.test_sign_key()
......@@ -541,7 +583,3 @@ class KeyserverTests(BaseTestCase):
def test_find_key(self):
"""this should find the key on the keyservers"""
self.ui.find_key()
if __name__ == '__main__':
unittest.main()
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment