PageRenderTime 22ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/pyxmpp/error.py

http://github.com/Jajcus/pyxmpp
Python | 537 lines | 471 code | 13 blank | 53 comment | 19 complexity | 0de12f938ba06f8bc1f3af2520ca381e MD5 | raw file
Possible License(s): LGPL-2.1
  1. #
  2. # (C) Copyright 2003-2010 Jacek Konieczny <jajcus@jajcus.net>
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU Lesser General Public License Version
  6. # 2.1 as published by the Free Software Foundation.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU Lesser General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU Lesser General Public
  14. # License along with this program; if not, write to the Free Software
  15. # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
  16. #
  17. """XMPP error handling.
  18. Normative reference:
  19. - `RFC 3920 <http://www.ietf.org/rfc/rfc3920.txt>`__
  20. - `JEP 86 <http://www.jabber.org/jeps/jep-0086.html>`__
  21. """
  22. __docformat__="restructuredtext en"
  23. import libxml2
  24. from pyxmpp.utils import from_utf8, to_utf8
  25. from pyxmpp.xmlextra import common_doc, common_root, common_ns
  26. from pyxmpp import xmlextra
  27. from pyxmpp.exceptions import ProtocolError
  28. stream_errors={
  29. u"bad-format":
  30. ("Received XML cannot be processed",),
  31. u"bad-namespace-prefix":
  32. ("Bad namespace prefix",),
  33. u"conflict":
  34. ("Closing stream because of conflicting stream being opened",),
  35. u"connection-timeout":
  36. ("Connection was idle too long",),
  37. u"host-gone":
  38. ("Hostname is no longer hosted on the server",),
  39. u"host-unknown":
  40. ("Hostname requested is not known to the server",),
  41. u"improper-addressing":
  42. ("Improper addressing",),
  43. u"internal-server-error":
  44. ("Internal server error",),
  45. u"invalid-from":
  46. ("Invalid sender address",),
  47. u"invalid-id":
  48. ("Invalid stream ID",),
  49. u"invalid-namespace":
  50. ("Invalid namespace",),
  51. u"invalid-xml":
  52. ("Invalid XML",),
  53. u"not-authorized":
  54. ("Not authorized",),
  55. u"policy-violation":
  56. ("Local policy violation",),
  57. u"remote-connection-failed":
  58. ("Remote connection failed",),
  59. u"resource-constraint":
  60. ("Remote connection failed",),
  61. u"restricted-xml":
  62. ("Restricted XML received",),
  63. u"see-other-host":
  64. ("Redirection required",),
  65. u"system-shutdown":
  66. ("The server is being shut down",),
  67. u"undefined-condition":
  68. ("Unknown error",),
  69. u"unsupported-encoding":
  70. ("Unsupported encoding",),
  71. u"unsupported-stanza-type":
  72. ("Unsupported stanza type",),
  73. u"unsupported-version":
  74. ("Unsupported protocol version",),
  75. u"xml-not-well-formed":
  76. ("XML sent by client is not well formed",),
  77. }
  78. stanza_errors={
  79. u"bad-request":
  80. ("Bad request",
  81. "modify",400),
  82. u"conflict":
  83. ("Named session or resource already exists",
  84. "cancel",409),
  85. u"feature-not-implemented":
  86. ("Feature requested is not implemented",
  87. "cancel",501),
  88. u"forbidden":
  89. ("You are forbidden to perform requested action",
  90. "auth",403),
  91. u"gone":
  92. ("Recipient or server can no longer be contacted at this address",
  93. "modify",302),
  94. u"internal-server-error":
  95. ("Internal server error",
  96. "wait",500),
  97. u"item-not-found":
  98. ("Item not found"
  99. ,"cancel",404),
  100. u"jid-malformed":
  101. ("JID malformed",
  102. "modify",400),
  103. u"not-acceptable":
  104. ("Requested action is not acceptable",
  105. "modify",406),
  106. u"not-allowed":
  107. ("Requested action is not allowed",
  108. "cancel",405),
  109. u"not-authorized":
  110. ("Not authorized",
  111. "auth",401),
  112. u"payment-required":
  113. ("Payment required",
  114. "auth",402),
  115. u"recipient-unavailable":
  116. ("Recipient is not available",
  117. "wait",404),
  118. u"redirect":
  119. ("Redirection",
  120. "modify",302),
  121. u"registration-required":
  122. ("Registration required",
  123. "auth",407),
  124. u"remote-server-not-found":
  125. ("Remote server not found",
  126. "cancel",404),
  127. u"remote-server-timeout":
  128. ("Remote server timeout",
  129. "wait",504),
  130. u"resource-constraint":
  131. ("Resource constraint",
  132. "wait",500),
  133. u"service-unavailable":
  134. ("Service is not available",
  135. "cancel",503),
  136. u"subscription-required":
  137. ("Subscription is required",
  138. "auth",407),
  139. u"undefined-condition":
  140. ("Unknown error",
  141. "cancel",500),
  142. u"unexpected-request":
  143. ("Unexpected request",
  144. "wait",400),
  145. }
  146. legacy_codes={
  147. 302: "redirect",
  148. 400: "bad-request",
  149. 401: "not-authorized",
  150. 402: "payment-required",
  151. 403: "forbidden",
  152. 404: "item-not-found",
  153. 405: "not-allowed",
  154. 406: "not-acceptable",
  155. 407: "registration-required",
  156. 408: "remote-server-timeout",
  157. 409: "conflict",
  158. 500: "internal-server-error",
  159. 501: "feature-not-implemented",
  160. 502: "service-unavailable",
  161. 503: "service-unavailable",
  162. 504: "remote-server-timeout",
  163. 510: "service-unavailable",
  164. }
  165. STANZA_ERROR_NS='urn:ietf:params:xml:ns:xmpp-stanzas'
  166. STREAM_ERROR_NS='urn:ietf:params:xml:ns:xmpp-streams'
  167. PYXMPP_ERROR_NS='http://pyxmpp.jajcus.net/xmlns/errors'
  168. STREAM_NS="http://etherx.jabber.org/streams"
  169. class ErrorNode:
  170. """Base class for both XMPP stream and stanza errors"""
  171. def __init__(self,xmlnode_or_cond,ns=None,copy=True,parent=None):
  172. """Initialize an ErrorNode object.
  173. :Parameters:
  174. - `xmlnode_or_cond`: XML node to be wrapped into this object
  175. or error condition name.
  176. - `ns`: XML namespace URI of the error condition element (to be
  177. used when the provided node has no namespace).
  178. - `copy`: When `True` then the XML node will be copied,
  179. otherwise it is only borrowed.
  180. - `parent`: Parent node for the XML node to be copied or created.
  181. :Types:
  182. - `xmlnode_or_cond`: `libxml2.xmlNode` or `unicode`
  183. - `ns`: `unicode`
  184. - `copy`: `bool`
  185. - `parent`: `libxml2.xmlNode`"""
  186. if type(xmlnode_or_cond) is str:
  187. xmlnode_or_cond=unicode(xmlnode_or_cond,"utf-8")
  188. self.xmlnode=None
  189. self.borrowed=0
  190. if isinstance(xmlnode_or_cond,libxml2.xmlNode):
  191. self.__from_xml(xmlnode_or_cond,ns,copy,parent)
  192. elif isinstance(xmlnode_or_cond,ErrorNode):
  193. if not copy:
  194. raise TypeError, "ErrorNodes may only be copied"
  195. self.ns=from_utf8(xmlnode_or_cond.ns.getContent())
  196. self.xmlnode=xmlnode_or_cond.xmlnode.docCopyNode(common_doc,1)
  197. if not parent:
  198. parent=common_root
  199. parent.addChild(self.xmlnode)
  200. elif ns is None:
  201. raise ValueError, "Condition namespace not given"
  202. else:
  203. if parent:
  204. self.xmlnode=parent.newChild(common_ns,"error",None)
  205. self.borrowed=1
  206. else:
  207. self.xmlnode=common_root.newChild(common_ns,"error",None)
  208. cond=self.xmlnode.newChild(None,to_utf8(xmlnode_or_cond),None)
  209. ns=cond.newNs(ns,None)
  210. cond.setNs(ns)
  211. self.ns=from_utf8(ns.getContent())
  212. def __from_xml(self,xmlnode,ns,copy,parent):
  213. """Initialize an ErrorNode object from an XML node.
  214. :Parameters:
  215. - `xmlnode`: XML node to be wrapped into this object.
  216. - `ns`: XML namespace URI of the error condition element (to be
  217. used when the provided node has no namespace).
  218. - `copy`: When `True` then the XML node will be copied,
  219. otherwise it is only borrowed.
  220. - `parent`: Parent node for the XML node to be copied or created.
  221. :Types:
  222. - `xmlnode`: `libxml2.xmlNode`
  223. - `ns`: `unicode`
  224. - `copy`: `bool`
  225. - `parent`: `libxml2.xmlNode`"""
  226. if not ns:
  227. ns=None
  228. c=xmlnode.children
  229. while c:
  230. ns=c.ns().getContent()
  231. if ns in (STREAM_ERROR_NS,STANZA_ERROR_NS):
  232. break
  233. ns=None
  234. c=c.next
  235. if ns==None:
  236. raise ProtocolError, "Bad error namespace"
  237. self.ns=from_utf8(ns)
  238. if copy:
  239. self.xmlnode=xmlnode.docCopyNode(common_doc,1)
  240. if not parent:
  241. parent=common_root
  242. parent.addChild(self.xmlnode)
  243. else:
  244. self.xmlnode=xmlnode
  245. self.borrowed=1
  246. if copy:
  247. ns1=xmlnode.ns()
  248. xmlextra.replace_ns(self.xmlnode, ns1, common_ns)
  249. def __del__(self):
  250. if self.xmlnode:
  251. self.free()
  252. def free(self):
  253. """Free the associated XML node."""
  254. if not self.borrowed:
  255. self.xmlnode.unlinkNode()
  256. self.xmlnode.freeNode()
  257. self.xmlnode=None
  258. def free_borrowed(self):
  259. """Free the associated "borrowed" XML node."""
  260. self.xmlnode=None
  261. def is_legacy(self):
  262. """Check if the error node is a legacy error element.
  263. :return: `True` if it is a legacy error.
  264. :returntype: `bool`"""
  265. return not self.xmlnode.hasProp("type")
  266. def xpath_eval(self,expr,namespaces=None):
  267. """Evaluate XPath expression on the error element.
  268. The expression will be evaluated in context where the common namespace
  269. (the one used for stanza elements, mapped to 'jabber:client',
  270. 'jabber:server', etc.) is bound to prefix "ns" and other namespaces are
  271. bound accordingly to the `namespaces` list.
  272. :Parameters:
  273. - `expr`: the XPath expression.
  274. - `namespaces`: prefix to namespace mapping.
  275. :Types:
  276. - `expr`: `unicode`
  277. - `namespaces`: `dict`
  278. :return: the result of the expression evaluation.
  279. """
  280. ctxt = common_doc.xpathNewContext()
  281. ctxt.setContextNode(self.xmlnode)
  282. ctxt.xpathRegisterNs("ns",to_utf8(self.ns))
  283. if namespaces:
  284. for prefix,uri in namespaces.items():
  285. ctxt.xpathRegisterNs(prefix,uri)
  286. ret=ctxt.xpathEval(expr)
  287. ctxt.xpathFreeContext()
  288. return ret
  289. def get_condition(self,ns=None):
  290. """Get the condition element of the error.
  291. :Parameters:
  292. - `ns`: namespace URI of the condition element if it is not
  293. the XMPP namespace of the error element.
  294. :Types:
  295. - `ns`: `unicode`
  296. :return: the condition element or `None`.
  297. :returntype: `libxml2.xmlNode`"""
  298. if ns is None:
  299. ns=self.ns
  300. c=self.xpath_eval("ns:*")
  301. if not c:
  302. self.upgrade()
  303. c=self.xpath_eval("ns:*")
  304. if not c:
  305. return None
  306. if ns==self.ns and c[0].name=="text":
  307. if len(c)==1:
  308. return None
  309. c=c[1:]
  310. return c[0]
  311. def get_text(self):
  312. """Get the description text from the error element.
  313. :return: the text provided with the error or `None`.
  314. :returntype: `unicode`"""
  315. c=self.xpath_eval("ns:*")
  316. if not c:
  317. self.upgrade()
  318. t=self.xpath_eval("ns:text")
  319. if not t:
  320. return None
  321. return from_utf8(t[0].getContent())
  322. def add_custom_condition(self,ns,cond,content=None):
  323. """Add custom condition element to the error.
  324. :Parameters:
  325. - `ns`: namespace URI.
  326. - `cond`: condition name.
  327. - `content`: content of the element.
  328. :Types:
  329. - `ns`: `unicode`
  330. - `cond`: `unicode`
  331. - `content`: `unicode`
  332. :return: the new condition element.
  333. :returntype: `libxml2.xmlNode`"""
  334. c=self.xmlnode.newTextChild(None,to_utf8(cond),content)
  335. ns=c.newNs(to_utf8(ns),None)
  336. c.setNs(ns)
  337. return c
  338. def upgrade(self):
  339. """Upgrade a legacy error element to the XMPP compliant one.
  340. Use the error code provided to select the condition and the
  341. <error/> CDATA for the error text."""
  342. if not self.xmlnode.hasProp("code"):
  343. code=None
  344. else:
  345. try:
  346. code=int(self.xmlnode.prop("code"))
  347. except (ValueError,KeyError):
  348. code=None
  349. if code and legacy_codes.has_key(code):
  350. cond=legacy_codes[code]
  351. else:
  352. cond=None
  353. condition=self.xpath_eval("ns:*")
  354. if condition:
  355. return
  356. elif cond is None:
  357. condition=self.xmlnode.newChild(None,"undefined-condition",None)
  358. ns=condition.newNs(to_utf8(self.ns),None)
  359. condition.setNs(ns)
  360. condition=self.xmlnode.newChild(None,"unknown-legacy-error",None)
  361. ns=condition.newNs(PYXMPP_ERROR_NS,None)
  362. condition.setNs(ns)
  363. else:
  364. condition=self.xmlnode.newChild(None,cond,None)
  365. ns=condition.newNs(to_utf8(self.ns),None)
  366. condition.setNs(ns)
  367. txt=self.xmlnode.getContent()
  368. if txt:
  369. text=self.xmlnode.newTextChild(None,"text",txt)
  370. ns=text.newNs(to_utf8(self.ns),None)
  371. text.setNs(ns)
  372. def downgrade(self):
  373. """Downgrade an XMPP error element to the legacy format.
  374. Add a numeric code attribute according to the condition name."""
  375. if self.xmlnode.hasProp("code"):
  376. return
  377. cond=self.get_condition()
  378. if not cond:
  379. return
  380. cond=cond.name
  381. if stanza_errors.has_key(cond) and stanza_errors[cond][2]:
  382. self.xmlnode.setProp("code",to_utf8(stanza_errors[cond][2]))
  383. def serialize(self):
  384. """Serialize the element node.
  385. :return: serialized element in UTF-8 encoding.
  386. :returntype: `str`"""
  387. return self.xmlnode.serialize(encoding="utf-8")
  388. class StreamErrorNode(ErrorNode):
  389. """Stream error element."""
  390. def __init__(self,xmlnode_or_cond,copy=1,parent=None):
  391. """Initialize a StreamErrorNode object.
  392. :Parameters:
  393. - `xmlnode_or_cond`: XML node to be wrapped into this object
  394. or the primary (defined by XMPP specification) error condition name.
  395. - `copy`: When `True` then the XML node will be copied,
  396. otherwise it is only borrowed.
  397. - `parent`: Parent node for the XML node to be copied or created.
  398. :Types:
  399. - `xmlnode_or_cond`: `libxml2.xmlNode` or `unicode`
  400. - `copy`: `bool`
  401. - `parent`: `libxml2.xmlNode`"""
  402. if type(xmlnode_or_cond) is str:
  403. xmlnode_or_cond = xmlnode_or_cond.decode("utf-8")
  404. if type(xmlnode_or_cond) is unicode:
  405. if not stream_errors.has_key(xmlnode_or_cond):
  406. raise ValueError, "Bad error condition"
  407. ErrorNode.__init__(self,xmlnode_or_cond,STREAM_ERROR_NS,copy=copy,parent=parent)
  408. def get_message(self):
  409. """Get the message for the error.
  410. :return: the error message.
  411. :returntype: `unicode`"""
  412. cond=self.get_condition()
  413. if not cond:
  414. self.upgrade()
  415. cond=self.get_condition()
  416. if not cond:
  417. return None
  418. cond=cond.name
  419. if not stream_errors.has_key(cond):
  420. return None
  421. return stream_errors[cond][0]
  422. class StanzaErrorNode(ErrorNode):
  423. """Stanza error element."""
  424. def __init__(self,xmlnode_or_cond,error_type=None,copy=1,parent=None):
  425. """Initialize a StreamErrorNode object.
  426. :Parameters:
  427. - `xmlnode_or_cond`: XML node to be wrapped into this object
  428. or the primary (defined by XMPP specification) error condition name.
  429. - `error_type`: type of the error, one of: 'cancel', 'continue',
  430. 'modify', 'auth', 'wait'.
  431. - `copy`: When `True` then the XML node will be copied,
  432. otherwise it is only borrowed.
  433. - `parent`: Parent node for the XML node to be copied or created.
  434. :Types:
  435. - `xmlnode_or_cond`: `libxml2.xmlNode` or `unicode`
  436. - `error_type`: `unicode`
  437. - `copy`: `bool`
  438. - `parent`: `libxml2.xmlNode`"""
  439. if type(xmlnode_or_cond) is str:
  440. xmlnode_or_cond=unicode(xmlnode_or_cond,"utf-8")
  441. if type(xmlnode_or_cond) is unicode:
  442. if not stanza_errors.has_key(xmlnode_or_cond):
  443. raise ValueError, "Bad error condition"
  444. ErrorNode.__init__(self,xmlnode_or_cond,STANZA_ERROR_NS,copy=copy,parent=parent)
  445. if type(xmlnode_or_cond) is unicode:
  446. if error_type is None:
  447. error_type=stanza_errors[xmlnode_or_cond][1]
  448. self.xmlnode.setProp("type",to_utf8(error_type))
  449. def get_type(self):
  450. """Get the error type.
  451. :return: type of the error.
  452. :returntype: `unicode`"""
  453. if not self.xmlnode.hasProp("type"):
  454. self.upgrade()
  455. return from_utf8(self.xmlnode.prop("type"))
  456. def upgrade(self):
  457. """Upgrade a legacy error element to the XMPP compliant one.
  458. Use the error code provided to select the condition and the
  459. <error/> CDATA for the error text."""
  460. ErrorNode.upgrade(self)
  461. if self.xmlnode.hasProp("type"):
  462. return
  463. cond=self.get_condition().name
  464. if stanza_errors.has_key(cond):
  465. typ=stanza_errors[cond][1]
  466. self.xmlnode.setProp("type",typ)
  467. def get_message(self):
  468. """Get the message for the error.
  469. :return: the error message.
  470. :returntype: `unicode`"""
  471. cond=self.get_condition()
  472. if not cond:
  473. self.upgrade()
  474. cond=self.get_condition()
  475. if not cond:
  476. return None
  477. cond=cond.name
  478. if not stanza_errors.has_key(cond):
  479. return None
  480. return stanza_errors[cond][0]
  481. # vi: sts=4 et sw=4