/usr/lib/python3/dist-packages/sleekxmpp/clientxmpp.py is in python3-sleekxmpp 1.0~beta5-1.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
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 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 | """
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from __future__ import absolute_import, unicode_literals
import logging
import base64
import sys
import hashlib
import random
import threading
from sleekxmpp import plugins
from sleekxmpp import stanza
from sleekxmpp.basexmpp import BaseXMPP
from sleekxmpp.stanza import Message, Presence, Iq
from sleekxmpp.xmlstream import XMLStream, RestartStream
from sleekxmpp.xmlstream import StanzaBase, ET
from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.handler import *
# Flag indicating if DNS SRV records are available for use.
SRV_SUPPORT = True
try:
import dns.resolver
except:
SRV_SUPPORT = False
log = logging.getLogger(__name__)
class ClientXMPP(BaseXMPP):
"""
SleekXMPP's client class.
Use only for good, not for evil.
Attributes:
Methods:
connect -- Overrides XMLStream.connect.
del_roster_item -- Delete a roster item.
get_roster -- Retrieve the roster from the server.
register_feature -- Register a stream feature.
update_roster -- Update a roster item.
"""
def __init__(self, jid, password, ssl=False, plugin_config={},
plugin_whitelist=[], escape_quotes=True):
"""
Create a new SleekXMPP client.
Arguments:
jid -- The JID of the XMPP user account.
password -- The password for the XMPP user account.
ssl -- Deprecated.
plugin_config -- A dictionary of plugin configurations.
plugin_whitelist -- A list of approved plugins that will be loaded
when calling register_plugins.
escape_quotes -- Deprecated.
"""
BaseXMPP.__init__(self, 'jabber:client')
self.set_jid(jid)
self.password = password
self.escape_quotes = escape_quotes
self.plugin_config = plugin_config
self.plugin_whitelist = plugin_whitelist
self.srv_support = SRV_SUPPORT
self.stream_header = "<stream:stream to='%s' %s %s version='1.0'>" % (
self.boundjid.host,
"xmlns:stream='%s'" % self.stream_ns,
"xmlns='%s'" % self.default_ns)
self.stream_footer = "</stream:stream>"
self.features = []
self.registered_features = []
#TODO: Use stream state here
self.authenticated = False
self.sessionstarted = False
self.bound = False
self.bindfail = False
self.add_event_handler('connected', self.handle_connected)
self.register_handler(
Callback('Stream Features',
MatchXPath('{%s}features' % self.stream_ns),
self._handle_stream_features))
self.register_handler(
Callback('Roster Update',
MatchXPath('{%s}iq/{%s}query' % (
self.default_ns,
'jabber:iq:roster')),
self._handle_roster))
self.register_feature(
"<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />",
self._handle_starttls, True)
self.register_feature(
"<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />",
self._handle_sasl_auth, True)
self.register_feature(
"<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />",
self._handle_bind_resource)
self.register_feature(
"<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />",
self._handle_start_session)
def handle_connected(self, event=None):
#TODO: Use stream state here
self.authenticated = False
self.sessionstarted = False
self.bound = False
self.bindfail = False
self.schedule("session timeout checker", 15,
self._session_timeout_check)
def _session_timeout_check(self):
if not self.session_started_event.isSet():
log.debug("Session start has taken more than 15 seconds")
self.disconnect(reconnect=self.auto_reconnect)
def connect(self, address=tuple(), reattempt=True, use_tls=True):
"""
Connect to the XMPP server.
When no address is given, a SRV lookup for the server will
be attempted. If that fails, the server user in the JID
will be used.
Arguments:
address -- A tuple containing the server's host and port.
reattempt -- If True, reattempt the connection if an
error occurs. Defaults to True.
use_tls -- Indicates if TLS should be used for the
connection. Defaults to True.
"""
self.session_started_event.clear()
if not address or len(address) < 2:
if not self.srv_support:
log.debug("Did not supply (address, port) to connect" + \
" to and no SRV support is installed" + \
" (http://www.dnspython.org)." + \
" Continuing to attempt connection, using" + \
" server hostname from JID.")
else:
log.debug("Since no address is supplied," + \
"attempting SRV lookup.")
try:
xmpp_srv = "_xmpp-client._tcp.%s" % self.boundjid.host
answers = dns.resolver.query(xmpp_srv, dns.rdatatype.SRV)
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
log.debug("No appropriate SRV record found." + \
" Using JID server name.")
except (dns.exception.Timeout,):
log.debug("DNS resolution timed out.")
else:
# Pick a random server, weighted by priority.
addresses = {}
intmax = 0
for answer in answers:
intmax += answer.priority
addresses[intmax] = (answer.target.to_text()[:-1],
answer.port)
#python3 returns a generator for dictionary keys
priorities = [x for x in addresses.keys()]
priorities.sort()
picked = random.randint(0, intmax)
for priority in priorities:
if picked <= priority:
address = addresses[priority]
break
if not address:
# If all else fails, use the server from the JID.
address = (self.boundjid.host, 5222)
return XMLStream.connect(self, address[0], address[1],
use_tls=use_tls, reattempt=reattempt)
def register_feature(self, mask, pointer, breaker=False):
"""
Register a stream feature.
Arguments:
mask -- An XML string matching the feature's element.
pointer -- The function to execute if the feature is received.
breaker -- Indicates if feature processing should halt with
this feature. Defaults to False.
"""
self.registered_features.append((MatchXMLMask(mask),
pointer,
breaker))
def update_roster(self, jid, name=None, subscription=None, groups=[],
block=True, timeout=None, callback=None):
"""
Add or change a roster item.
Arguments:
jid -- The JID of the entry to modify.
name -- The user's nickname for this JID.
subscription -- The subscription status. May be one of
'to', 'from', 'both', or 'none'. If set
to 'remove', the entry will be deleted.
groups -- The roster groups that contain this item.
block -- Specify if the roster request will block
until a response is received, or a timeout
occurs. Defaults to True.
timeout -- The length of time (in seconds) to wait
for a response before continuing if blocking
is used. Defaults to self.response_timeout.
callback -- Optional reference to a stream handler function.
Will be executed when the roster is received.
Implies block=False.
"""
iq = self.Iq()
iq['type'] = 'set'
iq['roster']['items'] = {jid: {'name': name,
'subscription': subscription,
'groups': groups}}
response = iq.send(block, timeout, callback)
if response in [False, None] or not isinstance(response, Iq):
return response
return response['type'] == 'result'
def del_roster_item(self, jid):
"""
Remove an item from the roster by setting its subscription
status to 'remove'.
Arguments:
jid -- The JID of the item to remove.
"""
return self.update_roster(jid, subscription='remove')
def get_roster(self, block=True, timeout=None, callback=None):
"""
Request the roster from the server.
Arguments:
block -- Specify if the roster request will block until a
response is received, or a timeout occurs.
Defaults to True.
timeout -- The length of time (in seconds) to wait for a response
before continuing if blocking is used.
Defaults to self.response_timeout.
callback -- Optional reference to a stream handler function. Will
be executed when the roster is received.
Implies block=False.
"""
iq = self.Iq()
iq['type'] = 'get'
iq.enable('roster')
response = iq.send(block, timeout, callback)
if response == False:
self.event('roster_timeout')
if response in [False, None] or not isinstance(response, Iq):
return response
else:
return self._handle_roster(response, request=True)
def _handle_stream_features(self, features):
"""
Process the received stream features.
Arguments:
features -- The features stanza.
"""
# Record all of the features.
self.features = []
for sub in features.xml:
self.features.append(sub.tag)
# Process the features.
for sub in features.xml:
for feature in self.registered_features:
mask, handler, halt = feature
if mask.match(sub):
if handler(sub) and halt:
# Don't continue if the feature was
# marked as a breaker.
return True
def _handle_starttls(self, xml):
"""
Handle notification that the server supports TLS.
Arguments:
xml -- The STARTLS proceed element.
"""
if not self.use_tls:
return False
elif not self.authenticated and self.ssl_support:
tls_ns = 'urn:ietf:params:xml:ns:xmpp-tls'
self.add_handler("<proceed xmlns='%s' />" % tls_ns,
self._handle_tls_start,
name='TLS Proceed',
instream=True)
self.send_xml(xml, now=True)
return True
else:
log.warning("The module tlslite is required to log in" +\
" to some servers, and has not been found.")
return False
def _handle_tls_start(self, xml):
"""
Handle encrypting the stream using TLS.
Restarts the stream.
"""
log.debug("Starting TLS")
if self.start_tls():
raise RestartStream()
def _handle_sasl_auth(self, xml):
"""
Handle authenticating using SASL.
Arguments:
xml -- The SASL mechanisms stanza.
"""
if self.use_tls and \
'{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features:
return False
log.debug("Starting SASL Auth")
sasl_ns = 'urn:ietf:params:xml:ns:xmpp-sasl'
self.add_handler("<success xmlns='%s' />" % sasl_ns,
self._handle_auth_success,
name='SASL Sucess',
instream=True)
self.add_handler("<failure xmlns='%s' />" % sasl_ns,
self._handle_auth_fail,
name='SASL Failure',
instream=True)
sasl_mechs = xml.findall('{%s}mechanism' % sasl_ns)
if sasl_mechs:
for sasl_mech in sasl_mechs:
self.features.append("sasl:%s" % sasl_mech.text)
if 'sasl:PLAIN' in self.features and self.boundjid.user:
if sys.version_info < (3, 0):
user = bytes(self.boundjid.user)
password = bytes(self.password)
else:
user = bytes(self.boundjid.user, 'utf-8')
password = bytes(self.password, 'utf-8')
auth = base64.b64encode(b'\x00' + user + \
b'\x00' + password).decode('utf-8')
self.send("<auth xmlns='%s' mechanism='PLAIN'>%s</auth>" % (
sasl_ns,
auth),
now=True)
elif 'sasl:ANONYMOUS' in self.features and not self.boundjid.user:
self.send("<auth xmlns='%s' mechanism='%s' />" % (
sasl_ns,
'ANONYMOUS'),
now=True)
else:
log.error("No appropriate login method.")
self.disconnect()
return True
def _handle_auth_success(self, xml):
"""
SASL authentication succeeded. Restart the stream.
Arguments:
xml -- The SASL authentication success element.
"""
self.authenticated = True
self.features = []
raise RestartStream()
def _handle_auth_fail(self, xml):
"""
SASL authentication failed. Disconnect and shutdown.
Arguments:
xml -- The SASL authentication failure element.
"""
log.info("Authentication failed.")
self.event("failed_auth", direct=True)
self.disconnect()
def _handle_bind_resource(self, xml):
"""
Handle requesting a specific resource.
Arguments:
xml -- The bind feature element.
"""
log.debug("Requesting resource: %s" % self.boundjid.resource)
xml.clear()
iq = self.Iq(stype='set')
if self.boundjid.resource:
res = ET.Element('resource')
res.text = self.boundjid.resource
xml.append(res)
iq.append(xml)
response = iq.send(now=True)
bind_ns = 'urn:ietf:params:xml:ns:xmpp-bind'
self.set_jid(response.xml.find('{%s}bind/{%s}jid' % (bind_ns,
bind_ns)).text)
self.bound = True
log.info("Node set to: %s" % self.boundjid.full)
session_ns = 'urn:ietf:params:xml:ns:xmpp-session'
if "{%s}session" % session_ns not in self.features or self.bindfail:
log.debug("Established Session")
self.sessionstarted = True
self.session_started_event.set()
self.event("session_start")
def _handle_start_session(self, xml):
"""
Handle the start of the session.
Arguments:
xml -- The session feature element.
"""
if self.authenticated and self.bound:
iq = self.makeIqSet(xml)
response = iq.send(now=True)
log.debug("Established Session")
self.sessionstarted = True
self.session_started_event.set()
self.event("session_start")
else:
# Bind probably hasn't happened yet.
self.bindfail = True
def _handle_roster(self, iq, request=False):
"""
Update the roster after receiving a roster stanza.
Arguments:
iq -- The roster stanza.
request -- Indicates if this stanza is a response
to a request for the roster.
"""
if iq['type'] == 'set' or (iq['type'] == 'result' and request):
for jid in iq['roster']['items']:
if not jid in self.roster:
self.roster[jid] = {'groups': [],
'name': '',
'subscription': 'none',
'presence': {},
'in_roster': True}
self.roster[jid].update(iq['roster']['items'][jid])
self.event('roster_received', iq)
self.event("roster_update", iq)
if iq['type'] == 'set':
iq.reply()
iq.enable('roster')
iq.send()
return True
# To comply with PEP8, method names now use underscores.
# Deprecated method names are re-mapped for backwards compatibility.
ClientXMPP.updateRoster = ClientXMPP.update_roster
ClientXMPP.delRosterItem = ClientXMPP.del_roster_item
ClientXMPP.getRoster = ClientXMPP.get_roster
ClientXMPP.registerFeature = ClientXMPP.register_feature
|