summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authormathieui <mathieui@mathieui.net>2014-12-11 22:28:44 +0100
committermathieui <mathieui@mathieui.net>2014-12-11 22:28:44 +0100
commit00396c158aa032585db88cfd4b622281ba3cbd7f (patch)
treedfe9711c6ccc3b908c1de8a06bc5080139113bf4 /src
parent21d8a3e7e19dc639262ac7fa7d7817351ff8b4c1 (diff)
downloadpoezio-00396c158aa032585db88cfd4b622281ba3cbd7f.tar.gz
poezio-00396c158aa032585db88cfd4b622281ba3cbd7f.tar.bz2
poezio-00396c158aa032585db88cfd4b622281ba3cbd7f.tar.xz
poezio-00396c158aa032585db88cfd4b622281ba3cbd7f.zip
Fix #2847 (SASL External support)
- Add two new options, keyfile and certfile, which must be both set for the auth to work. - if both are set, then poezio doesn’t force-prompt a password if there is none specified - add /cert_add, /cert_fetch, /cert_disable, /cert_revoke and /certs commands. - add a page of documentation on the process
Diffstat (limited to 'src')
-rw-r--r--src/config.py2
-rw-r--r--src/connection.py16
-rw-r--r--src/core/handlers.py1
-rw-r--r--src/tabs/rostertab.py166
4 files changed, 184 insertions, 1 deletions
diff --git a/src/config.py b/src/config.py
index 390fe7f1..86aae8ef 100644
--- a/src/config.py
+++ b/src/config.py
@@ -34,6 +34,7 @@ DEFAULT_CONFIG = {
'beep_on': 'highlight private invite',
'ca_cert_path': '',
'certificate': '',
+ 'certfile': '',
'ciphers': 'HIGH+kEDH:HIGH+kEECDH:HIGH:!PSK:!SRP:!3DES:!aNULL',
'connection_check_interval': 60,
'connection_timeout_delay': 10,
@@ -68,6 +69,7 @@ DEFAULT_CONFIG = {
'ignore_private': False,
'information_buffer_popup_on': 'error roster warning help info',
'jid': '',
+ 'keyfile': '',
'lang': 'en',
'lazy_resize': True,
'load_log': 10,
diff --git a/src/connection.py b/src/connection.py
index 1bbe632d..cd2ccedd 100644
--- a/src/connection.py
+++ b/src/connection.py
@@ -30,6 +30,10 @@ class Connection(slixmpp.ClientXMPP):
__init = False
def __init__(self):
resource = config.get('resource')
+
+ keyfile = config.get('keyfile')
+ certfile = config.get('certfile')
+
if config.get('jid'):
# Field used to know if we are anonymous or not.
# many features will be handled differently
@@ -38,7 +42,9 @@ class Connection(slixmpp.ClientXMPP):
jid = '%s' % config.get('jid')
if resource:
jid = '%s/%s'% (jid, resource)
- password = config.get('password') or getpass.getpass()
+ password = config.get('password')
+ if not password and not (keyfile and certfile):
+ password = getpass.getpass()
else: # anonymous auth
self.anon = True
jid = config.get('server')
@@ -57,6 +63,13 @@ class Connection(slixmpp.ClientXMPP):
self['feature_mechanisms'].unencrypted_cram = False
self['feature_mechanisms'].unencrypted_scram = False
+ self.keyfile = config.get('keyfile')
+ self.certfile = config.get('certfile')
+ if keyfile and not certfile:
+ log.error('keyfile is present in configuration file without certfile')
+ elif certfile and not keyfile:
+ log.error('certfile is present in configuration file without keyfile')
+
self.core = None
self.auto_reconnect = config.get('auto_reconnect')
self.reconnect_max_attempts = 0
@@ -127,6 +140,7 @@ class Connection(slixmpp.ClientXMPP):
self.register_plugin('xep_0202')
self.register_plugin('xep_0224')
self.register_plugin('xep_0249')
+ self.register_plugin('xep_0257')
self.register_plugin('xep_0280')
self.register_plugin('xep_0297')
self.register_plugin('xep_0308')
diff --git a/src/core/handlers.py b/src/core/handlers.py
index 6b613a93..a1e8596c 100644
--- a/src/core/handlers.py
+++ b/src/core/handlers.py
@@ -58,6 +58,7 @@ def on_session_start_features(self, _):
features = iq['disco_info']['features']
rostertab = self.get_tab_by_name('Roster', tabs.RosterInfoTab)
rostertab.check_blocking(features)
+ rostertab.check_saslexternal(features)
if (config.get('enable_carbons') and
'urn:xmpp:carbons:2' in features):
self.xmpp.plugin['xep_0280'].enable()
diff --git a/src/tabs/rostertab.py b/src/tabs/rostertab.py
index 52a3a88c..846ba327 100644
--- a/src/tabs/rostertab.py
+++ b/src/tabs/rostertab.py
@@ -10,9 +10,11 @@ from gettext import gettext as _
import logging
log = logging.getLogger(__name__)
+import base64
import curses
import difflib
import os
+import ssl
from os import getenv, path
from . import Tab
@@ -146,6 +148,170 @@ class RosterInfoTab(Tab):
self.core.xmpp.del_event_handler('session_start', self.check_blocking)
self.core.xmpp.add_event_handler('blocked_message', self.on_blocked_message)
+ def check_saslexternal(self, features):
+ if 'urn:xmpp:saslcert:1' in features:
+ self.register_command('certs', self.command_certs,
+ desc=_('List the fingerprints of certificates'
+ ' which can connect to your account.'),
+ shortdesc=_('List allowed client certs.'))
+ self.register_command('cert_add', self.command_cert_add,
+ desc=_('Add a client certificate to the authorized ones. '
+ 'It must have an unique name and be contained in '
+ 'a PEM file. [management] is a boolean indicating'
+ ' if a client connected using this certificate can'
+ ' manage the certificates itself.'),
+ shortdesc=_('Add a client certificate.'),
+ usage='<name> <certificate path> [management]')
+ self.register_command('cert_disable', self.command_cert_disable,
+ desc=_('Remove a certificate from the list '
+ 'of allowed ones. Clients currently '
+ 'using this certificate will not be '
+ 'forcefully disconnected.'),
+ shortdesc=_('Disable a certificate'),
+ usage='<name>')
+ self.register_command('cert_revoke', self.command_cert_revoke,
+ desc=_('Remove a certificate from the list '
+ 'of allowed ones. Clients currently '
+ 'using this certificate will be '
+ 'forcefully disconnected.'),
+ shortdesc=_('Revoke a certificate'),
+ usage='<name>')
+ self.register_command('cert_fetch', self.command_cert_fetch,
+ desc=_('Retrieve a certificate with its '
+ 'name. It will be stored in <path>.'),
+ shortdesc=_('Fetch a certificate'),
+ usage='<name> <path>')
+
+ @command_args_parser.ignored
+ def command_certs(self):
+ """
+ /certs
+ """
+ def cb(iq):
+ if iq['type'] == 'error':
+ self.core.information(_('Unable to retrieve the certificate list.'),
+ _('Error'))
+ return
+ certs = []
+ for item in iq['sasl_certs']['items']:
+ users = '\n'.join(item['users'])
+ certs.append((item['name'], users))
+
+ if not certs:
+ return self.core.information(_('No certificates found'), _('Info'))
+ msg = _('Certificates:\n')
+ msg += '\n'.join(((' %s%s' % (item[0] + (': ' if item[1] else ''), item[1])) for item in certs))
+ self.core.information(msg, 'Info')
+
+ self.core.xmpp.plugin['xep_0257'].get_certs(callback=cb, timeout=3)
+
+ @command_args_parser.quoted(2, 1)
+ def command_cert_add(self, args):
+ """
+ /cert_add <name> <certfile> [cert-management]
+ """
+ if not args or len(args) < 2:
+ return self.core.command_help('cert_add')
+ def cb(iq):
+ if iq['type'] == 'error':
+ self.core.information(_('Unable to add the certificate.'), _('Error'))
+ else:
+ self.core.information(_('Certificate added.'), _('Info'))
+
+ name = args[0]
+
+ try:
+ with open(args[1]) as fd:
+ crt = fd.read()
+ crt = crt.replace(ssl.PEM_FOOTER, '').replace(ssl.PEM_HEADER, '').replace(' ', '').replace('\n', '')
+ except Exception as e:
+ self.core.information('Unable to read the certificate: %s' % e, 'Error')
+ return
+
+ if len(args) > 2:
+ management = args[2]
+ if management:
+ management = management.lower()
+ if management not in ('false', '0'):
+ management = True
+ else:
+ management = False
+ else:
+ management = False
+ else:
+ management = True
+
+ self.core.xmpp.plugin['xep_0257'].add_cert(name, crt, callback=cb,
+ allow_management=management)
+
+ @command_args_parser.quoted(1)
+ def command_cert_disable(self, args):
+ """
+ /cert_disable <name>
+ """
+ if not args:
+ return self.core.command_help('cert_disable')
+ def cb(iq):
+ if iq['type'] == 'error':
+ self.core.information(_('Unable to disable the certificate.'), _('Error'))
+ else:
+ self.core.information(_('Certificate disabled.'), _('Info'))
+
+ name = args[0]
+
+ self.core.xmpp.plugin['xep_0257'].disable_cert(name, callback=cb)
+
+ @command_args_parser.quoted(1)
+ def command_cert_revoke(self, args):
+ """
+ /cert_revoke <name>
+ """
+ if not args:
+ return self.core.command_help('cert_revoke')
+ def cb(iq):
+ if iq['type'] == 'error':
+ self.core.information(_('Unable to revoke the certificate.'), _('Error'))
+ else:
+ self.core.information(_('Certificate revoked.'), _('Info'))
+
+ name = args[0]
+
+ self.core.xmpp.plugin['xep_0257'].revoke_cert(name, callback=cb)
+
+
+ @command_args_parser.quoted(2)
+ def command_cert_fetch(self, args):
+ """
+ /cert_fetch <name> <path>
+ """
+ if not args or len(args) < 2:
+ return self.core.command_help('cert_fetch')
+ def cb(iq):
+ if iq['type'] == 'error':
+ self.core.information(_('Unable to fetch the certificate.'),
+ _('Error'))
+ return
+
+ cert = None
+ for item in iq['sasl_certs']['items']:
+ if item['name'] == name:
+ cert = base64.b64decode(item['x509cert'])
+ break
+
+ if not cert:
+ return self.core.information(_('Certificate not found.'), _('Info'))
+
+ cert = ssl.DER_cert_to_PEM_cert(cert)
+ with open(path, 'w') as fd:
+ fd.write(cert)
+
+ self.core.information(_('File stored at %s') % path, 'Info')
+
+ name = args[0]
+ path = args[1]
+
+ self.core.xmpp.plugin['xep_0257'].get_certs(callback=cb)
+
def on_blocked_message(self, message):
"""
When we try to send a message to a blocked contact