connection.py 10 KB
Newer Older
louiz’'s avatar
louiz’ committed
1
# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org>
2 3 4 5
#
# This file is part of Poezio.
#
# Poezio is free software: you can redistribute it and/or modify
6
# it under the terms of the zlib license. See the COPYING file.
7 8 9 10
"""
Defines the Connection class
"""

11 12 13
import logging
log = logging.getLogger(__name__)

14
import getpass
mathieui's avatar
mathieui committed
15
import subprocess
16
import sys
17 18
import base64
import random
mathieui's avatar
mathieui committed
19

louiz’'s avatar
louiz’ committed
20
import slixmpp
21
from slixmpp import JID, InvalidJID
22
from slixmpp.xmlstream import ET
louiz’'s avatar
louiz’ committed
23
from slixmpp.plugins.xep_0184 import XEP_0184
24 25
from slixmpp.plugins.xep_0030 import DiscoInfo
from slixmpp.util import FileSystemCache
26

mathieui's avatar
mathieui committed
27 28
from poezio import common
from poezio import fixes
29 30
from poezio import xdg
from poezio.config import config, options
31

mathieui's avatar
mathieui committed
32

louiz’'s avatar
louiz’ committed
33
class Connection(slixmpp.ClientXMPP):
34
    """
35 36
    Receives everything from Jabber and emits the
    appropriate signals
37
    """
louiz’'s avatar
louiz’ committed
38
    __init = False
mathieui's avatar
mathieui committed
39

40
    def __init__(self):
mathieui's avatar
mathieui committed
41 42 43
        keyfile = config.get('keyfile')
        certfile = config.get('certfile')

44 45 46 47 48 49 50
        device_id = config.get('device_id')
        if not device_id:
            rng = random.SystemRandom()
            device_id = base64.urlsafe_b64encode(
                rng.getrandbits(24).to_bytes(3, 'little')).decode('ascii')
            config.set_and_save('device_id', device_id)

51
        if config.get('jid'):
52 53
            # Field used to know if we are anonymous or not.
            # many features will be handled differently
54
            # depending on this setting
55
            self.anon = False
56
            jid = config.get('jid')
mathieui's avatar
mathieui committed
57
            password = config.get('password')
mathieui's avatar
mathieui committed
58
            eval_password = config.get('eval_password')
mathieui's avatar
mathieui committed
59 60
            if not password and not eval_password and not (keyfile
                                                           and certfile):
mathieui's avatar
mathieui committed
61
                password = getpass.getpass()
mathieui's avatar
mathieui committed
62
            elif not password and not (keyfile and certfile):
mathieui's avatar
mathieui committed
63 64 65 66 67 68 69 70
                sys.stderr.write(
                    "No password or certificates provided, using the eval_password command.\n"
                )
                process = subprocess.Popen(
                    ['sh', '-c', eval_password],
                    stdin=subprocess.PIPE,
                    stdout=subprocess.PIPE,
                    close_fds=True)
71 72
                code = process.wait()
                if code != 0:
mathieui's avatar
mathieui committed
73 74 75
                    sys.stderr.write(
                        'The eval_password command (%s) returned a '
                        'nonzero status code: %s.\n' % (eval_password, code))
76 77
                    sys.stderr.write('Poezio will now exit\n')
                    sys.exit(code)
mathieui's avatar
mathieui committed
78 79 80
                password = process.stdout.readline().decode('utf-8').strip(
                    '\n')
        else:  # anonymous auth
81
            self.anon = True
82
            jid = config.get('server')
83
            password = None
84 85 86 87 88
        try:
            jid = JID(jid)
        except InvalidJID:
            sys.stderr.write('Invalid jid option: "%s" is not a valid JID\n' % jid)
            sys.exit(1)
mathieui's avatar
mathieui committed
89 90 91
        jid.resource = '%s-%s' % (
            jid.resource,
            device_id) if jid.resource else 'poezio-%s' % device_id
mathieui's avatar
mathieui committed
92
        # TODO: use the system language
mathieui's avatar
mathieui committed
93 94
        slixmpp.ClientXMPP.__init__(
            self, jid, password, lang=config.get('lang'))
95

96
        force_encryption = config.get('force_encryption')
