summaryrefslogtreecommitdiff
path: root/sleekxmpp/componentxmpp.py
blob: 1fb9c4d9c3bd8070c37c9f006a4d4fcebe1aa514 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# -*- 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=False, 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)