f49311ef9e
Certificate host names are now matched (using DNS, SRV, XMPPAddr, and Common Name), along with expiration check. Scheduled event to reset the stream once the server's cert expires. Handle invalid cert trust chains gracefully now.
162 lines
5.7 KiB
Python
162 lines
5.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
sleekxmpp.clientxmpp
|
|
~~~~~~~~~~~~~~~~~~~~
|
|
|
|
This module provides XMPP functionality that
|
|
is specific to external server component connections.
|
|
|
|
Part of SleekXMPP: The Sleek XMPP Library
|
|
|
|
:copyright: (c) 2011 Nathanael C. Fritz
|
|
:license: MIT, see LICENSE for more details
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
|
|
import logging
|
|
import sys
|
|
import hashlib
|
|
|
|
from sleekxmpp.basexmpp import BaseXMPP
|
|
from sleekxmpp.xmlstream import XMLStream
|
|
from sleekxmpp.xmlstream import ET
|
|
from sleekxmpp.xmlstream.matcher import MatchXPath
|
|
from sleekxmpp.xmlstream.handler import Callback
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class ComponentXMPP(BaseXMPP):
|
|
|
|
"""
|
|
SleekXMPP's basic XMPP server component.
|
|
|
|
Use only for good, not for evil.
|
|
|
|
:param jid: The JID of the component.
|
|
:param secret: The secret or password for the component.
|
|
:param host: The server accepting the component.
|
|
:param port: The port used to connect to the server.
|
|
:param plugin_config: A dictionary of plugin configurations.
|
|
:param plugin_whitelist: A list of approved plugins that
|
|
will be loaded when calling
|
|
:meth:`~sleekxmpp.basexmpp.BaseXMPP.register_plugins()`.
|
|
:param use_jc_ns: Indicates if the ``'jabber:client'`` namespace
|
|
should be used instead of the standard
|
|
``'jabber:component:accept'`` namespace.
|
|
Defaults to ``False``.
|
|
"""
|
|
|
|
def __init__(self, jid, secret, host=None, port=None,
|
|
plugin_config={}, plugin_whitelist=[], use_jc_ns=False):
|
|
if use_jc_ns:
|
|
default_ns = 'jabber:client'
|
|
else:
|
|
default_ns = 'jabber:component:accept'
|
|
BaseXMPP.__init__(self, jid, default_ns)
|
|
|
|
self.auto_authorize = None
|
|
self.stream_header = "<stream:stream %s %s to='%s'>" % (
|
|
'xmlns="jabber:component:accept"',
|
|
'xmlns:stream="%s"' % self.stream_ns,
|
|
jid)
|
|
self.stream_footer = "</stream:stream>"
|
|
self.server_host = host
|
|
self.server_port = port
|
|
self.secret = secret
|
|
|
|
self.plugin_config = plugin_config
|
|
self.plugin_whitelist = plugin_whitelist
|
|
self.is_component = True
|
|
|
|
self.register_handler(
|
|
Callback('Handshake',
|
|
MatchXPath('{jabber:component:accept}handshake'),
|
|
self._handle_handshake))
|
|
self.add_event_handler('presence_probe',
|
|
self._handle_probe)
|
|
|
|
def connect(self, host=None, port=None, use_ssl=False,
|
|
use_tls=True, reattempt=True):
|
|
"""Connect to the server.
|
|
|
|
Setting ``reattempt`` to ``True`` will cause connection attempts to
|
|
be made every second until a successful connection is established.
|
|
|
|
:param host: The name of the desired server for the connection.
|
|
Defaults to :attr:`server_host`.
|
|
:param port: Port to connect to on the server.
|
|
Defauts to :attr:`server_port`.
|
|
:param use_ssl: Flag indicating if SSL should be used by connecting
|
|
directly to a port using SSL.
|
|
:param use_tls: Flag indicating if TLS should be used, allowing for
|
|
connecting to a port without using SSL immediately and
|
|
later upgrading the connection.
|
|
:param reattempt: Flag indicating if the socket should reconnect
|
|
after disconnections.
|
|
"""
|
|
if host is None:
|
|
host = self.server_host
|
|
if port is None:
|
|
port = self.server_port
|
|
|
|
self.server_name = self.boundjid.host
|
|
|
|
log.debug("Connecting to %s:%s", host, port)
|
|
return XMLStream.connect(self, host=host, port=port,
|
|
use_ssl=use_ssl,
|
|
use_tls=use_tls,
|
|
reattempt=reattempt)
|
|
|
|
def incoming_filter(self, xml):
|
|
"""
|
|
Pre-process incoming XML stanzas by converting any
|
|
``'jabber:client'`` namespaced elements to the component's
|
|
default namespace.
|
|
|
|
:param xml: The XML stanza to pre-process.
|
|
"""
|
|
if xml.tag.startswith('{jabber:client}'):
|
|
xml.tag = xml.tag.replace('jabber:client', self.default_ns)
|
|
|
|
# The incoming_filter call is only made on top level stanza
|
|
# elements. So we manually continue filtering on sub-elements.
|
|
for sub in xml:
|
|
self.incoming_filter(sub)
|
|
|
|
return xml
|
|
|
|
def start_stream_handler(self, xml):
|
|
"""
|
|
Once the streams are established, attempt to handshake
|
|
with the server to be accepted as a component.
|
|
|
|
:param xml: The incoming stream's root element.
|
|
"""
|
|
BaseXMPP.start_stream_handler(self, xml)
|
|
|
|
# Construct a hash of the stream ID and the component secret.
|
|
sid = xml.get('id', '')
|
|
pre_hash = '%s%s' % (sid, self.secret)
|
|
if sys.version_info >= (3, 0):
|
|
# Handle Unicode byte encoding in Python 3.
|
|
pre_hash = bytes(pre_hash, 'utf-8')
|
|
|
|
handshake = ET.Element('{jabber:component:accept}handshake')
|
|
handshake.text = hashlib.sha1(pre_hash).hexdigest().lower()
|
|
self.send_xml(handshake, now=True)
|
|
|
|
def _handle_handshake(self, xml):
|
|
"""The handshake has been accepted.
|
|
|
|
:param xml: The reply handshake stanza.
|
|
"""
|
|
self.session_started_event.set()
|
|
self.event("session_start")
|
|
|
|
def _handle_probe(self, presence):
|
|
pto = presence['to'].bare
|
|
pfrom = presence['from'].bare
|
|
self.roster[pto][pfrom].handle_probe(presence)
|