mathieui's avatar
mathieui committed
97 98 99 100 101 102
        if force_encryption:
            self['feature_mechanisms'].unencrypted_plain = False
            self['feature_mechanisms'].unencrypted_digest = False
            self['feature_mechanisms'].unencrypted_cram = False
            self['feature_mechanisms'].unencrypted_scram = False

mathieui's avatar
mathieui committed
103 104 105
        self.keyfile = config.get('keyfile')
        self.certfile = config.get('certfile')
        if keyfile and not certfile:
mathieui's avatar
mathieui committed
106 107
            log.error(
                'keyfile is present in configuration file without certfile')
mathieui's avatar
mathieui committed
108
        elif certfile and not keyfile:
mathieui's avatar
mathieui committed
109 110
            log.error(
                'certfile is present in configuration file without keyfile')
mathieui's avatar
mathieui committed
111

112
        self.core = None
113
        self.auto_reconnect = config.get('auto_reconnect')
louiz’'s avatar
louiz’ committed
114
        self.auto_authorize = None
115 116
        # prosody defaults, lowest is AES128-SHA, it should be a minimum
        # for anything that came out after 2002
mathieui's avatar
mathieui committed
117 118 119
        self.ciphers = config.get(
            'ciphers', 'HIGH+kEDH:HIGH+kEECDH:HIGH:!PSK'
            ':!SRP:!3DES:!aNULL')
120 121 122
        self.ca_certs = config.get('ca_cert_path') or None
        interval = config.get('whitespace_interval')
        if int(interval) > 0:
mathieui's avatar
mathieui committed
123 124
            self.whitespace_keepalive_interval = int(interval)
        else:
125
            self.whitespace_keepalive = False
126
        self.register_plugin('xep_0004')
127
        self.register_plugin('xep_0012')
128
        # Must be loaded before 0030.
mathieui's avatar
mathieui committed
129 130 131 132 133 134 135 136 137 138 139
        self.register_plugin(
            'xep_0115', {
                'caps_node':
                'https://poez.io',
                'cache':
                FileSystemCache(
                    str(xdg.CACHE_HOME),
                    'caps',
                    encode=str,
                    decode=lambda x: DiscoInfo(ET.fromstring(x))),
            })
140
        self.register_plugin('xep_0030')
141
        self.register_plugin('xep_0045')
mathieui's avatar
mathieui committed
142
        self.register_plugin('xep_0048')
143
        self.register_plugin('xep_0050')
mathieui's avatar
mathieui committed
144
        self.register_plugin('xep_0054')
145
        self.register_plugin('xep_0060')
146
        self.register_plugin('xep_0066')
147
        self.register_plugin('xep_0070')
148
        self.register_plugin('xep_0071')
149 150
        self.register_plugin('xep_0077')
        self.plugin['xep_0077'].create_account = False
151
        self.register_plugin('xep_0084')
152
        self.register_plugin('xep_0085')
153
        self.register_plugin('xep_0153')
154

155 156 157
        # monkey-patch xep_0184 to avoid requesting receipts for messages
        # without a body
        XEP_0184._filter_add_receipt_request = fixes._filter_add_receipt_request
158
        self.register_plugin('xep_0184')
159
        self.plugin['xep_0184'].auto_ack = config.get('ack_message_receipts')
mathieui's avatar
mathieui committed
160 161
        self.plugin['xep_0184'].auto_request = config.get(
            'request_message_receipts')
162

163
        self.register_plugin('xep_0191')
mathieui's avatar
mathieui committed
164 165
        if config.get('enable_smacks'):
            self.register_plugin('xep_0198')
166
        self.register_plugin('xep_0199')
167

168
        if config.get('enable_user_nick'):
mathieui's avatar
mathieui committed
169
            self.register_plugin('xep_0172')
mathieui's avatar
mathieui committed
170

171
        if config.get('send_poezio_info'):
172
            info = {'name': 'poezio', 'version': options.custom_version}
173
            if config.get('send_os_info'):
174
                info['os'] = common.get_os_info()
mathieui's avatar
mathieui committed
175 176 177 178
            self.plugin['xep_0030'].set_identities(identities={('client',
                                                                'console',
                                                                None,
                                                                'Poezio')})
