PageRenderTime 46ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/xmpp/core.py

https://github.com/mor1/python-xmpp-server
Python | 461 lines | 400 code | 43 blank | 18 comment | 9 complexity | b73f921d1d5dd7776a26cf7bf6f6836d MD5 | raw file
  1. ## Copyright (c) 2010, Coptix, Inc. All rights reserved.
  2. ## See the LICENSE file for license terms and warranty disclaimer.
  3. """core -- xmpp core <http://xmpp.org/rfcs/rfc3920.html>"""
  4. from __future__ import absolute_import
  5. import time
  6. from . import state, xml, xmppstream, features, interfaces as i
  7. from .prelude import *
  8. try:
  9. import ssl
  10. except ImportError:
  11. ssl = None
  12. __all__ = (
  13. 'ServerCore', 'ClientCore',
  14. 'ReceivedOpenStream', 'ReceivedCloseStream',
  15. 'SentOpenStream', 'SentCloseStream', 'StreamClosed'
  16. )
  17. class Core(i.CoreInterface):
  18. def __init__(self, stream, jid, features=None, plugins=None, lang='en'):
  19. self.stream = stream.read(self._read)
  20. self.serverJID = jid
  21. self.lang = lang
  22. self.state = state.State(self, plugins)
  23. self.parser = xml.Parser(xmppstream.XMPPTarget(self)).start()
  24. self.E = xml.ElementMaker(namespace=self.__xmlns__, nsmap=self.nsmap)
  25. self.install_features(features)
  26. self._reset()
  27. def __repr__(self):
  28. peer = self.stream.socket and self.stream.socket.getpeername()
  29. return '<%s %r>' % (type(self).__name__, peer)
  30. __xmlns__ = property(lambda s: s.state.plugins.__xmlns__)
  31. nsmap = property(lambda s: s.state.plugins.nsmap)
  32. ### ---------- Stream Interaction ----------
  33. def initiate(self):
  34. """Initiate a stream after a reset."""
  35. def listen(self):
  36. """Bind important events and stanzas after a reset."""
  37. (self.state
  38. .bind_stanza(self.ERROR, self.handle_stream_error)
  39. .bind_stanza('{jabber:client}iq', self.info_query)
  40. .one(features.StreamSecured, self.on_stream_secured)
  41. .one(features.StreamAuthorized, self.on_stream_authorized)
  42. .one(features.StreamBound, self.on_stream_bound))
  43. return self
  44. def activate(self):
  45. """Default plugin activation is done after basic Features have
  46. been negotiated."""
  47. self.parser.stop_tokenizing()
  48. self.state.activate()
  49. return self
  50. def on_stream_secured(self, tls):
  51. self.secured = True
  52. def on_stream_authorized(self, auth):
  53. self.authJID = auth.jid
  54. def on_stream_bound(self, bindings):
  55. self.authJID = bindings.jid
  56. self.resources = bindings.resources
  57. def add_timeout(self, delay, callback):
  58. self.stream.io.add_timeout(time.time() + delay, callback)
  59. return self
  60. def remove_timeout(self, callback):
  61. self.stream.io.remove_timeout(callback)
  62. return self
  63. ### ---------- Incoming Stream ----------
  64. ## These are callbacks for the XMPPStream.
  65. def is_stanza(self, name):
  66. return self.state.is_stanza(name)
  67. def handle_open_stream(self, attr):
  68. self.state.trigger(ReceivedOpenStream, attr)
  69. def handle_stanza(self, elem):
  70. self.state.trigger_stanza(elem.tag, elem)
  71. def handle_close_stream(self):
  72. self.state.trigger(ReceivedCloseStream)
  73. self.close()
  74. ### ---------- Outgoing Stream ----------
  75. STREAM = '{http://etherx.jabber.org/streams}stream'
  76. LANG = '{http://www.w3.org/XML/1998/namespace}lang'
  77. @abc.abstractmethod
  78. def make_stream(self):
  79. """Create a <stream:stream> element."""
  80. def close(self):
  81. if self.stream:
  82. if self.root is not None:
  83. self.close_stream()
  84. self.state.run(self._close)
  85. def writer(method):
  86. """Push writes through the scheduled jobs queue."""
  87. @wraps(method)
  88. def queue_write(self, *args, **kwargs):
  89. self.state.run(method, self, *args, **kwargs)
  90. return self
  91. return queue_write
  92. @writer
  93. def write(self, data, *args):
  94. if xml.is_element(data):
  95. data = xml.stanza_tostring(self.root, data)
  96. self.stream.write(data, *args)
  97. @writer
  98. def open_stream(self, *args):
  99. if self.root is None:
  100. self.root = self.make_stream()
  101. self.stream.write(xml.open_tag(self.root), *args)
  102. self.state.trigger(SentOpenStream)
  103. @writer
  104. def reset(self):
  105. if self.stream:
  106. self._reset()
  107. @writer
  108. def close_stream(self, *args):
  109. if self.root is not None:
  110. self.stream.write(xml.close_tag(self.root), *args)
  111. self.root = None
  112. self.state.trigger(SentCloseStream)
  113. del writer
  114. ### ---------- Stream Errors ----------
  115. ERROR = '{http://etherx.jabber.org/streams}error'
  116. ERROR_NS = 'urn:ietf:params:xml:ns:xmpp-streams'
  117. TEXT = '{%s}text' % ERROR_NS
  118. def stream_error(self, name, text=None, exc=None):
  119. """Send a stream-level error and close the connection. Errors
  120. have this basic format:
  121. <stream:error>
  122. <{{ name }} xmlns="urn:ietf:params:xml:ns:xmpp-streams" />
  123. <text xml:lang="en" "urn:ietf:params:xml:ns:xmpp-streams">
  124. {{ text }}
  125. </text>
  126. </stream:error>
  127. See: <http://xmpp.org/rfcs/rfc3920.html#rfc.section.4.7.3>
  128. """
  129. try:
  130. log.error('Stream Error: %s %r' % (name, text), exc_info=bool(exc))
  131. with self.state.clear().lock():
  132. self.open_stream()
  133. elem = self.E(self.ERROR, self.E(name, xmlns=self.ERROR_NS))
  134. if text is not None:
  135. elem.append(self.E.text(
  136. { self.LANG: 'en', 'xmlns': self.ERROR_NS},
  137. text
  138. ))
  139. self.write(elem).close()
  140. except:
  141. log.exception('Exception while reporting stream error.')
  142. return self
  143. def handle_stream_error(self, elem):
  144. """Handle a stream-level error by logging it and closing the
  145. stream."""
  146. log.error('Received Error: %s %r' % (
  147. xml.tag(xml.child(elem, 0), 'unknown-error'),
  148. xml.text(xml.child(elem, self.TEXT), 'no description')
  149. ))
  150. with self.state.clear().lock():
  151. self.close()
  152. def stanza_error(self, elem, kind, condition, text=None):
  153. """Write a stanza-level error to the stream.
  154. <stanza-kind to='sender' type='error'>
  155. [RECOMMENDED to include sender XML here]
  156. <error type='error-type'>
  157. <defined-condition xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
  158. <text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'
  159. xml:lang='langcode'>
  160. OPTIONAL descriptive text
  161. </text>
  162. [OPTIONAL application-specific condition element]
  163. </error>
  164. </stanza-kind>
  165. """
  166. error = self.E.error(type=kind)
  167. error.append(self.E(condition, { 'xmlns': self.STANZAS }))
  168. if text:
  169. error.append(self.E.text({ 'xmlns': self.STANZAS }, text))
  170. stanza = self.E(elem.tag, {
  171. 'from': unicode(self.serverJID),
  172. 'type': 'error',
  173. 'id': elem.get('id')
  174. })
  175. if len(elem) > 0:
  176. stanza.append(elem[0])
  177. stanza.append(error)
  178. return self.write(stanza)
  179. ### ---------- Features ----------
  180. FEATURES = '{http://etherx.jabber.org/streams}features'
  181. def install_features(self, features=None):
  182. ## These track the results of core features: TLS, SASL, and
  183. ## Bind. They are updated by event listeners; see listen().
  184. self.secured = False
  185. self.authJID = None
  186. self.resources = None
  187. self.features = features.install(self.state) if features else ()
  188. return self
  189. def send_features(self):
  190. possible = self.features and self.features.include()
  191. self.write(self.E(self.FEATURES, *filter(xml.is_element, possible)))
  192. return self.authJID is None
  193. def wait_for_features(self):
  194. active = dict(self.features and self.features.active())
  195. self.state.bind_stanza(self.FEATURES, partial(self.negotiate, active))
  196. return self.authJID is None
  197. def negotiate(self, active, elem):
  198. stop_after_first = self.authJID is None
  199. for clause in elem:
  200. feature = active.get(clause.tag)
  201. if feature and feature.active():
  202. feature.reply(clause)
  203. if stop_after_first: break
  204. return self
  205. def use_tls(self):
  206. return bool(ssl and self.stream.socket)
  207. def starttls(self, callback, **options):
  208. self.stream.starttls(callback, **options)
  209. return self
  210. ### ---------- Core Stanzas ----------
  211. STANZAS = 'urn:ietf:params:xml:ns:xmpp-stanzas'
  212. def info_query(self, elem):
  213. if not self.authJID:
  214. return self.stream_error('not-authorized')
  215. kind = elem.get('type')
  216. if kind == 'error':
  217. log.exception('Unhandled stanza error %r.', xml.tostring(elem))
  218. return
  219. if kind == 'result':
  220. name = self.iq_ident(elem)
  221. else:
  222. child = xml.child(elem)
  223. if child is None:
  224. log.exception('No child element: %r.' % xml.tostring(elem))
  225. return self.stanza_error(
  226. elem, 'modify', 'not-acceptable',
  227. 'GET or SET must have a child element.'
  228. )
  229. name = '{jabber:client}iq/%s' % child.tag
  230. try:
  231. self.state.trigger_stanza(name, elem)
  232. except i.StreamError as exc:
  233. log.debug(
  234. 'Caught StreamError while dispatching %r.', name,
  235. exc_info=True
  236. )
  237. self.stanza_error(elem, 'cancel', 'feature-not-implemented')
  238. def iq(self, kind, elem_or_callback, *data):
  239. if xml.is_element(elem_or_callback):
  240. return self.iq_send(kind, elem_or_callback.get('id'), *data)
  241. return self.iq_send(kind, self.iq_bind(elem_or_callback), *data)
  242. def iq_bind(self, callback):
  243. ident = make_nonce()
  244. self.state.one_stanza(self.iq_ident(ident), callback, replace=False)
  245. return ident
  246. def iq_ident(self, ident):
  247. if xml.is_element(ident):
  248. ident = ident.get('id')
  249. return '{jabber:client}iq[id=%r]' % ident
  250. def iq_send(self, kind, ident, *data):
  251. return self.write(self.E.iq(
  252. { 'id': ident, 'type': kind },
  253. *data
  254. ))
  255. def routes(self, jid):
  256. if self.resources is None:
  257. raise state.NoRoute(jid)
  258. return self.resources.routes(xml.jid(jid))
  259. ### ---------- Private ----------
  260. def _read(self, data):
  261. if not self.stream:
  262. return
  263. try:
  264. self.parser.feed(data)
  265. except i.StreamError as exc:
  266. self.stream_error(exc.condition, exc.text, exc)
  267. except xml.XMLSyntaxError as exc:
  268. self.stream_error('bad-format', str(exc), exc)
  269. except Exception as exc:
  270. self.stream_error('internal-server-error', str(exc), exc)
  271. def _reset(self):
  272. self.state.reset()
  273. self.root = None
  274. self.listen()
  275. self.parser.reset()
  276. self.initiate()
  277. return self
  278. def _close(self):
  279. if self.stream:
  280. ## This causes a segfault when the stream is closed.
  281. ## self.parser.close()
  282. try:
  283. self.state.trigger(StreamClosed).clear()
  284. self.stream.shutdown()
  285. finally:
  286. self.stream = None
  287. ### Events
  288. class SentOpenStream(i.Event):
  289. """A <stream:stream> tag has been sent."""
  290. class SentCloseStream(i.Event):
  291. """A </stream:stream> tag has been sent."""
  292. class ReceivedOpenStream(i.Event):
  293. """A <stream:stream> tag has been received."""
  294. class ReceivedCloseStream(i.Event):
  295. """A </stream:stream> tag has been received."""
  296. class StreamClosed(i.Event):
  297. """Triggered just before a stream is shutdown."""
  298. ### Client
  299. class ClientCore(Core):
  300. ### ---------- Incoming Stream ----------
  301. def handle_open_stream(self, attr):
  302. self.id = attr.get('id')
  303. self.state.trigger(ReceivedOpenStream).run(self._opened)
  304. def _opened(self):
  305. self.state.one(features.SessionStarted, thunk(self.activate))
  306. self.wait_for_features()
  307. ### ---------- Outgoing Stream ----------
  308. def make_stream(self):
  309. return self.E(self.STREAM, {
  310. 'to': unicode(self.serverJID),
  311. self.LANG: self.lang,
  312. 'version': '1.0'
  313. })
  314. def initiate(self):
  315. self.open_stream()
  316. ### Server
  317. class ServerCore(Core):
  318. ### ---------- Incoming Stream ----------
  319. def handle_open_stream(self, attr):
  320. self.state.trigger(ReceivedOpenStream).run(self._opened)
  321. def _opened(self):
  322. self.open_stream()
  323. if not self.send_features():
  324. self.state.one(features.SessionStarted, thunk(self.activate))
  325. pass
  326. def handle_stanza(self, elem):
  327. if self.authJID:
  328. if not elem.get('from'):
  329. elem.set('from', unicode(self.authJID))
  330. self.state.trigger_stanza(elem.tag, elem)
  331. def handle_close_stream(self):
  332. self.state.trigger(ReceivedCloseStream)
  333. self.close()
  334. def close(self):
  335. if self.stream:
  336. if self.root is not None:
  337. self.close_stream(self._close)
  338. else:
  339. self.state.run(self._close)
  340. ### ---------- Outgoing Stream ----------
  341. def make_stream(self):
  342. self.id = make_nonce()
  343. return self.E(self.STREAM, {
  344. 'from': unicode(self.serverJID),
  345. 'id': self.id,
  346. self.LANG: self.lang,
  347. 'version': '1.0'
  348. })
  349. def make_nonce():
  350. import random, hashlib
  351. random.seed()
  352. return hashlib.md5(str(random.getrandbits(64))).hexdigest()