179 180
        else:
            info = {'name': '', 'version': ''}
mathieui's avatar
mathieui committed
181 182 183
            self.plugin['xep_0030'].set_identities(identities={('client',
                                                                'console',
                                                                None, '')})
184
        self.register_plugin('xep_0092', pconfig=info)
185
        if config.get('send_time'):
186
            self.register_plugin('xep_0202')
187
        self.register_plugin('xep_0224')
188
        self.register_plugin('xep_0231')
189
        self.register_plugin('xep_0249')
mathieui's avatar
mathieui committed
190
        self.register_plugin('xep_0257')
mathieui's avatar
mathieui committed
191 192
        self.register_plugin('xep_0280')
        self.register_plugin('xep_0297')
Link Mauve's avatar
Link Mauve committed
193
        self.register_plugin('xep_0308')
Madhur Garg's avatar
Madhur Garg committed
194
        self.register_plugin('xep_0313')
195
        self.register_plugin('xep_0319')
196
        self.register_plugin('xep_0334')
mathieui's avatar
mathieui committed
197
        self.register_plugin('xep_0352')
Link Mauve's avatar
Link Mauve committed
198 199 200 201 202 203 204 205
        try:
            self.register_plugin('xep_0363')
        except SyntaxError:
            log.error('Failed to load HTTP File Upload plugin, it can only be '
                      'used on Python 3.5+')
        except slixmpp.plugins.base.PluginNotFound:
            log.error('Failed to load HTTP File Upload plugin, it can only be '
                      'used with aiohttp installed')
206
        self.register_plugin('xep_0380')
louiz’'s avatar
louiz’ committed
207
        self.init_plugins()
208

209 210
    def set_keepalive_values(self, option=None, value=None):
        """
211
        Called after the XMPP session has been started, or triggered when one of
212
        "connection_timeout_delay" and "connection_check_interval" options
213
        is changed.  Unload and reload the ping plugin, with the new values.
214
        """
215 216 217 218
        if not self.is_connected():
            # Happens when we change the value with /set while we are not
            # connected. Do nothing in that case
            return
219 220
        ping_interval = config.get('connection_check_interval')
        timeout_delay = config.get('connection_timeout_delay')
221
        if timeout_delay <= 0:
222 223 224 225 226
            # We help the stupid user (with a delay of 0, poezio will try to
            # reconnect immediately because the timeout is immediately
            # passed)
            # 1 second is short, but, well
            timeout_delay = 1
227
        self.plugin['xep_0199'].disable_keepalive()
228 229
        # If the ping_interval is 0 or less, we just disable the keepalive
        if ping_interval > 0:
230 231
            self.plugin['xep_0199'].enable_keepalive(ping_interval,
                                                     timeout_delay)
232

233
    def start(self):
234 235 236
        """
        Connect and process events.
        """
237
        custom_host = config.get('custom_host')
238
        custom_port = config.get('custom_port', 5222)
239 240
        if custom_port == -1:
            custom_port = 5222
241
        if custom_host:
louiz’'s avatar
louiz’ committed
242
            self.connect((custom_host, custom_port))
mathieui's avatar
mathieui committed
243
        elif custom_port != 5222 and custom_port != -1:
louiz’'s avatar
louiz’ committed
244
            self.connect((self.boundjid.host, custom_port))
245
        else:
louiz’'s avatar
louiz’ committed
246
            self.connect()
247

louiz’'s avatar
louiz’ committed
248
    def send_raw(self, data):
249 250 251 252
        """
        Overrides XMLStream.send_raw, with an event added
        """
        if self.core:
253
            self.core.handler.outgoing_stanza(data)
louiz’'s avatar
louiz’ committed
254
        slixmpp.ClientXMPP.send_raw(self, data)
255

mathieui's avatar
mathieui committed
256

louiz’'s avatar
louiz’ committed
257
class MatchAll(slixmpp.xmlstream.matcher.base.MatcherBase):
mathieui's avatar
mathieui committed
258 259 260
    """
    Callback to retrieve all the stanzas for the XML tab
    """
mathieui's avatar
mathieui committed
261

262
    def match(self, xml):
263
        "match everything"
264
        return True