pyxmpp-1.1.2/0000755000175000017500000000000011561501465013516 5ustar jajcususers00000000000000pyxmpp-1.1.2/COPYING0000644000175000017500000006347511560014012014552 0ustar jajcususers00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! pyxmpp-1.1.2/utils/0000755000175000017500000000000011561501465014656 5ustar jajcususers00000000000000pyxmpp-1.1.2/utils/migrate-0_5-0_6.py0000755000175000017500000000264111560025462017626 0ustar jajcususers00000000000000#!/usr/bin/python import sys import re import os args=sys.argv[1:] if not args or "-h" in args or "--help" in args: print "PyXMPP 0.5 to 0.6 code updater." print "Usage:" print " %s file..." % (sys.argv[0],) print print "This script will try to update your code for the recent changes" print "in the PyXMPP package. But this updates are just simple regexp" print "substitutions which may _break_ your code. Always check the result." sys.exit(0) in_par=r"(?:\([^)]*\)|[^()])" updates=[ (r"(\b(?:Muc)?(?:Stanza|Message|Iq|Presence)\("+in_par+r"*)\bfr=("+in_par+r"+\))",r"\1from_jid=\2"), (r"(\b(?:Muc)?(?:Stanza|Message|Iq|Presence)\("+in_par+r"*)\bto=("+in_par+r"+\))",r"\1to_jid=\2"), (r"(\b(?:Muc)?(?:Stanza|Message|Iq|Presence)\("+in_par+r"*)\btype?=("+in_par+r"+\))",r"\1stanza_type=\2"), (r"(\b(?:Muc)?(?:Stanza|Message|Iq|Presence)\("+in_par+r"*)\bs?id=("+in_par+r"+\))",r"\1stanza_id=\2"), ] updates=[(re.compile(u_re,re.MULTILINE|re.DOTALL),u_repl) for u_re,u_repl in updates] for fn in args: print fn+":", orig_code=file(fn).read() changes_made=0 code=orig_code for u_re,u_repl in updates: (code,cm)=u_re.subn(u_repl,code) changes_made+=cm if changes_made: print changes_made,"changes" os.rename(fn,fn+".bak") file(fn,"w").write(code) else: print "no changes" # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/0000755000175000017500000000000011561501464015052 5ustar jajcususers00000000000000pyxmpp-1.1.2/pyxmpp/error.py0000644000175000017500000004523711560025462016566 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """XMPP error handling. Normative reference: - `RFC 3920 `__ - `JEP 86 `__ """ __docformat__="restructuredtext en" import libxml2 from pyxmpp.utils import from_utf8, to_utf8 from pyxmpp.xmlextra import common_doc, common_root, common_ns from pyxmpp import xmlextra from pyxmpp.exceptions import ProtocolError stream_errors={ u"bad-format": ("Received XML cannot be processed",), u"bad-namespace-prefix": ("Bad namespace prefix",), u"conflict": ("Closing stream because of conflicting stream being opened",), u"connection-timeout": ("Connection was idle too long",), u"host-gone": ("Hostname is no longer hosted on the server",), u"host-unknown": ("Hostname requested is not known to the server",), u"improper-addressing": ("Improper addressing",), u"internal-server-error": ("Internal server error",), u"invalid-from": ("Invalid sender address",), u"invalid-id": ("Invalid stream ID",), u"invalid-namespace": ("Invalid namespace",), u"invalid-xml": ("Invalid XML",), u"not-authorized": ("Not authorized",), u"policy-violation": ("Local policy violation",), u"remote-connection-failed": ("Remote connection failed",), u"resource-constraint": ("Remote connection failed",), u"restricted-xml": ("Restricted XML received",), u"see-other-host": ("Redirection required",), u"system-shutdown": ("The server is being shut down",), u"undefined-condition": ("Unknown error",), u"unsupported-encoding": ("Unsupported encoding",), u"unsupported-stanza-type": ("Unsupported stanza type",), u"unsupported-version": ("Unsupported protocol version",), u"xml-not-well-formed": ("XML sent by client is not well formed",), } stanza_errors={ u"bad-request": ("Bad request", "modify",400), u"conflict": ("Named session or resource already exists", "cancel",409), u"feature-not-implemented": ("Feature requested is not implemented", "cancel",501), u"forbidden": ("You are forbidden to perform requested action", "auth",403), u"gone": ("Recipient or server can no longer be contacted at this address", "modify",302), u"internal-server-error": ("Internal server error", "wait",500), u"item-not-found": ("Item not found" ,"cancel",404), u"jid-malformed": ("JID malformed", "modify",400), u"not-acceptable": ("Requested action is not acceptable", "modify",406), u"not-allowed": ("Requested action is not allowed", "cancel",405), u"not-authorized": ("Not authorized", "auth",401), u"payment-required": ("Payment required", "auth",402), u"recipient-unavailable": ("Recipient is not available", "wait",404), u"redirect": ("Redirection", "modify",302), u"registration-required": ("Registration required", "auth",407), u"remote-server-not-found": ("Remote server not found", "cancel",404), u"remote-server-timeout": ("Remote server timeout", "wait",504), u"resource-constraint": ("Resource constraint", "wait",500), u"service-unavailable": ("Service is not available", "cancel",503), u"subscription-required": ("Subscription is required", "auth",407), u"undefined-condition": ("Unknown error", "cancel",500), u"unexpected-request": ("Unexpected request", "wait",400), } legacy_codes={ 302: "redirect", 400: "bad-request", 401: "not-authorized", 402: "payment-required", 403: "forbidden", 404: "item-not-found", 405: "not-allowed", 406: "not-acceptable", 407: "registration-required", 408: "remote-server-timeout", 409: "conflict", 500: "internal-server-error", 501: "feature-not-implemented", 502: "service-unavailable", 503: "service-unavailable", 504: "remote-server-timeout", 510: "service-unavailable", } STANZA_ERROR_NS='urn:ietf:params:xml:ns:xmpp-stanzas' STREAM_ERROR_NS='urn:ietf:params:xml:ns:xmpp-streams' PYXMPP_ERROR_NS='http://pyxmpp.jajcus.net/xmlns/errors' STREAM_NS="http://etherx.jabber.org/streams" class ErrorNode: """Base class for both XMPP stream and stanza errors""" def __init__(self,xmlnode_or_cond,ns=None,copy=True,parent=None): """Initialize an ErrorNode object. :Parameters: - `xmlnode_or_cond`: XML node to be wrapped into this object or error condition name. - `ns`: XML namespace URI of the error condition element (to be used when the provided node has no namespace). - `copy`: When `True` then the XML node will be copied, otherwise it is only borrowed. - `parent`: Parent node for the XML node to be copied or created. :Types: - `xmlnode_or_cond`: `libxml2.xmlNode` or `unicode` - `ns`: `unicode` - `copy`: `bool` - `parent`: `libxml2.xmlNode`""" if type(xmlnode_or_cond) is str: xmlnode_or_cond=unicode(xmlnode_or_cond,"utf-8") self.xmlnode=None self.borrowed=0 if isinstance(xmlnode_or_cond,libxml2.xmlNode): self.__from_xml(xmlnode_or_cond,ns,copy,parent) elif isinstance(xmlnode_or_cond,ErrorNode): if not copy: raise TypeError, "ErrorNodes may only be copied" self.ns=from_utf8(xmlnode_or_cond.ns.getContent()) self.xmlnode=xmlnode_or_cond.xmlnode.docCopyNode(common_doc,1) if not parent: parent=common_root parent.addChild(self.xmlnode) elif ns is None: raise ValueError, "Condition namespace not given" else: if parent: self.xmlnode=parent.newChild(common_ns,"error",None) self.borrowed=1 else: self.xmlnode=common_root.newChild(common_ns,"error",None) cond=self.xmlnode.newChild(None,to_utf8(xmlnode_or_cond),None) ns=cond.newNs(ns,None) cond.setNs(ns) self.ns=from_utf8(ns.getContent()) def __from_xml(self,xmlnode,ns,copy,parent): """Initialize an ErrorNode object from an XML node. :Parameters: - `xmlnode`: XML node to be wrapped into this object. - `ns`: XML namespace URI of the error condition element (to be used when the provided node has no namespace). - `copy`: When `True` then the XML node will be copied, otherwise it is only borrowed. - `parent`: Parent node for the XML node to be copied or created. :Types: - `xmlnode`: `libxml2.xmlNode` - `ns`: `unicode` - `copy`: `bool` - `parent`: `libxml2.xmlNode`""" if not ns: ns=None c=xmlnode.children while c: ns=c.ns().getContent() if ns in (STREAM_ERROR_NS,STANZA_ERROR_NS): break ns=None c=c.next if ns==None: raise ProtocolError, "Bad error namespace" self.ns=from_utf8(ns) if copy: self.xmlnode=xmlnode.docCopyNode(common_doc,1) if not parent: parent=common_root parent.addChild(self.xmlnode) else: self.xmlnode=xmlnode self.borrowed=1 if copy: ns1=xmlnode.ns() xmlextra.replace_ns(self.xmlnode, ns1, common_ns) def __del__(self): if self.xmlnode: self.free() def free(self): """Free the associated XML node.""" if not self.borrowed: self.xmlnode.unlinkNode() self.xmlnode.freeNode() self.xmlnode=None def free_borrowed(self): """Free the associated "borrowed" XML node.""" self.xmlnode=None def is_legacy(self): """Check if the error node is a legacy error element. :return: `True` if it is a legacy error. :returntype: `bool`""" return not self.xmlnode.hasProp("type") def xpath_eval(self,expr,namespaces=None): """Evaluate XPath expression on the error element. The expression will be evaluated in context where the common namespace (the one used for stanza elements, mapped to 'jabber:client', 'jabber:server', etc.) is bound to prefix "ns" and other namespaces are bound accordingly to the `namespaces` list. :Parameters: - `expr`: the XPath expression. - `namespaces`: prefix to namespace mapping. :Types: - `expr`: `unicode` - `namespaces`: `dict` :return: the result of the expression evaluation. """ ctxt = common_doc.xpathNewContext() ctxt.setContextNode(self.xmlnode) ctxt.xpathRegisterNs("ns",to_utf8(self.ns)) if namespaces: for prefix,uri in namespaces.items(): ctxt.xpathRegisterNs(prefix,uri) ret=ctxt.xpathEval(expr) ctxt.xpathFreeContext() return ret def get_condition(self,ns=None): """Get the condition element of the error. :Parameters: - `ns`: namespace URI of the condition element if it is not the XMPP namespace of the error element. :Types: - `ns`: `unicode` :return: the condition element or `None`. :returntype: `libxml2.xmlNode`""" if ns is None: ns=self.ns c=self.xpath_eval("ns:*") if not c: self.upgrade() c=self.xpath_eval("ns:*") if not c: return None if ns==self.ns and c[0].name=="text": if len(c)==1: return None c=c[1:] return c[0] def get_text(self): """Get the description text from the error element. :return: the text provided with the error or `None`. :returntype: `unicode`""" c=self.xpath_eval("ns:*") if not c: self.upgrade() t=self.xpath_eval("ns:text") if not t: return None return from_utf8(t[0].getContent()) def add_custom_condition(self,ns,cond,content=None): """Add custom condition element to the error. :Parameters: - `ns`: namespace URI. - `cond`: condition name. - `content`: content of the element. :Types: - `ns`: `unicode` - `cond`: `unicode` - `content`: `unicode` :return: the new condition element. :returntype: `libxml2.xmlNode`""" c=self.xmlnode.newTextChild(None,to_utf8(cond),content) ns=c.newNs(to_utf8(ns),None) c.setNs(ns) return c def upgrade(self): """Upgrade a legacy error element to the XMPP compliant one. Use the error code provided to select the condition and the CDATA for the error text.""" if not self.xmlnode.hasProp("code"): code=None else: try: code=int(self.xmlnode.prop("code")) except (ValueError,KeyError): code=None if code and legacy_codes.has_key(code): cond=legacy_codes[code] else: cond=None condition=self.xpath_eval("ns:*") if condition: return elif cond is None: condition=self.xmlnode.newChild(None,"undefined-condition",None) ns=condition.newNs(to_utf8(self.ns),None) condition.setNs(ns) condition=self.xmlnode.newChild(None,"unknown-legacy-error",None) ns=condition.newNs(PYXMPP_ERROR_NS,None) condition.setNs(ns) else: condition=self.xmlnode.newChild(None,cond,None) ns=condition.newNs(to_utf8(self.ns),None) condition.setNs(ns) txt=self.xmlnode.getContent() if txt: text=self.xmlnode.newTextChild(None,"text",txt) ns=text.newNs(to_utf8(self.ns),None) text.setNs(ns) def downgrade(self): """Downgrade an XMPP error element to the legacy format. Add a numeric code attribute according to the condition name.""" if self.xmlnode.hasProp("code"): return cond=self.get_condition() if not cond: return cond=cond.name if stanza_errors.has_key(cond) and stanza_errors[cond][2]: self.xmlnode.setProp("code",to_utf8(stanza_errors[cond][2])) def serialize(self): """Serialize the element node. :return: serialized element in UTF-8 encoding. :returntype: `str`""" return self.xmlnode.serialize(encoding="utf-8") class StreamErrorNode(ErrorNode): """Stream error element.""" def __init__(self,xmlnode_or_cond,copy=1,parent=None): """Initialize a StreamErrorNode object. :Parameters: - `xmlnode_or_cond`: XML node to be wrapped into this object or the primary (defined by XMPP specification) error condition name. - `copy`: When `True` then the XML node will be copied, otherwise it is only borrowed. - `parent`: Parent node for the XML node to be copied or created. :Types: - `xmlnode_or_cond`: `libxml2.xmlNode` or `unicode` - `copy`: `bool` - `parent`: `libxml2.xmlNode`""" if type(xmlnode_or_cond) is str: xmlnode_or_cond = xmlnode_or_cond.decode("utf-8") if type(xmlnode_or_cond) is unicode: if not stream_errors.has_key(xmlnode_or_cond): raise ValueError, "Bad error condition" ErrorNode.__init__(self,xmlnode_or_cond,STREAM_ERROR_NS,copy=copy,parent=parent) def get_message(self): """Get the message for the error. :return: the error message. :returntype: `unicode`""" cond=self.get_condition() if not cond: self.upgrade() cond=self.get_condition() if not cond: return None cond=cond.name if not stream_errors.has_key(cond): return None return stream_errors[cond][0] class StanzaErrorNode(ErrorNode): """Stanza error element.""" def __init__(self,xmlnode_or_cond,error_type=None,copy=1,parent=None): """Initialize a StreamErrorNode object. :Parameters: - `xmlnode_or_cond`: XML node to be wrapped into this object or the primary (defined by XMPP specification) error condition name. - `error_type`: type of the error, one of: 'cancel', 'continue', 'modify', 'auth', 'wait'. - `copy`: When `True` then the XML node will be copied, otherwise it is only borrowed. - `parent`: Parent node for the XML node to be copied or created. :Types: - `xmlnode_or_cond`: `libxml2.xmlNode` or `unicode` - `error_type`: `unicode` - `copy`: `bool` - `parent`: `libxml2.xmlNode`""" if type(xmlnode_or_cond) is str: xmlnode_or_cond=unicode(xmlnode_or_cond,"utf-8") if type(xmlnode_or_cond) is unicode: if not stanza_errors.has_key(xmlnode_or_cond): raise ValueError, "Bad error condition" ErrorNode.__init__(self,xmlnode_or_cond,STANZA_ERROR_NS,copy=copy,parent=parent) if type(xmlnode_or_cond) is unicode: if error_type is None: error_type=stanza_errors[xmlnode_or_cond][1] self.xmlnode.setProp("type",to_utf8(error_type)) def get_type(self): """Get the error type. :return: type of the error. :returntype: `unicode`""" if not self.xmlnode.hasProp("type"): self.upgrade() return from_utf8(self.xmlnode.prop("type")) def upgrade(self): """Upgrade a legacy error element to the XMPP compliant one. Use the error code provided to select the condition and the CDATA for the error text.""" ErrorNode.upgrade(self) if self.xmlnode.hasProp("type"): return cond=self.get_condition().name if stanza_errors.has_key(cond): typ=stanza_errors[cond][1] self.xmlnode.setProp("type",typ) def get_message(self): """Get the message for the error. :return: the error message. :returntype: `unicode`""" cond=self.get_condition() if not cond: self.upgrade() cond=self.get_condition() if not cond: return None cond=cond.name if not stanza_errors.has_key(cond): return None return stanza_errors[cond][0] # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/client.py0000644000175000017500000004114011560025462016700 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Basic XMPP-IM client implementation. Normative reference: - `RFC 3921 `__ """ __docformat__="restructuredtext en" import threading import logging from pyxmpp.clientstream import ClientStream from pyxmpp.iq import Iq from pyxmpp.presence import Presence from pyxmpp.roster import Roster from pyxmpp.exceptions import ClientError, FatalClientError from pyxmpp.interfaces import IPresenceHandlersProvider, IMessageHandlersProvider from pyxmpp.interfaces import IIqHandlersProvider, IStanzaHandlersProvider class Client: """Base class for an XMPP-IM client. This class does not provide any JSF extensions to the XMPP protocol, including legacy authentication methods. :Ivariables: - `jid`: configured JID of the client (current actual JID is avialable as `self.stream.jid`). - `password`: authentication password. - `server`: server to use if non-standard and not discoverable by SRV lookups. - `port`: port number on the server to use if non-standard and not discoverable by SRV lookups. - `auth_methods`: methods allowed for stream authentication. SASL mechanism names should be preceded with "sasl:" prefix. - `keepalive`: keepalive interval for the stream or 0 when keepalive is disabled. - `stream`: current stream when the client is connected, `None` otherwise. - `roster`: user's roster or `None` if the roster is not yet retrieved. - `session_established`: `True` when an IM session is established. - `lock`: lock for synchronizing `Client` attributes access. - `state_changed`: condition notified the the object state changes (stream becomes connected, session established etc.). - `interface_providers`: list of object providing interfaces that could be used by the Client object. Initialized to [`self`] by the constructor if not set earlier. Put objects providing `IPresenceHandlersProvider`, `IMessageHandlersProvider`, `IIqHandlersProvider` or `IStanzaHandlersProvider` into this list. :Types: - `jid`: `pyxmpp.JID` - `password`: `unicode` - `server`: `unicode` - `port`: `int` - `auth_methods`: `list` of `str` - `keepalive`: `int` - `stream`: `pyxmpp.ClientStream` - `roster`: `pyxmpp.Roster` - `session_established`: `bool` - `lock`: `threading.RLock` - `state_changed`: `threading.Condition` - `interface_providers`: `list` """ def __init__(self,jid=None,password=None,server=None,port=5222, auth_methods=("sasl:DIGEST-MD5",), tls_settings=None,keepalive=0): """Initialize a Client object. :Parameters: - `jid`: user full JID for the connection. - `password`: user password. - `server`: server to use. If not given then address will be derived form the JID. - `port`: port number to use. If not given then address will be derived form the JID. - `auth_methods`: sallowed authentication methods. SASL authentication mechanisms in the list should be prefixed with "sasl:" string. - `tls_settings`: settings for StartTLS -- `TLSSettings` instance. - `keepalive`: keepalive output interval. 0 to disable. :Types: - `jid`: `pyxmpp.JID` - `password`: `unicode` - `server`: `unicode` - `port`: `int` - `auth_methods`: sequence of `str` - `tls_settings`: `pyxmpp.TLSSettings` - `keepalive`: `int` """ self.jid=jid self.password=password self.server=server self.port=port self.auth_methods=list(auth_methods) self.tls_settings=tls_settings self.keepalive=keepalive self.stream=None self.lock=threading.RLock() self.state_changed=threading.Condition(self.lock) self.session_established=False self.roster=None self.stream_class=ClientStream if not hasattr(self, "interface_providers"): self.interface_providers = [self] self.__logger=logging.getLogger("pyxmpp.Client") # public methods def connect(self, register = False): """Connect to the server and set up the stream. Set `self.stream` and notify `self.state_changed` when connection succeeds.""" if not self.jid: raise ClientError, "Cannot connect: no or bad JID given" self.lock.acquire() try: stream = self.stream self.stream = None if stream: stream.close() self.__logger.debug("Creating client stream: %r, auth_methods=%r" % (self.stream_class, self.auth_methods)) stream=self.stream_class(jid = self.jid, password = self.password, server = self.server, port = self.port, auth_methods = self.auth_methods, tls_settings = self.tls_settings, keepalive = self.keepalive, owner = self) stream.process_stream_error = self.stream_error self.stream_created(stream) stream.state_change = self.__stream_state_change stream.connect() self.stream = stream self.state_changed.notify() self.state_changed.release() except: self.stream = None self.state_changed.release() raise def get_stream(self): """Get the connected stream object. :return: stream object or `None` if the client is not connected. :returntype: `pyxmpp.ClientStream`""" self.lock.acquire() stream=self.stream self.lock.release() return stream def disconnect(self): """Disconnect from the server.""" stream=self.get_stream() if stream: stream.disconnect() def request_session(self): """Request an IM session.""" stream=self.get_stream() if not stream.version: need_session=False elif not stream.features: need_session=False else: ctxt = stream.doc_in.xpathNewContext() ctxt.setContextNode(stream.features) ctxt.xpathRegisterNs("sess","urn:ietf:params:xml:ns:xmpp-session") # jabberd2 hack ctxt.xpathRegisterNs("jsess","http://jabberd.jabberstudio.org/ns/session/1.0") sess_n=None try: sess_n=ctxt.xpathEval("sess:session or jsess:session") finally: ctxt.xpathFreeContext() if sess_n: need_session=True else: need_session=False if not need_session: self.state_changed.acquire() self.session_established=1 self.state_changed.notify() self.state_changed.release() self._session_started() else: iq=Iq(stanza_type="set") iq.new_query("urn:ietf:params:xml:ns:xmpp-session","session") stream.set_response_handlers(iq, self.__session_result,self.__session_error,self.__session_timeout) stream.send(iq) def request_roster(self): """Request the user's roster.""" stream=self.get_stream() iq=Iq(stanza_type="get") iq.new_query("jabber:iq:roster") stream.set_response_handlers(iq, self.__roster_result,self.__roster_error,self.__roster_timeout) stream.set_iq_set_handler("query","jabber:iq:roster",self.__roster_push) stream.send(iq) def get_socket(self): """Get the socket object of the active connection. :return: socket used by the stream. :returntype: `socket.socket`""" return self.stream.socket def loop(self,timeout=1): """Simple "main loop" for the client. By default just call the `pyxmpp.Stream.loop_iter` method of `self.stream`, which handles stream input and `self.idle` for some "housekeeping" work until the stream is closed. This usually will be replaced by something more sophisticated. E.g. handling of other input sources.""" while 1: stream=self.get_stream() if not stream: break act=stream.loop_iter(timeout) if not act: self.idle() # private methods def __session_timeout(self): """Process session request time out. :raise FatalClientError:""" raise FatalClientError("Timeout while tryin to establish a session") def __session_error(self,iq): """Process session request failure. :Parameters: - `iq`: IQ error stanza received as result of the session request. :Types: - `iq`: `pyxmpp.Iq` :raise FatalClientError:""" err=iq.get_error() msg=err.get_message() raise FatalClientError("Failed to establish a session: "+msg) def __session_result(self, _unused): """Process session request success. :Parameters: - `_unused`: IQ result stanza received in reply to the session request. :Types: - `_unused`: `pyxmpp.Iq`""" self.state_changed.acquire() self.session_established=True self.state_changed.notify() self.state_changed.release() self._session_started() def _session_started(self): """Called when session is started. Activates objects from `self.interface_provides` by installing their stanza handlers, etc.""" for ob in self.interface_providers: if IPresenceHandlersProvider.providedBy(ob): for handler_data in ob.get_presence_handlers(): self.stream.set_presence_handler(*handler_data) if IMessageHandlersProvider.providedBy(ob): for handler_data in ob.get_message_handlers(): self.stream.set_message_handler(*handler_data) if IIqHandlersProvider.providedBy(ob): for handler_data in ob.get_iq_get_handlers(): self.stream.set_iq_get_handler(*handler_data) for handler_data in ob.get_iq_set_handlers(): self.stream.set_iq_set_handler(*handler_data) self.session_started() def __roster_timeout(self): """Process roster request time out. :raise ClientError:""" raise ClientError("Timeout while tryin to retrieve roster") def __roster_error(self,iq): """Process roster request failure. :Parameters: - `iq`: IQ error stanza received as result of the roster request. :Types: - `iq`: `pyxmpp.Iq` :raise ClientError:""" err=iq.get_error() msg=err.get_message() raise ClientError("Roster retrieval failed: "+msg) def __roster_result(self,iq): """Process roster request success. :Parameters: - `iq`: IQ result stanza received in reply to the roster request. :Types: - `iq`: `pyxmpp.Iq`""" q=iq.get_query() if q: self.state_changed.acquire() self.roster=Roster(q) self.state_changed.notify() self.state_changed.release() self.roster_updated() else: raise ClientError("Roster retrieval failed") def __roster_push(self,iq): """Process a "roster push" (change notification) received. :Parameters: - `iq`: IQ result stanza received. :Types: - `iq`: `pyxmpp.Iq`""" fr=iq.get_from() if fr and fr != self.jid and fr != self.jid.bare(): resp=iq.make_error_response("forbidden") self.stream.send(resp) self.__logger.warning("Got roster update from wrong source") return if not self.roster: raise ClientError("Roster update, but no roster") q=iq.get_query() item=self.roster.update(q) if item: self.roster_updated(item) resp=iq.make_result_response() self.stream.send(resp) def __stream_state_change(self,state,arg): """Handle stream state changes. Call apopriate methods of self. :Parameters: - `state`: the new state. - `arg`: state change argument. :Types: - `state`: `str`""" self.stream_state_changed(state,arg) if state=="fully connected": self.connected() elif state=="authorized": self.authorized() elif state=="disconnected": self.state_changed.acquire() try: if self.stream: self.stream.close() self.stream_closed(self.stream) self.stream=None self.state_changed.notify() finally: self.state_changed.release() self.disconnected() # Method to override def idle(self): """Do some "housekeeping" work like cache expiration or timeout handling. Should be called periodically from the application main loop. May be overriden in derived classes.""" stream=self.get_stream() if stream: stream.idle() def stream_created(self,stream): """Handle stream creation event. May be overriden in derived classes. This one does nothing. :Parameters: - `stream`: the new stream. :Types: - `stream`: `pyxmpp.ClientStream`""" pass def stream_closed(self,stream): """Handle stream closure event. May be overriden in derived classes. This one does nothing. :Parameters: - `stream`: the new stream. :Types: - `stream`: `pyxmpp.ClientStream`""" pass def session_started(self): """Handle session started event. May be overriden in derived classes. This one requests the user's roster and sends the initial presence.""" self.request_roster() p=Presence() self.stream.send(p) def stream_error(self,err): """Handle stream error received. May be overriden in derived classes. This one passes an error messages to logging facilities. :Parameters: - `err`: the error element received. :Types: - `err`: `pyxmpp.error.StreamErrorNode`""" self.__logger.error("Stream error: condition: %s %r" % (err.get_condition().name,err.serialize())) def roster_updated(self,item=None): """Handle roster update event. May be overriden in derived classes. This one does nothing. :Parameters: - `item`: the roster item changed or `None` if whole roster was received. :Types: - `item`: `pyxmpp.RosterItem`""" pass def stream_state_changed(self,state,arg): """Handle any stream state change. May be overriden in derived classes. This one does nothing. :Parameters: - `state`: the new state. - `arg`: state change argument. :Types: - `state`: `str`""" pass def connected(self): """Handle "connected" event. May be overriden in derived classes. This one does nothing.""" pass def authenticated(self): """Handle "authenticated" event. May be overriden in derived classes. This one does nothing.""" pass def authorized(self): """Handle "authorized" event. May be overriden in derived classes. This one requests an IM session.""" self.request_session() def disconnected(self): """Handle "disconnected" event. May be overriden in derived classes. This one does nothing.""" pass # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/jid.py0000644000175000017500000002030211560025462016165 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # pylint: disable-msg=W0232, E0201 """jid -- Jabber ID handling Normative reference: - `RFC 3920 `__ """ __docformat__="restructuredtext en" import re import weakref import warnings from encodings import idna from pyxmpp.xmppstringprep import nodeprep,resourceprep from pyxmpp.exceptions import JIDError node_invalid_re=re.compile(ur"[" u'"' ur"&'/:<>@\s\x00-\x19]",re.UNICODE) resource_invalid_re=re.compile(ur"[\s\x00-\x19]",re.UNICODE) def are_domains_equal(a,b): """Compare two International Domain Names. :Parameters: - `a`,`b`: domains names to compare :return: True `a` and `b` are equal as domain names.""" a=idna.ToASCII(a) b=idna.ToASCII(b) return a.lower()==b.lower() class JID(object): """JID. :Ivariables: - `node`: node part of the JID - `domain`: domain part of the JID - `resource`: resource part of the JID JID objects are immutable. They are also cached for better performance. """ cache=weakref.WeakValueDictionary() __slots__=["node","domain","resource","__weakref__"] def __new__(cls,node_or_jid=None,domain=None,resource=None,check=True): """Create a new JID object or take one from the cache. :Parameters: - `node_or_jid`: node part of the JID, JID object to copy or Unicode representation of the JID. - `domain`: domain part of the JID - `resource`: resource part of the JID - `check`: if `False` then JID is not checked for specifiaction compliance. """ if isinstance(node_or_jid,JID): return node_or_jid if domain is None and resource is None: obj=cls.cache.get(node_or_jid) if obj: return obj else: obj=None if obj is None: obj=object.__new__(cls) if node_or_jid: node_or_jid = unicode(node_or_jid) if (node_or_jid and ((u"@" in node_or_jid) or (u"/" in node_or_jid))): obj.__from_unicode(node_or_jid) cls.cache[node_or_jid]=obj else: if domain is None and resource is None: if node_or_jid is None: raise JIDError,"At least domain must be given" domain=node_or_jid node_or_jid=None if check: obj.__set_node(node_or_jid) obj.__set_domain(domain) obj.__set_resource(resource) else: object.__setattr__(obj,"node",node_or_jid) object.__setattr__(obj,"domain",domain) object.__setattr__(obj,"resource",resource) return obj def __setattr__(self,name,value): raise RuntimeError,"JID objects are immutable!" def __from_unicode(self,s,check=True): """Initialize JID object from Unicode string. :Parameters: - `s`: the JID string - `check`: when `False` then the JID is not checked for specification compliance.""" s1=s.split(u"/",1) s2=s1[0].split(u"@",1) if len(s2)==2: if check: self.__set_node(s2[0]) self.__set_domain(s2[1]) else: object.__setattr__(self,"node",s2[0]) object.__setattr__(self,"domain",s2[1]) else: if check: self.__set_domain(s2[0]) else: object.__setattr__(self,"domain",s2[0]) object.__setattr__(self,"node",None) if len(s1)==2: if check: self.__set_resource(s1[1]) else: object.__setattr__(self,"resource",s1[1]) else: object.__setattr__(self,"resource",None) if not self.domain: raise JIDError,"Domain is required in JID." def __set_node(self,s): """Initialize `self.node` :Parameters: - `s`: Node part of the JID :Types: - `s`: unicode :raise JIDError: if the node name is too long. :raise pyxmpp.xmppstringprep.StringprepError: if the node name fails Nodeprep preparation.""" if s: s = unicode(s) s=nodeprep.prepare(s) if len(s.encode("utf-8"))>1023: raise JIDError,"Node name too long" else: s=None object.__setattr__(self,"node",s) def __set_domain(self,s): """Initialize `self.domain` :Parameters: - `s`: Unicode or UTF-8 domain part of the JID :raise JIDError: if the domain name is too long.""" if s is None: raise JIDError,"Domain must be given" if s: s = unicode(s) s=idna.nameprep(s) if len(s.encode("utf-8"))>1023: raise JIDError,"Domain name too long" object.__setattr__(self,"domain",s) def __set_resource(self,s): """Initialize `self.resource` :Parameters: - `s`: Unicode or UTF-8 resource part of the JID :raise JIDError: if the resource name is too long. :raise pyxmpp.xmppstringprep.StringprepError: if the node name fails Resourceprep preparation.""" if s: s = unicode(s) s=resourceprep.prepare(s) if len(s.encode("utf-8"))>1023: raise JIDError,"Resource name too long" else: s=None object.__setattr__(self,"resource",s) def __str__(self): warnings.warn("JIDs should not be used as strings", DeprecationWarning, stacklevel=2) return self.as_utf8() def __unicode__(self): return self.as_unicode() def __repr__(self): return "" % (self.as_unicode()) def as_utf8(self): """UTF-8 encoded JID representation. :return: UTF-8 encoded JID.""" return self.as_unicode().encode("utf-8") def as_string(self): """UTF-8 encoded JID representation. *Deprecated* Always use Unicode objects, or `as_utf8` if you really want. :return: UTF-8 encoded JID.""" warnings.warn("JID.as_string() is deprecated. Use unicode() or `as_utf8` instead.", DeprecationWarning, stacklevel=1) return self.as_utf8() def as_unicode(self): """Unicode string JID representation. :return: JID as Unicode string.""" r=self.domain if self.node: r=self.node+u'@'+r if self.resource: r=r+u'/'+self.resource if not JID.cache.has_key(r): JID.cache[r]=self return r def bare(self): """Make bare JID made by removing resource from current `self`. :return: new JID object without resource part.""" return JID(self.node,self.domain,check=False) def __eq__(self,other): if other is None: return False elif type(other) in (str, unicode): try: other=JID(other) except: return False elif not isinstance(other,JID): return False return (self.node==other.node and are_domains_equal(self.domain,other.domain) and self.resource==other.resource) def __ne__(self,other): return not self.__eq__(other) def __cmp__(self,other): a=self.as_unicode() return cmp(a,other) def __hash__(self): return hash(self.node)^hash(self.domain)^hash(self.resource) # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/streamtls.py0000644000175000017500000003316711561473656017467 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # pylint: disable-msg=W0201 """TLS support for XMPP streams. Normative reference: - `RFC 3920 `__ """ __docformat__="restructuredtext en" import socket import sys import errno import logging import ssl import warnings import inspect from ssl import SSLError from pyxmpp.streambase import StreamBase,STREAM_NS from pyxmpp.streambase import FatalStreamError,StreamEncryptionRequired from pyxmpp.exceptions import TLSNegotiationFailed, TLSError, TLSNegotiatedButNotAvailableError from pyxmpp.jid import JID TLS_NS="urn:ietf:params:xml:ns:xmpp-tls" tls_available = True def _count_args(callable): """Utility function to count expected arguments of a callable""" vc_args = inspect.getargspec(callable) count = len(vc_args.args) if not hasattr(callable, "im_self"): return count if callable.im_self is None: return count return count - 1 class TLSSettings: """Storage for TLS-related settings of an XMPP stream. :Ivariables: - `require`: is TLS required - `verify_peer`: should the peer's certificate be verified - `cert_file`: path to own X.509 certificate - `key_file`: path to the private key for own X.509 certificate - `cacert_file`: path to a file with trusted CA certificates - `verify_callback`: callback function for certificate verification.""" def __init__(self, require = False, verify_peer = True, cert_file = None, key_file = None, cacert_file = None, verify_callback = None, ctx = None): """Initialize the TLSSettings object. :Parameters: - `require`: is TLS required - `verify_peer`: should the peer's certificate be verified - `cert_file`: path to own X.509 certificate - `key_file`: path to the private key for own X.509 certificate - `cacert_file`: path to a file with trusted CA certificates - `verify_callback`: callback function for certificate verification. The callback function must accept a single argument: the certificate to verify, as returned by `ssl.SSLSocket.getpeercert()` and return True if a certificate is accepted. The verification callback should call Stream.tls_is_certificate_valid() to check if certificate subject name or alt subject name matches stream peer JID.""" if ctx is not None: warnings.warn("ctx argument of TLSSettings is deprecated", DeprecationWarning) self.require = require self.verify_peer = verify_peer self.cert_file = cert_file self.cacert_file = cacert_file self.key_file = key_file if verify_callback: if _count_args(verify_callback) > 1 : warnings.warn("Two-argument TLS verify callback is deprecated", DeprecationWarning) verify_callback = None self.verify_callback = verify_callback class StreamTLSMixIn: """Mix-in class providing TLS support for an XMPP stream. :Ivariables: - `tls`: TLS connection object. """ def __init__(self, tls_settings = None): """Initialize TLS support of a Stream object :Parameters: - `tls_settings`: settings for StartTLS. :Types: - `tls_settings`: `TLSSettings` """ self.tls_settings = tls_settings self.__logger = logging.getLogger("pyxmpp.StreamTLSMixIn") def _reset_tls(self): """Reset `StreamTLSMixIn` object state making it ready to handle new connections.""" self.tls = None self.tls_requested = False def _make_stream_tls_features(self, features): """Update the with StartTLS feature. [receving entity only] :Parameters: - `features`: the element of the stream. :Types: - `features`: `libxml2.xmlNode` :returns: updated element node. :returntype: `libxml2.xmlNode`""" if self.tls_settings and not self.tls: tls = features.newChild(None, "starttls", None) ns = tls.newNs(TLS_NS, None) tls.setNs(ns) if self.tls_settings.require: tls.newChild(None, "required", None) return features def _write_raw(self,data): """Same as `Stream.write_raw` but assume `self.lock` is acquired.""" logging.getLogger("pyxmpp.Stream.out").debug("OUT: %r",data) try: while self.socket: try: while data: sent = self.socket.send(data) data = data[sent:] except SSLError, err: if err.args[0] == ssl.SSL_ERROR_WANT_WRITE: continue raise break except (IOError, OSError, socket.error),e: raise FatalStreamError("IO Error: "+str(e)) except SSLError,e: raise TLSError("TLS Error: "+str(e)) def _read_tls(self): """Read data pending on the stream socket and pass it to the parser.""" if self.eof: return while self.socket: try: r = self.socket.read() # .recv() blocks in python 2.6.4 if r is None: return except SSLError, err: if err.args[0] == ssl.SSL_ERROR_WANT_READ: return raise except socket.error, err: if err.args[0] != errno.EINTR: raise return self._feed_reader(r) def _read(self): """Read data pending on the stream socket and pass it to the parser.""" self.__logger.debug("StreamTLSMixIn._read(), socket: %r",self.socket) if self.tls: self._read_tls() else: StreamBase._read(self) def _process(self): """Same as `Stream.process` but assume `self.lock` is acquired.""" try: StreamBase._process(self) except SSLError,e: self.close() raise TLSError("TLS Error: "+str(e)) def _process_node_tls(self,xmlnode): """Process incoming stream element. Pass it to _process_tls_node if it is in TLS namespace. :raise StreamEncryptionRequired: if encryption is required by current configuration, it is not active and the element is not in the TLS namespace nor in the stream namespace. :return: `True` when the node was recognized as TLS element. :returntype: `bool`""" ns_uri=xmlnode.ns().getContent() if ns_uri==STREAM_NS: return False elif ns_uri==TLS_NS: self._process_tls_node(xmlnode) return True if self.tls_settings and self.tls_settings.require and not self.tls: raise StreamEncryptionRequired,"TLS encryption required and not started yet" return False def _handle_tls_features(self): """Process incoming StartTLS related element of . [initiating entity only] The received features node is available in `self.features`.""" ctxt = self.doc_in.xpathNewContext() ctxt.setContextNode(self.features) ctxt.xpathRegisterNs("tls",TLS_NS) try: tls_n=ctxt.xpathEval("tls:starttls") tls_required_n=ctxt.xpathEval("tls:starttls/tls:required") finally: ctxt.xpathFreeContext() if not self.tls: if tls_required_n and not self.tls_settings: raise FatalStreamError,"StartTLS support disabled, but required by peer" if self.tls_settings and self.tls_settings.require and not tls_n: raise FatalStreamError,"StartTLS required, but not supported by peer" if self.tls_settings and tls_n: self.__logger.debug("StartTLS negotiated") if self.initiator: self._request_tls() else: self.__logger.debug("StartTLS not negotiated") def _request_tls(self): """Request a TLS-encrypted connection. [initiating entity only]""" self.tls_requested=1 self.features=None root=self.doc_out.getRootElement() xmlnode=root.newChild(None,"starttls",None) ns=xmlnode.newNs(TLS_NS,None) xmlnode.setNs(ns) self._write_raw(xmlnode.serialize(encoding="UTF-8")) xmlnode.unlinkNode() xmlnode.freeNode() def _process_tls_node(self,xmlnode): """Process stream element in the TLS namespace. :Parameters: - `xmlnode`: the XML node received """ if not self.tls_settings or not tls_available: self.__logger.debug("Unexpected TLS node: %r" % (xmlnode.serialize())) return False if self.initiator: if xmlnode.name=="failure": raise TLSNegotiationFailed,"Peer failed to initialize TLS connection" elif xmlnode.name!="proceed" or not self.tls_requested: self.__logger.debug("Unexpected TLS node: %r" % (xmlnode.serialize())) return False try: self.tls_requested=0 self._make_tls_connection() self.socket=self.tls except SSLError,e: self.tls=None raise TLSError("TLS Error: "+str(e)) self.__logger.debug("Restarting XMPP stream") self._restart_stream() return True else: raise FatalStreamError,"TLS not implemented for the receiving side yet" def _make_tls_connection(self): """Initiate TLS connection. [initiating entity only]""" if not tls_available or not self.tls_settings: raise TLSError,"TLS is not available" self.state_change("tls connecting",self.peer) if not self.tls_settings.verify_callback: self.tls_settings.verify_callback = self.tls_is_certificate_valid self.__logger.debug("tls_settings: {0!r}".format(self.tls_settings.__dict__)) self.__logger.debug("Creating TLS connection") if self.tls_settings.verify_peer: cert_reqs = ssl.CERT_REQUIRED else: cert_reqs = ssl.CERT_NONE self.tls = ssl.wrap_socket(self.socket, keyfile = self.tls_settings.key_file, certfile = self.tls_settings.cert_file, server_side = not self.initiator, cert_reqs = cert_reqs, ssl_version = ssl.PROTOCOL_TLSv1, ca_certs = self.tls_settings.cacert_file, do_handshake_on_connect = False, ) self.socket = None self.__logger.debug("Starting TLS handshake") self.tls.do_handshake() self.tls.setblocking(False) if self.tls_settings.verify_peer: valid = self.tls_settings.verify_callback(self.tls.getpeercert()) if not valid: raise SSLError, "Certificate verification failed" self.socket = self.tls self.state_change("tls connected", self.peer) def tls_is_certificate_valid(self, cert): """Default certificate verification callback for TLS connections. :Parameters: - `cert`: certificate information, as returned by `ssl.SSLSocket.getpeercert` :return: computed verification result.""" try: self.__logger.debug("tls_is_certificate_valid(cert = %r)" % ( cert,)) if not cert: self.__logger.warning("No TLS certificate information received.") return False valid_hostname_found = False if 'subject' in cert: for rdns in cert['subject']: for key, value in rdns: if key == 'commonName' and JID(value) == self.peer: self.__logger.debug(" good commonName: {0}".format(value)) valid_hostname_found = True if 'subjectAltName' in cert: for key, value in cert['subjectAltName']: if key == 'DNS' and JID(value) == self.peer: self.__logger.debug(" good subjectAltName({0}): {1}" .format(key, value)) valid_hostname_found = True return valid_hostname_found except: self.__logger.exception("Exception caught") raise def get_tls_connection(self): """Get the TLS connection object for the stream. :return: `self.tls`""" return self.tls # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/interfaces.py0000644000175000017500000000431111560025462017544 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Interfaces for flexible API extensions.""" __docformat__ = "restructuredtext en" from pyxmpp.interface import Interface, Attribute class IPyXMPPHelper(Interface): """Base for all interfaces used as PyXMPP helpers.""" class IPresenceHandlersProvider(IPyXMPPHelper): def get_presence_handlers(): """Returns iterable over (presence_type, handler[, namespace[, priority]]) tuples. The tuples will be used as arguments for `Stream.set_presence_handler`.""" class IMessageHandlersProvider(IPyXMPPHelper): def get_message_handlers(): """Returns iterable over (message_type, handler[, namespace[, priority]]) tuples. The tuples will be used as arguments for `Stream.set_message_handler`.""" class IIqHandlersProvider(IPyXMPPHelper): def get_iq_get_handlers(): """Returns iterable over (element_name, namespace) tuples. The tuples will be used as arguments for `Stream.set_iq_get_handler`.""" def get_iq_set_handlers(): """Returns iterable over (element_name, namespace) tuples. The tuples will be used as arguments for `Stream.set_iq_set_handler`.""" class IStanzaHandlersProvider(IPresenceHandlersProvider, IMessageHandlersProvider, IIqHandlersProvider): pass class IFeaturesProvider(IPyXMPPHelper): def get_features(): """Return iterable of namespaces (features) supported, for disco#info query response.""" __all__ = [ name for name in dir() if name.startswith("I") and name != "Interface" ] # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/stream.py0000644000175000017500000001103711560025462016717 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Generic XMPP stream implementation. Normative reference: - `RFC 3920 `__ """ __docformat__="restructuredtext en" import logging from pyxmpp.streambase import StreamBase from pyxmpp.streamtls import StreamTLSMixIn from pyxmpp.streamsasl import StreamSASLMixIn class Stream(StreamTLSMixIn,StreamSASLMixIn,StreamBase): """Generic XMPP stream class. Responsible for establishing connection, parsing the stream, StartTLS encryption and SASL authentication negotiation and usage, dispatching received stanzas to apopriate handlers and sending application's stanzas. Whenever we say "stream" here we actually mean two streams (incoming and outgoing) of one connections, as defined by the XMPP specification. :Ivariables: - `lock`: RLock object used to synchronize access to Stream object. - `features`: stream features as annouced by the initiator. - `me`: local stream endpoint JID. - `peer`: remote stream endpoint JID. - `process_all_stanzas`: when `True` then all stanzas received are considered local. - `tls`: TLS connection object. - `initiator`: `True` if local stream endpoint is the initiating entity. - `_reader`: the stream reader object (push parser) for the stream. """ def __init__(self, default_ns, extra_ns = (), sasl_mechanisms = (), tls_settings = None, keepalive = 0, owner = None): """Initialize Stream object :Parameters: - `default_ns`: stream's default namespace ("jabber:client" for client, "jabber:server" for server, etc.) - `extra_ns`: sequence of extra namespace URIs to be defined for the stream. - `sasl_mechanisms`: sequence of SASL mechanisms allowed for authentication. Currently "PLAIN", "DIGEST-MD5" and "GSSAPI" are supported. - `tls_settings`: settings for StartTLS -- `TLSSettings` instance. - `keepalive`: keepalive output interval. 0 to disable. - `owner`: `Client`, `Component` or similar object "owning" this stream. """ StreamBase.__init__(self, default_ns, extra_ns, keepalive, owner) StreamTLSMixIn.__init__(self, tls_settings) StreamSASLMixIn.__init__(self, sasl_mechanisms) self.__logger = logging.getLogger("pyxmpp.Stream") def _reset(self): """Reset `Stream` object state making it ready to handle new connections.""" StreamBase._reset(self) self._reset_tls() self._reset_sasl() def _make_stream_features(self): """Create the element for the stream. [receving entity only] :returns: new element node.""" features=StreamBase._make_stream_features(self) self._make_stream_tls_features(features) self._make_stream_sasl_features(features) return features def _process_node(self,xmlnode): """Process first level element of the stream. The element may be stream error or features, StartTLS request/response, SASL request/response or a stanza. :Parameters: - `xmlnode`: XML node describing the element """ if self._process_node_tls(xmlnode): return if self._process_node_sasl(xmlnode): return StreamBase._process_node(self,xmlnode) def _got_features(self): """Process incoming element. [initiating entity only] The received features node is available in `self.features`.""" self._handle_tls_features() self._handle_sasl_features() StreamBase._got_features(self) if not self.tls_requested and not self.authenticated: self.state_change("fully connected",self.peer) self._post_connect() # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/exceptions.py0000644000175000017500000001330011561455316017606 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """PyXMPP exceptions. This module defines all exceptions raised by PyXMPP. """ __docformat__="restructuredtext en" import logging class Error(StandardError): """Base class for all PyXMPP exceptions.""" pass class JIDError(Error, ValueError): "Exception raised when invalid JID is used" pass class StreamError(Error): """Base class for all stream errors.""" pass class StreamEncryptionRequired(StreamError): """Exception raised when stream encryption is requested, but not used.""" pass class HostMismatch(StreamError): """Exception raised when the connected host name is other then requested.""" pass class FatalStreamError(StreamError): """Base class for all fatal Stream exceptions. When `FatalStreamError` is raised the stream is no longer usable.""" pass class StreamParseError(FatalStreamError): """Raised when invalid XML is received in an XMPP stream.""" pass class DNSError(FatalStreamError): """Raised when no host name could be resolved for the target.""" pass class UnexpectedCNAMEError(DNSError): """Raised when CNAME record was found when A or AAAA was expected.""" pass class StreamAuthenticationError(FatalStreamError): """Raised when stream authentication fails.""" pass class TLSNegotiationFailed(FatalStreamError): """Raised when stream TLS negotiation fails.""" pass class TLSError(FatalStreamError): """Raised on TLS error during stream processing.""" pass class TLSNegotiatedButNotAvailableError(TLSError): """Raised on TLS error during stream processing.""" pass class SASLNotAvailable(StreamAuthenticationError): """Raised when SASL authentication is requested, but not available.""" pass class SASLMechanismNotAvailable(StreamAuthenticationError): """Raised when none of SASL authentication mechanisms requested is available.""" pass class SASLAuthenticationFailed(StreamAuthenticationError): """Raised when stream SASL authentication fails.""" pass class StringprepError(Error): """Exception raised when string preparation results in error.""" pass class ClientError(Error): """Raised on a client error.""" pass class FatalClientError(ClientError): """Raised on a fatal client error.""" pass class ClientStreamError(StreamError): """Raised on a client stream error.""" pass class FatalClientStreamError(FatalStreamError): """Raised on a fatal client stream error.""" pass class LegacyAuthenticationError(ClientStreamError): """Raised on a legacy authentication error.""" pass class RegistrationError(ClientStreamError): """Raised on a in-band registration error.""" pass class ComponentStreamError(StreamError): """Raised on a component error.""" pass class FatalComponentStreamError(ComponentStreamError,FatalStreamError): """Raised on a fatal component error.""" pass ######################## # Protocol Errors class ProtocolError(Error): """Raised when there is something wrong with a stanza processed. When not processed earlier by an application, the exception will be catched by the stanza dispatcher to return XMPP error to the stanza sender, when allowed. ProtocolErrors handled internally by PyXMPP will be logged via the logging interface. Errors reported to the sender will be logged using "pyxmpp.ProtocolError.reported" channel and the ignored errors using "pyxmpp.ProtocolError.ignored" channel. Both with the "debug" level. :Ivariables: - `xmpp_name` -- XMPP error name which should be reported. - `message` -- the error message.""" logger_reported = logging.getLogger("pyxmpp.ProtocolError.reported") logger_ignored = logging.getLogger("pyxmpp.ProtocolError.ignored") def __init__(self, xmpp_name, message): self.args = (xmpp_name, message) @property def xmpp_name(self): return self.args[0] @property def message(self): return self.args[1] def log_reported(self): self.logger_reported.debug(u"Protocol error detected: %s", self.message) def log_ignored(self): self.logger_ignored.debug(u"Protocol error detected: %s", self.message) def __unicode__(self): return str(self.args[1]) def __repr__(self): return "" % (self.xmpp_name, self.message) class BadRequestProtocolError(ProtocolError): """Raised when invalid stanza is processed and 'bad-request' error should be reported.""" def __init__(self, message): ProtocolError.__init__(self, "bad-request", message) class JIDMalformedProtocolError(ProtocolError, JIDError): """Raised when invalid JID is encountered.""" def __init__(self, message): ProtocolError.__init__(self, "jid-malformed", message) class FeatureNotImplementedProtocolError(ProtocolError): """Raised when stanza requests a feature which is not (yet) implemented.""" def __init__(self, message): ProtocolError.__init__(self, "feature-not-implemented", message) # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/interface.py0000644000175000017500000000243311560025462017364 0ustar jajcususers00000000000000# # (C) Copyright 2006 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Interface API. If zope.interface is available this module will be its equivalent, otherwise minimum interface API (partially compatible with zope.interface) will be defined here. When full ZopeInterfaces API is needed impoer zope.interface instead of this module.""" try: from zope.interface import Interface, Attribute, providedBy, implementedBy, implements except ImportError: from pyxmpp.interface_micro_impl import Interface, Attribute, providedBy, implementedBy, implements __all__ = ("Interface", "Attribute", "providedBy", "implementedBy", "implements") # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/resolver.py0000644000175000017500000001641411561462156017277 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """DNS resolever with SRV record support. Normative reference: - `RFC 1035 `__ - `RFC 2782 `__ """ __docformat__="restructuredtext en" import re import socket from socket import AF_UNSPEC, AF_INET, AF_INET6 import dns.resolver import dns.name import dns.exception import random import logging from .exceptions import DNSError, UnexpectedCNAMEError logger = logging.getLogger("pyxmpp.resolver") # check IPv6 support try: socket.socket(AF_INET6) except socket.error: default_address_family = AF_INET else: default_address_family = AF_UNSPEC def set_default_address_family(family): """Select default address family. :Parameters: - `family`: `AF_INET` for IPv4, `AF_INET6` for IPv6 and `AF_UNSPEC` for dual stack.""" global default_address_family default_address_family = family service_aliases={"xmpp-server": ("jabber-server","jabber")} # should match all valid IP addresses, but can pass some false-positives, # which are not valid domain names ipv4_re=re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$") ipv6_re=re.compile(r"^[0-9a-f]{0,4}:[0-9a-f:]{0,29}:([0-9a-f]{0,4}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$") def shuffle_srv(records): """Randomly reorder SRV records using their weights. :Parameters: - `records`: SRV records to shuffle. :Types: - `records`: sequence of `dns.rdtypes.IN.SRV` :return: reordered records. :returntype: `list` of `dns.rdtypes.IN.SRV`""" if not records: return [] ret=[] while len(records)>1: weight_sum=0 for rr in records: weight_sum+=rr.weight+0.1 thres=random.random()*weight_sum weight_sum=0 for rr in records: weight_sum+=rr.weight+0.1 if thres # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Caching proxy for Jabber/XMPP objects. This package provides facilities to retrieve and transparently cache cachable objects like Service Discovery responses or e.g. client version informations.""" __docformat__ = "restructuredtext en" import threading from datetime import datetime, timedelta _state_values = { 'new': 0, 'fresh': 1, 'old': 2, 'stale': 3, 'purged': 4 }; # locking order (anti-deadlock): # CacheSuite, Cache, CacheHandler, CacheItem class CacheItem(object): """An item in a cache. :Ivariables: - `value`: item value (cached object). - `address`: item address. - `state`: current state. - `state_value`: numerical value of the current state (lower number means fresher item). - `timestamp`: time when the object was created. - `freshness_time`: time when the object stops being fresh. - `expire_time`: time when the object expires. - `purge_time`: time when the object should be purged. When 0 then item will never be automaticaly purged. - `_lock`: lock for thread safety. :Types: - `value`: `instance` - `address`: any hashable - `state`: `str` - `state_value`: `int` - `timestamp`: `datetime` - `freshness_time`: `datetime` - `expire_time`: `datetime` - `purge_time`: `datetime` - `_lock`: `threading.RLock`""" __slots__ = ['value', 'address', 'state', 'timestamp', 'freshness_time', 'expire_time', 'purge_time', 'state_value', '_lock'] def __init__(self, address, value, freshness_period, expiration_period, purge_period, state = "new"): """Initialize an CacheItem object. :Parameters: - `address`: item address. - `value`: item value (cached object). - `freshness_period`: time interval after which the object stops being fresh. - `expiration_period`: time interval after which the object expires. - `purge_period`: time interval after which the object should be purged. When 0 then item will never be automaticaly purged. - `state`: initial state. :Types: - `address`: any hashable - `value`: `instance` - `freshness_period`: `timedelta` - `expiration_period`: `timedelta` - `purge_period`: `timedelta` - `state`: `str`""" if freshness_period>expiration_period: raise ValueError, "freshness_period greater then expiration_period" if expiration_period>purge_period: raise ValueError, "expiration_period greater then purge_period" self.address = address self.value = value now = datetime.utcnow() self.timestamp = now self.freshness_time = now+freshness_period self.expire_time = now+expiration_period if purge_period: self.purge_time = now+purge_period else: self.purge_time = datetime.max self.state = state self.state_value = _state_values[state] self._lock = threading.RLock() def update_state(self): """Update current status of the item and compute time of the next state change. :return: the new state. :returntype: `datetime`""" self._lock.acquire() try: now = datetime.utcnow() if self.state == 'new': self.state = 'fresh' if self.state == 'fresh': if now > self.freshness_time: self.state = 'old' if self.state == 'old': if now > self.expire_time: self.state = 'stale' if self.state == 'stale': if now > self.purge_time: self.state = 'purged' self.state_value = _state_values[self.state] return self.state finally: self._lock.release() def __cmp__(self,other): try: return cmp( (-self.state_value, self.timestamp, id(self)), (-other.state_value, other.timestamp, id(other)) ) except AttributeError: return cmp(id(self),id(other)) _hour = timedelta(hours = 1) class CacheFetcher: """Base class for cache object fetchers -- classes responsible for retrieving objects from network. An instance of a fetcher class is created for each object requested and not found in the cache, then `fetch` method is called to initialize the asynchronous retrieval process. Fetcher object's `got_it` method should be called on a successfull retrieval and `error` otherwise. `timeout` will be called when the request timeouts. :Ivariables: - `cache`: cache object which created this fetcher. - `address`: requested item address. - `timeout_time`: timeout time. - `active`: `True` as long as the fetcher is active and requestor expects one of the handlers to be called. :Types: - `cache`: `Cache` - `address`: any hashable - `timeout_time`: `datetime` - `active`: `bool` """ def __init__(self, cache, address, item_freshness_period, item_expiration_period, item_purge_period, object_handler, error_handler, timeout_handler, timeout_period, backup_state = None): """Initialize an `CacheFetcher` object. :Parameters: - `cache`: cache object which created this fetcher. - `address`: requested item address. - `item_freshness_period`: freshness period for the requested item. - `item_expiration_period`: expiration period for the requested item. - `item_purge_period`: purge period for the requested item. - `object_handler`: function to be called after the item is fetched. - `error_handler`: function to be called on error. - `timeout_handler`: function to be called on timeout - `timeout_period`: timeout interval. - `backup_state`: when not `None` and the fetch fails than an object from cache of at least this state will be passed to the `object_handler`. If such object is not available, then `error_handler` is called. :Types: - `cache`: `Cache` - `address`: any hashable - `item_freshness_period`: `timedelta` - `item_expiration_period`: `timedelta` - `item_purge_period`: `timedelta` - `object_handler`: callable(address, value, state) - `error_handler`: callable(address, error_data) - `timeout_handler`: callable(address) - `timeout_period`: `timedelta` - `backup_state`: `bool`""" self.cache = cache self.address = address self._item_freshness_period = item_freshness_period self._item_expiration_period = item_expiration_period self._item_purge_period = item_purge_period self._object_handler = object_handler self._error_handler = error_handler self._timeout_handler = timeout_handler if timeout_period: self.timeout_time = datetime.utcnow()+timeout_period else: self.timeout_time = datetime.max self._backup_state = backup_state self.active = True def _deactivate(self): """Remove the fetcher from cache and mark it not active.""" self.cache.remove_fetcher(self) if self.active: self._deactivated() def _deactivated(self): """Mark the fetcher inactive after it is removed from the cache.""" self.active = False def fetch(self): """Start the retrieval process. This method must be implemented in any fetcher class.""" raise RuntimeError, "Pure virtual method called" def got_it(self, value, state = "new"): """Handle a successfull retrieval and call apriopriate handler. Should be called when retrieval succeeds. Do nothing when the fetcher is not active any more (after one of handlers was already called). :Parameters: - `value`: fetched object. - `state`: initial state of the object. :Types: - `value`: any - `state`: `str`""" if not self.active: return item = CacheItem(self.address, value, self._item_freshness_period, self._item_expiration_period, self._item_purge_period, state) self._object_handler(item.address, item.value, item.state) self.cache.add_item(item) self._deactivate() def error(self, error_data): """Handle a retrieval error and call apriopriate handler. Should be called when retrieval fails. Do nothing when the fetcher is not active any more (after one of handlers was already called). :Parameters: - `error_data`: additional information about the error (e.g. `StanzaError` instance). :Types: - `error_data`: fetcher dependant """ if not self.active: return if not self._try_backup_item(): self._error_handler(self.address, error_data) self.cache.invalidate_object(self.address) self._deactivate() def timeout(self): """Handle fetcher timeout and call apriopriate handler. Is called by the cache object and should _not_ be called by fetcher or application. Do nothing when the fetcher is not active any more (after one of handlers was already called).""" if not self.active: return if not self._try_backup_item(): if self._timeout_handler: self._timeout_handler(self.address) else: self._error_handler(self.address, None) self.cache.invalidate_object(self.address) self._deactivate() def _try_backup_item(self): """Check if a backup item is available in cache and call the item handler if it is. :return: `True` if backup item was found. :returntype: `bool`""" if not self._backup_state: return False item = self.cache.get_item(self.address, self._backup_state) if item: self._object_handler(item.address, item.value, item.state) return True else: False class Cache: """Caching proxy for object retrieval and caching. Object factories ("fetchers") are registered in the `Cache` object and used to e.g. retrieve requested objects from network. They are called only when the requested object is not in the cache or is not fresh enough. A state (freshness level) name may be provided when requesting an object. When the cached item state is "less fresh" then requested, then new object will be retrieved. Following states are defined: - 'new': always a new object should be retrieved. - 'fresh': a fresh object (not older than freshness time) - 'old': object not fresh, but most probably still valid. - 'stale': object known to be expired. :Ivariables: - `default_freshness_period`: default freshness period (in seconds). - `default_expiration_period`: default expiration period (in seconds). - `default_purge_period`: default purge period (in seconds). When 0 then items are never purged because of their age. - `max_items`: maximum number of items to store. - `_items`: dictionary of stored items. - `_items_list`: list of stored items with the most suitable for purging first. - `_fetcher`: fetcher class for this cache. - `_active_fetchers`: list of active fetchers sorted by the time of its expiration time. - `_lock`: lock for thread safety. :Types: - `default_freshness_period`: timedelta - `default_expiration_period`: timedelta - `default_purge_period`: timedelta - `max_items`: `int` - `_items`: `dict` of (`classobj`, addr) -> `CacheItem` - `_items_list`: `list` of (`int`, `datetime`, `CacheItem`) - `_fetcher`: `CacheFetcher` based class - `_active_fetchers`: `list` of (`int`, `CacheFetcher`) - `_lock`: `threading.RLock` """ def __init__(self, max_items, default_freshness_period = _hour, default_expiration_period = 12*_hour, default_purge_period = 24*_hour): """Initialize a `Cache` object. :Parameters: - `default_freshness_period`: default freshness period (in seconds). - `default_expiration_period`: default expiration period (in seconds). - `default_purge_period`: default purge period (in seconds). When 0 then items are never purged because of their age. - `max_items`: maximum number of items to store. :Types: - `default_freshness_period`: number - `default_expiration_period`: number - `default_purge_period`: number - `max_items`: number """ self.default_freshness_period = default_freshness_period self.default_expiration_period = default_expiration_period self.default_purge_period = default_purge_period self.max_items = max_items self._items = {} self._items_list = [] self._fetcher = None self._active_fetchers = [] self._purged = 0 self._lock = threading.RLock() def request_object(self, address, state, object_handler, error_handler = None, timeout_handler = None, backup_state = None, timeout = timedelta(minutes=60), freshness_period = None, expiration_period = None, purge_period = None): """Request an object with given address and state not worse than `state`. The object will be taken from cache if available, and created/fetched otherwise. The request is asynchronous -- this metod doesn't return the object directly, but the `object_handler` is called as soon as the object is available (this may be before `request_object` returns and may happen in other thread). On error the `error_handler` will be called, and on timeout -- the `timeout_handler`. :Parameters: - `address`: address of the object requested. - `state`: the worst acceptable object state. When 'new' then always a new object will be created/fetched. 'stale' will select any item available in cache. - `object_handler`: function to be called when object is available. It will be called with the following arguments: address, object and its state. - `error_handler`: function to be called on object retrieval error. It will be called with two arguments: requested address and additional error information (fetcher-specific, may be StanzaError for XMPP objects). If not given, then the object handler will be called with object set to `None` and state "error". - `timeout_handler`: function to be called on object retrieval timeout. It will be called with only one argument: the requested address. If not given, then the `error_handler` will be called instead, with error details set to `None`. - `backup_state`: when set and object in state `state` is not available in the cache and object retrieval failed then object with this state will also be looked-up in the cache and provided if available. - `timeout`: time interval after which retrieval of the object should be given up. - `freshness_period`: time interval after which the item created should become 'old'. - `expiration_period`: time interval after which the item created should become 'stale'. - `purge_period`: time interval after which the item created shuld be removed from the cache. :Types: - `address`: any hashable - `state`: "new", "fresh", "old" or "stale" - `object_handler`: callable(address, value, state) - `error_handler`: callable(address, error_data) - `timeout_handler`: callable(address) - `backup_state`: "new", "fresh", "old" or "stale" - `timeout`: `timedelta` - `freshness_period`: `timedelta` - `expiration_period`: `timedelta` - `purge_period`: `timedelta` """ self._lock.acquire() try: if state == 'stale': state = 'purged' item = self.get_item(address, state) if item: object_handler(item.address, item.value, item.state) return if not self._fetcher: raise TypeError, "No cache fetcher defined" if not error_handler: def default_error_handler(address, _unused): "Default error handler." return object_handler(address, None, 'error') error_handler = default_error_handler if not timeout_handler: def default_timeout_handler(address): "Default timeout handler." return error_handler(address, None) timeout_handler = default_timeout_handler if freshness_period is None: freshness_period = self.default_freshness_period if expiration_period is None: expiration_period = self.default_expiration_period if purge_period is None: purge_period = self.default_purge_period fetcher = self._fetcher(self, address, freshness_period, expiration_period, purge_period, object_handler, error_handler, timeout_handler, timeout, backup_state) fetcher.fetch() self._active_fetchers.append((fetcher.timeout_time,fetcher)) self._active_fetchers.sort() finally: self._lock.release() def invalidate_object(self, address, state = 'stale'): """Force cache item state change (to 'worse' state only). :Parameters: - `state`: the new state requested. :Types: - `state`: `str`""" self._lock.acquire() try: item = self.get_item(address) if item and item.state_value<_state_values[state]: item.state=state item.update_state() self._items_list.sort() finally: self._lock.release() def add_item(self, item): """Add an item to the cache. Item state is updated before adding it (it will not be 'new' any more). :Parameters: - `item`: the item to add. :Types: - `item`: `CacheItem` :return: state of the item after addition. :returntype: `str` """ self._lock.acquire() try: state = item.update_state() if state != 'purged': if len(self._items_list) >= self.max_items: self.purge_items() self._items[item.address] = item self._items_list.append(item) self._items_list.sort() return item.state finally: self._lock.release() def get_item(self, address, state = 'fresh'): """Get an item from the cache. :Parameters: - `address`: its address. - `state`: the worst state that is acceptable. :Types: - `address`: any hashable - `state`: `str` :return: the item or `None` if it was not found. :returntype: `CacheItem`""" self._lock.acquire() try: item = self._items.get(address) if not item: return None self.update_item(item) if _state_values[state] >= item.state_value: return item return None finally: self._lock.release() def update_item(self, item): """Update state of an item in the cache. Update item's state and remove the item from the cache if its new state is 'purged' :Parameters: - `item`: item to update. :Types: - `item`: `CacheItem` :return: new state of the item. :returntype: `str`""" self._lock.acquire() try: state = item.update_state() self._items_list.sort() if item.state == 'purged': self._purged += 1 if self._purged > 0.25*self.max_items: self.purge_items() return state finally: self._lock.release() def num_items(self): """Get the number of items in the cache. :return: number of items. :returntype: `int`""" return len(self._items_list) def purge_items(self): """Remove purged and overlimit items from the cache. TODO: optimize somehow. Leave no more than 75% of `self.max_items` items in the cache.""" self._lock.acquire() try: il=self._items_list num_items = len(il) need_remove = num_items - int(0.75 * self.max_items) for _unused in range(need_remove): item=il.pop(0) try: del self._items[item.address] except KeyError: pass while il and il[0].update_state()=="purged": item=il.pop(0) try: del self._items[item.address] except KeyError: pass finally: self._lock.release() def tick(self): """Do the regular cache maintenance. Must be called from time to time for timeouts and cache old items purging to work.""" self._lock.acquire() try: now = datetime.utcnow() for t,f in list(self._active_fetchers): if t > now: break f.timeout() self.purge_items() finally: self._lock.release() def remove_fetcher(self, fetcher): """Remove a running fetcher from the list of active fetchers. :Parameters: - `fetcher`: fetcher instance. :Types: - `fetcher`: `CacheFetcher`""" self._lock.acquire() try: for t, f in list(self._active_fetchers): if f is fetcher: self._active_fetchers.remove((t, f)) f._deactivated() return finally: self._lock.release() def set_fetcher(self, fetcher_class): """Set the fetcher class. :Parameters: - `fetcher_class`: the fetcher class. :Types: - `fetcher_class`: `CacheFetcher` based class """ self._lock.acquire() try: self._fetcher = fetcher_class finally: self._lock.release() class CacheSuite: """Caching proxy for object retrieval and caching. Object factories for other classes are registered in the `Cache` object and used to e.g. retrieve requested objects from network. They are called only when the requested object is not in the cache or is not fresh enough. Objects are addressed using their class and a class dependant address. Eg. `pyxmpp.jabber.disco.DiscoInfo` objects are addressed using (`pyxmpp.jabber.disco.DiscoInfo`,(jid, node)) tuple. Additionaly a state (freshness level) name may be provided when requesting an object. When the cached item state is "less fresh" then requested, then new object will be retrieved. Following states are defined: - 'new': always a new object should be retrieved. - 'fresh': a fresh object (not older than freshness time) - 'old': object not fresh, but most probably still valid. - 'stale': object known to be expired. :Ivariables: - `default_freshness_period`: default freshness period (in seconds). - `default_expiration_period`: default expiration period (in seconds). - `default_purge_period`: default purge period (in seconds). When 0 then items are never purged because of their age. - `max_items`: maximum number of obejects of one class to store. - `_caches`: dictionary of per-class caches. - `_lock`: lock for thread safety. :Types: - `default_freshness_period`: timedelta - `default_expiration_period`: timedelta - `default_purge_period`: timedelta - `max_items`: `int` - `_caches`: `dict` of (`classobj`, addr) -> `Cache` - `_lock`: `threading.RLock` """ def __init__(self, max_items, default_freshness_period = _hour, default_expiration_period = 12*_hour, default_purge_period = 24*_hour): """Initialize a `Cache` object. :Parameters: - `default_freshness_period`: default freshness period (in seconds). - `default_expiration_period`: default expiration period (in seconds). - `default_purge_period`: default purge period (in seconds). When 0 then items are never purged because of their age. - `max_items`: maximum number of items to store. :Types: - `default_freshness_period`: number - `default_expiration_period`: number - `default_purge_period`: number - `max_items`: number """ self.default_freshness_period = default_freshness_period self.default_expiration_period = default_expiration_period self.default_purge_period = default_purge_period self.max_items = max_items self._caches = {} self._lock = threading.RLock() def request_object(self, object_class, address, state, object_handler, error_handler = None, timeout_handler = None, backup_state = None, timeout = None, freshness_period = None, expiration_period = None, purge_period = None): """Request an object of given class, with given address and state not worse than `state`. The object will be taken from cache if available, and created/fetched otherwise. The request is asynchronous -- this metod doesn't return the object directly, but the `object_handler` is called as soon as the object is available (this may be before `request_object` returns and may happen in other thread). On error the `error_handler` will be called, and on timeout -- the `timeout_handler`. :Parameters: - `object_class`: class (type) of the object requested. - `address`: address of the object requested. - `state`: the worst acceptable object state. When 'new' then always a new object will be created/fetched. 'stale' will select any item available in cache. - `object_handler`: function to be called when object is available. It will be called with the following arguments: address, object and its state. - `error_handler`: function to be called on object retrieval error. It will be called with two arguments: requested address and additional error information (fetcher-specific, may be StanzaError for XMPP objects). If not given, then the object handler will be called with object set to `None` and state "error". - `timeout_handler`: function to be called on object retrieval timeout. It will be called with only one argument: the requested address. If not given, then the `error_handler` will be called instead, with error details set to `None`. - `backup_state`: when set and object in state `state` is not available in the cache and object retrieval failed then object with this state will also be looked-up in the cache and provided if available. - `timeout`: time interval after which retrieval of the object should be given up. - `freshness_period`: time interval after which the item created should become 'old'. - `expiration_period`: time interval after which the item created should become 'stale'. - `purge_period`: time interval after which the item created shuld be removed from the cache. :Types: - `object_class`: `classobj` - `address`: any hashable - `state`: "new", "fresh", "old" or "stale" - `object_handler`: callable(address, value, state) - `error_handler`: callable(address, error_data) - `timeout_handler`: callable(address) - `backup_state`: "new", "fresh", "old" or "stale" - `timeout`: `timedelta` - `freshness_period`: `timedelta` - `expiration_period`: `timedelta` - `purge_period`: `timedelta` """ self._lock.acquire() try: if object_class not in self._caches: raise TypeError, "No cache for %r" % (object_class,) self._caches[object_class].request_object(address, state, object_handler, error_handler, timeout_handler, backup_state, timeout, freshness_period, expiration_period, purge_period) finally: self._lock.release() def tick(self): """Do the regular cache maintenance. Must be called from time to time for timeouts and cache old items purging to work.""" self._lock.acquire() try: for cache in self._caches.values(): cache.tick() finally: self._lock.release() def register_fetcher(self, object_class, fetcher_class): """Register a fetcher class for an object class. :Parameters: - `object_class`: class to be retrieved by the fetcher. - `fetcher_class`: the fetcher class. :Types: - `object_class`: `classobj` - `fetcher_class`: `CacheFetcher` based class """ self._lock.acquire() try: cache = self._caches.get(object_class) if not cache: cache = Cache(self.max_items, self.default_freshness_period, self.default_expiration_period, self.default_purge_period) self._caches[object_class] = cache cache.set_fetcher(fetcher_class) finally: self._lock.release() def unregister_fetcher(self, object_class): """Unregister a fetcher class for an object class. :Parameters: - `object_class`: class retrieved by the fetcher. :Types: - `object_class`: `classobj` """ self._lock.acquire() try: cache = self._caches.get(object_class) if not cache: return cache.set_fetcher(None) finally: self._lock.release() # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/jabberd/0000755000175000017500000000000011561501464016443 5ustar jajcususers00000000000000pyxmpp-1.1.2/pyxmpp/jabberd/__init__.py0000644000175000017500000000153011560025462020551 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Facilities for jabber server implementation specific features, like components.""" __docformat__="restructuredtext en" # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/jabberd/component.py0000644000175000017500000003502011560025462021015 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Jabberd external component interface (jabber:component:accept). Normative reference: - `JEP 114 `__ """ __docformat__="restructuredtext en" import threading import logging from pyxmpp.jabberd.componentstream import ComponentStream from pyxmpp.utils import from_utf8 from pyxmpp.jabber.disco import DiscoItems,DiscoInfo,DiscoIdentity from pyxmpp.stanza import Stanza class Component: """Jabber external component ("jabber:component:accept" protocol) interface implementation. Override this class to build your components. :Ivariables: - `jid`: component JID (should contain only the domain part). - `secret`: the authentication secret. - `server`: server to which the commonent will connect. - `port`: port number on the server to which the commonent will connect. - `keepalive`: keepalive interval for the stream. - `stream`: the XMPP stream object for the active connection or `None` if no connection is active. - `disco_items`: disco items announced by the component. Created when a stream is connected. - `disco_info`: disco info announced by the component. Created when a stream is connected. - `disco_identity`: disco identity (part of disco info) announced by the component. Created when a stream is connected. - `disco_category`: disco category to be used to create `disco_identity`. - `disco_type`: disco type to be used to create `disco_identity`. :Types: - `jid`: `pyxmpp.JID` - `secret`: `unicode` - `server`: `unicode` - `port`: `int` - `keepalive`: `int` - `stream`: `pyxmpp.jabberd.ComponentStream` - `disco_items`: `pyxmpp.jabber.DiscoItems` - `disco_info`: `pyxmpp.jabber.DiscoInfo` - `disco_identity`: `pyxmpp.jabber.DiscoIdentity` - `disco_category`: `str` - `disco_type`: `str`""" def __init__(self, jid=None, secret=None, server=None, port=5347, disco_name=u"PyXMPP based component", disco_category=u"x-service", disco_type=u"x-unknown", keepalive=0): """Initialize a `Component` object. :Parameters: - `jid`: component JID (should contain only the domain part). - `secret`: the authentication secret. - `server`: server name or address the component should connect. - `port`: port number on the server where the component should connect. - `disco_name`: disco identity name to be used in the disco#info responses. - `disco_category`: disco identity category to be used in the disco#info responses. Use `the categories registered by Jabber Registrar `__ - `disco_type`: disco identity type to be used in the component's disco#info responses. Use `the types registered by Jabber Registrar `__ - `keepalive`: keepalive interval for the stream. :Types: - `jid`: `pyxmpp.JID` - `secret`: `unicode` - `server`: `str` or `unicode` - `port`: `int` - `disco_name`: `unicode` - `disco_category`: `unicode` - `disco_type`: `unicode` - `keepalive`: `int`""" self.jid=jid self.secret=secret self.server=server self.port=port self.keepalive=keepalive self.stream=None self.lock=threading.RLock() self.state_changed=threading.Condition(self.lock) self.stream_class=ComponentStream self.disco_items=DiscoItems() self.disco_info=DiscoInfo() self.disco_identity=DiscoIdentity(self.disco_info, disco_name, disco_category, disco_type) self.register_feature("stringprep") self.__logger=logging.getLogger("pyxmpp.jabberd.Component") # public methods def connect(self): """Establish a connection with the server. Set `self.stream` to the `pyxmpp.jabberd.ComponentStream` when initial connection succeeds. :raise ValueError: when some of the component properties (`self.jid`, `self.secret`,`self.server` or `self.port`) are wrong.""" if not self.jid or self.jid.node or self.jid.resource: raise ValueError,"Cannot connect: no or bad JID given" if not self.secret: raise ValueError,"Cannot connect: no secret given" if not self.server: raise ValueError,"Cannot connect: no server given" if not self.port: raise ValueError,"Cannot connect: no port given" self.lock.acquire() try: stream=self.stream self.stream=None if stream: stream.close() self.__logger.debug("Creating component stream: %r" % (self.stream_class,)) stream=self.stream_class(jid = self.jid, secret = self.secret, server = self.server, port = self.port, keepalive = self.keepalive, owner = self) stream.process_stream_error=self.stream_error self.stream_created(stream) stream.state_change=self.__stream_state_change stream.connect() self.stream=stream self.state_changed.notify() self.state_changed.release() except: self.stream=None self.state_changed.release() raise def get_stream(self): """Get the stream of the component in a safe way. :return: Stream object for the component or `None` if no connection is active. :returntype: `pyxmpp.jabberd.ComponentStream`""" self.lock.acquire() stream=self.stream self.lock.release() return stream def disconnect(self): """Disconnect from the server.""" stream=self.get_stream() if stream: stream.disconnect() def socket(self): """Get the socket of the connection to the server. :return: the socket. :returntype: `socket.socket`""" return self.stream.socket def loop(self,timeout=1): """Simple 'main loop' for a component. This usually will be replaced by something more sophisticated. E.g. handling of other input sources.""" self.stream.loop(timeout) def register_feature(self, feature_name): """Register a feature to be announced by Service Discovery. :Parameters: - `feature_name`: feature namespace or name. :Types: - `feature_name`: `unicode`""" self.disco_info.add_feature(feature_name) def unregister_feature(self, feature_name): """Unregister a feature to be announced by Service Discovery. :Parameters: - `feature_name`: feature namespace or name. :Types: - `feature_name`: `unicode`""" self.disco_info.remove_feature(feature_name) # private methods def __stream_state_change(self,state,arg): """Handle various stream state changes and call right methods of `self`. :Parameters: - `state`: state name. - `arg`: state parameter. :Types: - `state`: `string` - `arg`: any object""" self.stream_state_changed(state,arg) if state=="fully connected": self.connected() elif state=="authenticated": self.authenticated() elif state=="authorized": self.authorized() elif state=="disconnected": self.state_changed.acquire() try: if self.stream: self.stream.close() self.stream_closed(self.stream) self.stream=None self.state_changed.notify() finally: self.state_changed.release() self.disconnected() def __disco_info(self,iq): """Handle a disco-info query. :Parameters: - `iq`: the stanza received. Types: - `iq`: `pyxmpp.Iq`""" q=iq.get_query() if q.hasProp("node"): node=from_utf8(q.prop("node")) else: node=None info=self.disco_get_info(node,iq) if isinstance(info,DiscoInfo): resp=iq.make_result_response() self.__logger.debug("Disco-info query: %s preparing response: %s with reply: %s" % (iq.serialize(),resp.serialize(),info.xmlnode.serialize())) resp.set_content(info.xmlnode.copyNode(1)) elif isinstance(info,Stanza): resp=info else: resp=iq.make_error_response("item-not-found") self.__logger.debug("Disco-info response: %s" % (resp.serialize(),)) self.stream.send(resp) def __disco_items(self,iq): """Handle a disco-items query. :Parameters: - `iq`: the stanza received. Types: - `iq`: `pyxmpp.Iq`""" q=iq.get_query() if q.hasProp("node"): node=from_utf8(q.prop("node")) else: node=None items=self.disco_get_items(node,iq) if isinstance(items,DiscoItems): resp=iq.make_result_response() self.__logger.debug("Disco-items query: %s preparing response: %s with reply: %s" % (iq.serialize(),resp.serialize(),items.xmlnode.serialize())) resp.set_content(items.xmlnode.copyNode(1)) elif isinstance(items,Stanza): resp=items else: resp=iq.make_error_response("item-not-found") self.__logger.debug("Disco-items response: %s" % (resp.serialize(),)) self.stream.send(resp) # Method to override def idle(self): """Do some "housekeeping" work like result expiration. Should be called on a regular basis, usually when the component is idle.""" stream=self.get_stream() if stream: stream.idle() def stream_created(self,stream): """Handle stream creation event. [may be overriden in derived classes] By default: do nothing. :Parameters: - `stream`: the stream just created. :Types: - `stream`: `pyxmpp.jabberd.ComponentStream`""" pass def stream_closed(self,stream): """Handle stream closure event. [may be overriden in derived classes] By default: do nothing. :Parameters: - `stream`: the stream just created. :Types: - `stream`: `pyxmpp.jabberd.ComponentStream`""" pass def stream_error(self,err): """Handle a stream error received. [may be overriden in derived classes] By default: just log it. The stream will be closed anyway. :Parameters: - `err`: the error element received. :Types: - `err`: `pyxmpp.error.StreamErrorNode`""" self.__logger.debug("Stream error: condition: %s %r" % (err.get_condition().name,err.serialize())) def stream_state_changed(self,state,arg): """Handle a stream state change. [may be overriden in derived classes] By default: do nothing. :Parameters: - `state`: state name. - `arg`: state parameter. :Types: - `state`: `string` - `arg`: any object""" pass def connected(self): """Handle stream connection event. [may be overriden in derived classes] By default: do nothing.""" pass def authenticated(self): """Handle successful authentication event. A good place to register stanza handlers and disco features. [should be overriden in derived classes] By default: set disco#info and disco#items handlers.""" self.__logger.debug("Setting up Disco handlers...") self.stream.set_iq_get_handler("query","http://jabber.org/protocol/disco#items", self.__disco_items) self.stream.set_iq_get_handler("query","http://jabber.org/protocol/disco#info", self.__disco_info) def authorized(self): """Handle successful authorization event.""" pass def disco_get_info(self,node,iq): """Get disco#info data for a node. [may be overriden in derived classes] By default: return `self.disco_info` if no specific node name is provided. :Parameters: - `node`: name of the node queried. - `iq`: the stanza received. :Types: - `node`: `unicode` - `iq`: `pyxmpp.Iq`""" to=iq.get_to() if to and to!=self.jid: return iq.make_error_response("recipient-unavailable") if not node and self.disco_info: return self.disco_info return None def disco_get_items(self,node,iq): """Get disco#items data for a node. [may be overriden in derived classes] By default: return `self.disco_items` if no specific node name is provided. :Parameters: - `node`: name of the node queried. - `iq`: the stanza received. :Types: - `node`: `unicode` - `iq`: `pyxmpp.Iq`""" to=iq.get_to() if to and to!=self.jid: return iq.make_error_response("recipient-unavailable") if not node and self.disco_items: return self.disco_items return None def disconnected(self): """Handle stream disconnection (connection closed by peer) event. [may be overriden in derived classes] By default: do nothing.""" pass # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/jabberd/componentstream.py0000644000175000017500000001565211560025462022242 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # pylint: disable-msg=W0221, W0201 """Component (jabber:component:accept) stream handling. Normative reference: - `JEP 114 `__ """ __docformat__="restructuredtext en" import hashlib import logging from pyxmpp.stream import Stream from pyxmpp.streambase import stanza_factory,HostMismatch from pyxmpp.xmlextra import common_doc,common_root from pyxmpp.utils import to_utf8 from pyxmpp.exceptions import StreamError,FatalStreamError,ComponentStreamError,FatalComponentStreamError class ComponentStream(Stream): """Handles jabberd component (jabber:component:accept) connection stream. :Ivariables: - `server`: server to use. - `port`: port number to use. - `secret`: authentication secret. :Types: - `server`: `unicode` - `port`: `int` - `secret`: `unicode`""" def __init__(self, jid, secret, server, port, keepalive = 0, owner = None): """Initialize a `ComponentStream` object. :Parameters: - `jid`: JID of the component. - `secret`: authentication secret. - `server`: server address. - `port`: TCP port number on the server. - `keepalive`: keepalive interval. 0 to disable. - `owner`: `Client`, `Component` or similar object "owning" this stream. """ Stream.__init__(self, "jabber:component:accept", sasl_mechanisms = [], tls_settings = None, keepalive = keepalive, owner = owner) self.server=server self.port=port self.me=jid self.secret=secret self.process_all_stanzas=1 self.__logger=logging.getLogger("pyxmpp.jabberd.ComponentStream") def _reset(self): """Reset `ComponentStream` object state, making the object ready to handle new connections.""" Stream._reset(self) def connect(self,server=None,port=None): """Establish a client connection to a server. [component only] :Parameters: - `server`: name or address of the server to use. If not given then use the one specified when creating the object. - `port`: port number of the server to use. If not given then use the one specified when creating the object. :Types: - `server`: `unicode` - `port`: `int`""" self.lock.acquire() try: self._connect(server,port) finally: self.lock.release() def _connect(self,server=None,port=None): """Same as `ComponentStream.connect` but assume `self.lock` is acquired.""" if self.me.node or self.me.resource: raise Value, "Component JID may have only domain defined" if not server: server=self.server if not port: port=self.port if not server or not port: raise ValueError, "Server or port not given" Stream._connect(self,server,port,None,self.me) def accept(self,sock): """Accept an incoming component connection. [server only] :Parameters: - `sock`: a listening socket.""" Stream.accept(self,sock,None) def stream_start(self,doc): """Process (stream start) tag received from peer. Call `Stream.stream_start`, but ignore any `HostMismatch` error. :Parameters: - `doc`: document created by the parser""" try: Stream.stream_start(self,doc) except HostMismatch: pass def _post_connect(self): """Initialize authentication when the connection is established and we are the initiator.""" if self.initiator: self._auth() def _compute_handshake(self): """Compute the authentication handshake value. :return: the computed hash value. :returntype: `str`""" return hashlib.sha1(to_utf8(self.stream_id)+to_utf8(self.secret)).hexdigest() def _auth(self): """Authenticate on the server. [component only]""" if self.authenticated: self.__logger.debug("_auth: already authenticated") return self.__logger.debug("doing handshake...") hash_value=self._compute_handshake() n=common_root.newTextChild(None,"handshake",hash_value) self._write_node(n) n.unlinkNode() n.freeNode() self.__logger.debug("handshake hash sent.") def _process_node(self,node): """Process first level element of the stream. Handle component handshake (authentication) element, and treat elements in "jabber:component:accept", "jabber:client" and "jabber:server" equally (pass to `self.process_stanza`). All other elements are passed to `Stream._process_node`. :Parameters: - `node`: XML node describing the element """ ns=node.ns() if ns: ns_uri=node.ns().getContent() if (not ns or ns_uri=="jabber:component:accept") and node.name=="handshake": if self.initiator and not self.authenticated: self.authenticated=1 self.state_change("authenticated",self.me) self._post_auth() return elif not self.authenticated and node.getContent()==self._compute_handshake(): self.peer=self.me n=common_doc.newChild(None,"handshake",None) self._write_node(n) n.unlinkNode() n.freeNode() self.peer_authenticated=1 self.state_change("authenticated",self.peer) self._post_auth() return else: self._send_stream_error("not-authorized") raise FatalComponentStreamError,"Hanshake error." if ns_uri in ("jabber:component:accept","jabber:client","jabber:server"): stanza=stanza_factory(node) self.lock.release() try: self.process_stanza(stanza) finally: self.lock.acquire() stanza.free() return return Stream._process_node(self,node) # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/jabberd/all.py0000644000175000017500000000233611560025462017567 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # pylint: disable-msg=W0611 """Convenience module containing most important objects fr pyxmpp.jabberd package. Suggested usage:: import pyxmpp.jabberd.all (imports all important names into pyxmpp.jabberd namespace)""" __docformat__="restructuredtext en" import pyxmpp.jabberd from pyxmpp.jabberd.componentstream import ComponentStream from pyxmpp.jabberd.component import Component for name in dir(): if not name.startswith("__") and name!="pyxmpp": setattr(pyxmpp.jabberd,name,globals()[name]) # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/roster.py0000644000175000017500000003030711560025462016743 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """XMPP-IM roster handling. Normative reference: - `RFC 3921 `__ """ __docformat__="restructuredtext en" import libxml2 from pyxmpp.xmlextra import common_doc, get_node_ns_uri from pyxmpp.iq import Iq from pyxmpp.jid import JID from pyxmpp.utils import to_utf8,from_utf8 from pyxmpp.objects import StanzaPayloadObject ROSTER_NS="jabber:iq:roster" class RosterItem(StanzaPayloadObject): """ Roster item. Represents part of a roster, or roster update request. """ xml_element_name = "item" xml_element_namespace = ROSTER_NS def __init__(self,node_or_jid,subscription="none",name=None,groups=(),ask=None): """ Initialize a roster item from XML node or jid and optional attributes. :Parameters: - `node_or_jid`: XML node or JID - `subscription`: subscription type ("none", "to", "from" or "both" - `name`: item visible name - `groups`: sequence of groups the item is member of - `ask`: True if there was unreplied subsription or unsubscription request sent.""" if isinstance(node_or_jid,libxml2.xmlNode): self.from_xml(node_or_jid) else: node_or_jid=JID(node_or_jid) if subscription not in ("none","from","to","both","remove"): raise ValueError,"Bad subscription type: %r" % (subscription,) if ask not in ("subscribe",None): raise ValueError,"Bad ask type: %r" % (ask,) self.jid=node_or_jid self.ask=ask self.subscription=subscription self.name=name self.groups=list(groups) def from_xml(self,node): """Initialize RosterItem from XML node.""" if node.type!="element": raise ValueError,"XML node is not a roster item (not en element)" ns=get_node_ns_uri(node) if ns and ns!=ROSTER_NS or node.name!="item": raise ValueError,"XML node is not a roster item" jid=JID(node.prop("jid").decode("utf-8")) subscription=node.prop("subscription") if subscription not in ("none","from","to","both","remove"): subscription="none" ask=node.prop("ask") if ask not in ("subscribe",None): ask=None name=from_utf8(node.prop("name")) groups=[] n=node.children while n: if n.type!="element": n=n.next continue ns=get_node_ns_uri(n) if ns and ns!=ROSTER_NS or n.name!="group": n=n.next continue group=n.getContent() if group: groups.append(from_utf8(group)) n=n.next self.jid=jid self.name=name self.groups=groups self.subscription=subscription self.ask=ask def complete_xml_element(self, xmlnode, _unused): """Complete the XML node with `self` content. Should be overriden in classes derived from `StanzaPayloadObject`. :Parameters: - `xmlnode`: XML node with the element being built. It has already right name and namespace, but no attributes or content. - `_unused`: document to which the element belongs. :Types: - `xmlnode`: `libxml2.xmlNode` - `_unused`: `libxml2.xmlDoc`""" xmlnode.setProp("jid",self.jid.as_utf8()) if self.name: xmlnode.setProp("name",to_utf8(self.name)) xmlnode.setProp("subscription",self.subscription) if self.ask: xmlnode.setProp("ask",to_utf8(self.ask)) for g in self.groups: xmlnode.newTextChild(None, "group", to_utf8(g)) def __str__(self): n=self.as_xml(doc=common_doc) r=n.serialize() n.unlinkNode() n.freeNode() return r def make_roster_push(self): """ Make "roster push" IQ stanza from the item representing roster update request. """ iq=Iq(stanza_type="set") q=iq.new_query(ROSTER_NS) self.as_xml(parent=q, doc=common_doc) return iq class Roster(StanzaPayloadObject): """Class representing XMPP-IM roster. Iteration over `Roster` object iterates over roster items. ``for item in roster: ...`` may be used to iterate over roster items, ``roster[jid]`` to get roster item by jid, ``jid in roster`` to test roster for jid presence. :Ivariables: - `items_dict`: items indexed by JID. :Properties: - `items`: roster items. :Types: - `items_dict`: `dict` of `JID` -> `RosterItem` - `items`: `list` of `RosterItem`""" xml_element_name = "query" xml_element_namespace = ROSTER_NS def __init__(self,node=None,server=False,strict=True): """ Initialize Roster object. `node` should be an XML representation of the roster (e.g. as sent from server in response to roster request). When `node` is None empty roster will be created. If `server` is true the object is considered server-side roster. If `strict` is False, than invalid items in the XML will be ignored. """ self.items_dict={} self.server=server self.node=None if node: self.from_xml(node,strict) def from_xml(self,node,strict=True): """ Initialize Roster object from XML node. If `strict` is False, than invalid items in the XML will be ignored. """ self.items_dict={} if node.type!="element": raise ValueError,"XML node is not a roster (not en element)" ns=get_node_ns_uri(node) if ns and ns!=ROSTER_NS or node.name!="query": raise ValueError,"XML node is not a roster" n=node.children while n: if n.type!="element": n=n.next continue ns=get_node_ns_uri(n) if ns and ns!=ROSTER_NS or n.name!="item": n=n.next continue try: item=RosterItem(n) self.items_dict[item.jid]=item except ValueError: if strict: raise n=n.next def complete_xml_element(self, xmlnode, doc): """Complete the XML node with `self` content. Should be overriden in classes derived from `StanzaPayloadObject`. :Parameters: - `xmlnode`: XML node with the element being built. It has already right name and namespace, but no attributes or content. - `doc`: document to which the element belongs. :Types: - `xmlnode`: `libxml2.xmlNode` - `doc`: `libxml2.xmlDoc`""" for it in self.items_dict.values(): it.as_xml(parent=xmlnode, doc=doc) def __str__(self): n=self.as_xml(doc=common_doc) r=n.serialize() n.unlinkNode() n.freeNode() return r def __iter__(self): return self.items_dict.itervalues() def __contains__(self, jid): return jid in self.items_dict def __getitem__(self, jid): return self.items_dict[jid] def get_items(self): """Return a list of items in the roster.""" return self.items_dict.values() items = property(get_items) def get_groups(self): """Return a list of groups in the roster.""" r={} for it in self.items_dict.values(): it.groups=[g for g in it.groups if g] if it.groups: for g in it.groups: r[g]=True else: r[None]=True return r.keys() def get_items_by_name(self, name, case_sensitive = True): """ Return a list of items with given `name`. If `case_sensitive` is False the matching will be case insensitive. """ if not case_sensitive and name: name = name.lower() r = [] for it in self.items_dict.values(): if it.name == name: r.append(it) elif it.name is None: continue elif not case_sensitive and it.name.lower() == name: r.append(it) return r def get_items_by_group(self,group,case_sensitive=True): """ Return a list of groups with given name. If `case_sensitive` is False the matching will be case insensitive. """ r=[] if not group: for it in self.items_dict.values(): it.groups=[g for g in it.groups if g] if not it.groups: r.append(it) return r if not case_sensitive: group=group.lower() for it in self.items_dict.values(): if group in it.groups: r.append(it) elif not case_sensitive and group in [g.lower() for g in it.groups]: r.append(it) return r def get_item_by_jid(self,jid): """ Return roster item with given `jid`. :raise KeyError: if the item is not found. """ if not jid: raise ValueError,"jid is None" return self.items_dict[jid] def add_item(self,item_or_jid,subscription="none",name=None,groups=(),ask=None): """ Add an item to the roster. The `item_or_jid` argument may be a `RosterItem` object or a `JID`. If it is a JID then `subscription`, `name`, `groups` and `ask` may also be specified. """ if isinstance(item_or_jid,RosterItem): item=item_or_jid if self.items_dict.has_key(item.jid): raise ValueError,"Item already exists" else: if self.items_dict.has_key(item_or_jid): raise ValueError,"Item already exists" if not self.server or subscription not in ("none","from","to","both"): subscription="none" if not self.server: ask=None item=RosterItem(item_or_jid,subscription,name,groups,ask) self.items_dict[item.jid]=item return item def remove_item(self,jid): """Remove item from the roster.""" del self.items_dict[jid] return RosterItem(jid,"remove") def update(self,query): """ Apply an update request to the roster. `query` should be a query included in a "roster push" IQ received. """ ctxt=common_doc.xpathNewContext() ctxt.setContextNode(query) ctxt.xpathRegisterNs("r",ROSTER_NS) item=ctxt.xpathEval("r:item") ctxt.xpathFreeContext() if not item: raise ValueError,"No item to update" item=item[0] item=RosterItem(item) jid=item.jid subscription=item.subscription try: local_item=self.get_item_by_jid(jid) local_item.subscription=subscription except KeyError: if subscription=="remove": return RosterItem(jid,"remove") if self.server or subscription not in ("none","from","to","both"): subscription="none" local_item=RosterItem(jid,subscription) if subscription=="remove": del self.items_dict[local_item.jid] return RosterItem(jid,"remove") local_item.name=item.name local_item.groups=list(item.groups) if not self.server: local_item.ask=item.ask self.items_dict[local_item.jid]=local_item return local_item # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/version.py0000644000175000017500000000002011561501464017101 0ustar jajcususers00000000000000version='1.1.2' pyxmpp-1.1.2/pyxmpp/streambase.py0000644000175000017500000006757711561473626017610 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # pylint: disable-msg=W0201 """Core XMPP stream functionality. Normative reference: - `RFC 3920 `__ """ __docformat__="restructuredtext en" import libxml2 import socket import os import time import random import threading import errno import logging from pyxmpp import xmlextra from pyxmpp.expdict import ExpiringDictionary from pyxmpp.utils import to_utf8, from_utf8 from pyxmpp.stanza import Stanza from pyxmpp.error import StreamErrorNode from pyxmpp.iq import Iq from pyxmpp.presence import Presence from pyxmpp.message import Message from pyxmpp.jid import JID from pyxmpp import resolver from pyxmpp.stanzaprocessor import StanzaProcessor from pyxmpp.exceptions import StreamError, StreamEncryptionRequired from pyxmpp.exceptions import HostMismatch, ProtocolError from pyxmpp.exceptions import DNSError, UnexpectedCNAMEError from pyxmpp.exceptions import FatalStreamError, StreamParseError, StreamAuthenticationError STREAM_NS="http://etherx.jabber.org/streams" BIND_NS="urn:ietf:params:xml:ns:xmpp-bind" def stanza_factory(xmlnode, stream = None): """Creates Iq, Message or Presence object for XML stanza `xmlnode`""" if xmlnode.name=="iq": return Iq(xmlnode, stream = stream) if xmlnode.name=="message": return Message(xmlnode, stream = stream) if xmlnode.name=="presence": return Presence(xmlnode, stream = stream) else: return Stanza(xmlnode, stream = stream) class StreamBase(StanzaProcessor,xmlextra.StreamHandler): """Base class for a generic XMPP stream. Responsible for establishing connection, parsing the stream, dispatching received stanzas to apopriate handlers and sending application's stanzas. This doesn't provide any authentication or encryption (both required by the XMPP specification) and is not usable on its own. Whenever we say "stream" here we actually mean two streams (incoming and outgoing) of one connections, as defined by the XMPP specification. :Ivariables: - `lock`: RLock object used to synchronize access to Stream object. - `features`: stream features as annouced by the initiator. - `me`: local stream endpoint JID. - `peer`: remote stream endpoint JID. - `process_all_stanzas`: when `True` then all stanzas received are considered local. - `initiator`: `True` if local stream endpoint is the initiating entity. - `owner`: `Client`, `Component` or similar object "owning" this stream. - `_reader`: the stream reader object (push parser) for the stream. """ def __init__(self, default_ns, extra_ns = (), keepalive = 0, owner = None): """Initialize Stream object :Parameters: - `default_ns`: stream's default namespace ("jabber:client" for client, "jabber:server" for server, etc.) - `extra_ns`: sequence of extra namespace URIs to be defined for the stream. - `keepalive`: keepalive output interval. 0 to disable. - `owner`: `Client`, `Component` or similar object "owning" this stream. """ StanzaProcessor.__init__(self) xmlextra.StreamHandler.__init__(self) self.default_ns_uri=default_ns if extra_ns: self.extra_ns_uris=extra_ns else: self.extra_ns_uris=[] self.keepalive=keepalive self._reader_lock=threading.Lock() self.process_all_stanzas=False self.port=None self._reset() self.owner = owner self.__logger=logging.getLogger("pyxmpp.Stream") def _reset(self): """Reset `Stream` object state making it ready to handle new connections.""" self.doc_in=None self.doc_out=None self.socket=None self._reader=None self.addr=None self.default_ns=None self.extra_ns={} self.stream_ns=None self._reader=None self.ioreader=None self.me=None self.peer=None self.skip=False self.stream_id=None self._iq_response_handlers=ExpiringDictionary() self._iq_get_handlers={} self._iq_set_handlers={} self._message_handlers=[] self._presence_handlers=[] self.eof=False self.initiator=None self.features=None self.authenticated=False self.peer_authenticated=False self.auth_method_used=None self.version=None self.last_keepalive=False def __del__(self): self.close() def _connect_socket(self,sock,to=None): """Initialize stream on outgoing connection. :Parameters: - `sock`: connected socket for the stream - `to`: name of the remote host """ self.eof=0 self.socket=sock if to: self.peer=JID(to) else: self.peer=None self.initiator=1 self._send_stream_start() self._make_reader() def connect(self,addr,port,service=None,to=None): """Establish XMPP connection with given address. [initiating entity only] :Parameters: - `addr`: peer name or IP address - `port`: port number to connect to - `service`: service name (to be resolved using SRV DNS records) - `to`: peer name if different than `addr` """ self.lock.acquire() try: return self._connect(addr,port,service,to) finally: self.lock.release() def _connect(self, addr, port, service = None, to = None): """Same as `Stream.connect` but assume `self.lock` is acquired.""" if to is None: to = from_utf8(addr) allow_cname = True if service is not None: self.state_change("resolving srv", (addr, service)) addrs = resolver.resolve_srv(addr, service) if not addrs: addrs = [(addr, port)] else: allow_cname = False else: addrs=[(addr, port)] exception = None for addr, port in addrs: if type(addr) in (str, unicode): self.state_change("resolving", addr) s=None while True: try: addresses = resolver.getaddrinfo(addr, port, None, socket.SOCK_STREAM, allow_cname = allow_cname) break except UnexpectedCNAMEError, err: self.__logger.warning(str(err)) allow_cname = True continue except DNSError, err: self.__logger.debug(str(err)) exception = err addresses = [] break for res in addresses: family, socktype, proto, _unused, sockaddr = res try: s=socket.socket(family,socktype,proto) self.state_change("connecting",sockaddr) s.connect(sockaddr) self.state_change("connected",sockaddr) except socket.error, err: exception = err self.__logger.debug("Connect to %r failed" % (sockaddr,)) if s: s.close() s=None continue break if s: break if not s: if exception: raise exception else: raise FatalStreamError,"Cannot connect" self.addr=addr self.port=port self._connect_socket(s, to) self.last_keepalive=time.time() def accept(self,sock,myname): """Accept incoming connection. [receiving entity only] :Parameters: - `sock`: a listening socket. - `myname`: local stream endpoint name.""" self.lock.acquire() try: return self._accept(sock,myname) finally: self.lock.release() def _accept(self,sock,myname): """Same as `Stream.accept` but assume `self.lock` is acquired.""" self.eof=0 self.socket,addr=sock.accept() self.__logger.debug("Connection from: %r" % (addr,)) self.addr,self.port=addr if myname: self.me=JID(myname) else: self.me=None self.initiator=0 self._make_reader() self.last_keepalive=time.time() def disconnect(self): """Gracefully close the connection.""" self.lock.acquire() try: return self._disconnect() finally: self.lock.release() def _disconnect(self): """Same as `Stream.disconnect` but assume `self.lock` is acquired.""" if self.doc_out: self._send_stream_end() def _post_connect(self): """Called when connection is established. This method is supposed to be overriden in derived classes.""" pass def _post_auth(self): """Called when connection is authenticated. This method is supposed to be overriden in derived classes.""" pass def state_change(self,state,arg): """Called when connection state is changed. This method is supposed to be overriden in derived classes or replaced by an application. It may be used to display the connection progress.""" self.__logger.debug("State: %s: %r" % (state,arg)) def close(self): """Forcibly close the connection and clear the stream state.""" self.lock.acquire() try: return self._close() finally: self.lock.release() def _close(self): """Same as `Stream.close` but assume `self.lock` is acquired.""" self._disconnect() if self.doc_in: self.doc_in=None if self.features: self.features=None self._reader=None self.stream_id=None if self.socket: self.socket.close() self._reset() def _make_reader(self): """Create ne `xmlextra.StreamReader` instace as `self._reader`.""" self._reader=xmlextra.StreamReader(self) def stream_start(self,doc): """Process (stream start) tag received from peer. :Parameters: - `doc`: document created by the parser""" self.doc_in=doc self.__logger.debug("input document: %r" % (self.doc_in.serialize(),)) try: r=self.doc_in.getRootElement() if r.ns().getContent() != STREAM_NS: self._send_stream_error("invalid-namespace") raise FatalStreamError,"Invalid namespace." except libxml2.treeError: self._send_stream_error("invalid-namespace") raise FatalStreamError,"Couldn't get the namespace." self.version=r.prop("version") if self.version and self.version!="1.0": self._send_stream_error("unsupported-version") raise FatalStreamError,"Unsupported protocol version." to_from_mismatch=0 if self.initiator: self.stream_id=r.prop("id") peer = from_utf8(r.prop("from")) if peer: peer = JID(peer) if self.peer: if peer and peer!=self.peer: self.__logger.debug("peer hostname mismatch:" " %r != %r" % (peer,self.peer)) to_from_mismatch=1 else: self.peer=peer else: to = from_utf8(r.prop("to")) if to: to=self.check_to(to) if not to: self._send_stream_error("host-unknown") raise FatalStreamError,'Bad "to"' self.me=JID(to) self._send_stream_start(self.generate_id()) self._send_stream_features() self.state_change("fully connected",self.peer) self._post_connect() if not self.version: self.state_change("fully connected",self.peer) self._post_connect() if to_from_mismatch: raise HostMismatch def stream_end(self, _unused): """Process (stream end) tag received from peer. :Parameters: - `_unused`: document created by the parser""" self.__logger.debug("Stream ended") self.eof=1 if self.doc_out: self._send_stream_end() if self.doc_in: self.doc_in=None self._reader=None if self.features: self.features=None self.state_change("disconnected",self.peer) def stanza_start(self,doc,node): """Process stanza (first level child element of the stream) start tag -- do nothing. :Parameters: - `doc`: parsed document - `node`: stanza's full XML """ pass def stanza(self, _unused, node): """Process stanza (first level child element of the stream). :Parameters: - `_unused`: parsed document - `node`: stanza's full XML """ self._process_node(node) def error(self,descr): """Handle stream XML parse error. :Parameters: - `descr`: error description """ raise StreamParseError,descr def _send_stream_end(self): """Send stream end tag.""" self.doc_out.getRootElement().addContent(" ") s=self.doc_out.getRootElement().serialize(encoding="UTF-8") end=s.rindex("<") try: self._write_raw(s[end:]) except (IOError,SystemError,socket.error),e: self.__logger.debug("Sending stream closing tag failed:"+str(e)) self.doc_out.freeDoc() self.doc_out=None if self.features: self.features=None def _send_stream_start(self,sid=None): """Send stream start tag.""" if self.doc_out: raise StreamError,"Stream start already sent" self.doc_out=libxml2.newDoc("1.0") root=self.doc_out.newChild(None, "stream", None) self.stream_ns=root.newNs(STREAM_NS,"stream") root.setNs(self.stream_ns) self.default_ns=root.newNs(self.default_ns_uri,None) for prefix,uri in self.extra_ns: self.extra_ns[uri]=root.newNs(uri,prefix) if self.peer and self.initiator: root.setProp("to",self.peer.as_utf8()) if self.me and not self.initiator: root.setProp("from",self.me.as_utf8()) root.setProp("version","1.0") if sid: root.setProp("id",sid) self.stream_id=sid sr=self.doc_out.serialize(encoding="UTF-8") self._write_raw(sr[:sr.find("/>")]+">") def _send_stream_error(self,condition): """Send stream error element. :Parameters: - `condition`: stream error condition name, as defined in the XMPP specification.""" if not self.doc_out: self._send_stream_start() e=StreamErrorNode(condition) e.xmlnode.setNs(self.stream_ns) self._write_raw(e.serialize()) e.free() self._send_stream_end() def _restart_stream(self): """Restart the stream as needed after SASL and StartTLS negotiation.""" self._reader=None #self.doc_out.freeDoc() self.doc_out=None #self.doc_in.freeDoc() # memleak, but the node which caused the restart # will be freed after this function returns self.doc_in=None self.features=None if self.initiator: self._send_stream_start(self.stream_id) self._make_reader() def _make_stream_features(self): """Create the element for the stream. [receving entity only] :returns: new element node.""" root=self.doc_out.getRootElement() features=root.newChild(root.ns(),"features",None) return features def _send_stream_features(self): """Send stream . [receiving entity only]""" self.features=self._make_stream_features() self._write_raw(self.features.serialize(encoding="UTF-8")) def write_raw(self,data): """Write raw data to the stream socket. :Parameters: - `data`: data to send""" self.lock.acquire() try: return self._write_raw(data) finally: self.lock.release() def _write_raw(self,data): """Same as `Stream.write_raw` but assume `self.lock` is acquired.""" logging.getLogger("pyxmpp.Stream.out").debug("OUT: %r",data) try: while data: sent = self.socket.send(data) data = data[sent:] except (IOError,OSError,socket.error),e: raise FatalStreamError("IO Error: "+str(e)) def _write_node(self,xmlnode): """Write XML `xmlnode` to the stream. :Parameters: - `xmlnode`: XML node to send.""" if self.eof or not self.socket or not self.doc_out: self.__logger.debug("Dropping stanza: %r" % (xmlnode,)) return xmlnode=xmlnode.docCopyNode(self.doc_out,1) self.doc_out.addChild(xmlnode) try: ns = xmlnode.ns() except libxml2.treeError: ns = None if ns and ns.content == xmlextra.COMMON_NS: xmlextra.replace_ns(xmlnode, ns, self.default_ns) s = xmlextra.safe_serialize(xmlnode) self._write_raw(s) xmlnode.unlinkNode() xmlnode.freeNode() def send(self,stanza): """Write stanza to the stream. :Parameters: - `stanza`: XMPP stanza to send.""" self.lock.acquire() try: return self._send(stanza) finally: self.lock.release() def _send(self,stanza): """Same as `Stream.send` but assume `self.lock` is acquired.""" if not self.version: try: err = stanza.get_error() except ProtocolError: err = None if err: err.downgrade() self.fix_out_stanza(stanza) self._write_node(stanza.xmlnode) def idle(self): """Do some housekeeping (cache expiration, timeout handling). This method should be called periodically from the application's main loop.""" self.lock.acquire() try: return self._idle() finally: self.lock.release() def _idle(self): """Same as `Stream.idle` but assume `self.lock` is acquired.""" self._iq_response_handlers.expire() if not self.socket or self.eof: return now=time.time() if self.keepalive and now-self.last_keepalive>=self.keepalive: self._write_raw(" ") self.last_keepalive=now def fileno(self): """Return filedescriptor of the stream socket.""" self.lock.acquire() try: return self.socket.fileno() finally: self.lock.release() def loop(self,timeout): """Simple "main loop" for the stream.""" self.lock.acquire() try: while not self.eof and self.socket is not None: act=self._loop_iter(timeout) if not act: self._idle() finally: self.lock.release() def loop_iter(self,timeout): """Single iteration of a simple "main loop" for the stream.""" self.lock.acquire() try: return self._loop_iter(timeout) finally: self.lock.release() def _loop_iter(self,timeout): """Same as `Stream.loop_iter` but assume `self.lock` is acquired.""" import select self.lock.release() try: if not self.socket: time.sleep(timeout) return False try: ifd, _unused, efd = select.select( [self.socket], [], [self.socket], timeout ) except select.error,e: if e.args[0]!=errno.EINTR: raise ifd, _unused, efd=[], [], [] finally: self.lock.acquire() if self.socket in ifd or self.socket in efd: self._process() return True else: return False def process(self): """Process stream's pending events. Should be called whenever there is input available on `self.fileno()` socket descriptor. Is called by `self.loop_iter`.""" self.lock.acquire() try: self._process() finally: self.lock.release() def _process(self): """Same as `Stream.process` but assume `self.lock` is acquired.""" try: try: self._read() except (xmlextra.error,),e: self.__logger.exception("Exception during read()") raise StreamParseError(unicode(e)) except: raise except (IOError,OSError,socket.error),e: self.close() raise FatalStreamError("IO Error: "+str(e)) except (FatalStreamError,KeyboardInterrupt,SystemExit),e: self.close() raise def _read(self): """Read data pending on the stream socket and pass it to the parser.""" self.__logger.debug("StreamBase._read(), socket: %r",self.socket) if self.eof: return try: r=self.socket.recv(1024) except socket.error,e: if e.args[0]!=errno.EINTR: raise return self._feed_reader(r) def _feed_reader(self,data): """Feed the stream reader with data received. If `data` is None or empty, then stream end (peer disconnected) is assumed and the stream is closed. :Parameters: - `data`: data received from the stream socket. :Types: - `data`: `unicode` """ logging.getLogger("pyxmpp.Stream.in").debug("IN: %r",data) if data: try: r=self._reader.feed(data) while r: r=self._reader.feed("") if r is None: self.eof=1 self.disconnect() except StreamParseError: self._send_stream_error("xml-not-well-formed") raise else: self.eof=1 self.disconnect() if self.eof: self.stream_end(None) def _process_node(self,xmlnode): """Process first level element of the stream. The element may be stream error or features, StartTLS request/response, SASL request/response or a stanza. :Parameters: - `xmlnode`: XML node describing the element """ ns_uri=xmlnode.ns().getContent() if ns_uri=="http://etherx.jabber.org/streams": self._process_stream_node(xmlnode) return if ns_uri==self.default_ns_uri: stanza=stanza_factory(xmlnode, self) self.lock.release() try: self.process_stanza(stanza) finally: self.lock.acquire() stanza.free() else: self.__logger.debug("Unhandled node: %r" % (xmlnode.serialize(),)) def _process_stream_node(self,xmlnode): """Process first level stream-namespaced element of the stream. The element may be stream error or stream features. :Parameters: - `xmlnode`: XML node describing the element """ if xmlnode.name=="error": e=StreamErrorNode(xmlnode) self.lock.release() try: self.process_stream_error(e) finally: self.lock.acquire() e.free() return elif xmlnode.name=="features": self.__logger.debug("Got stream features") self.__logger.debug("Node: %r" % (xmlnode,)) self.features=xmlnode.copyNode(1) self.doc_in.addChild(self.features) self._got_features() return self.__logger.debug("Unhandled stream node: %r" % (xmlnode.serialize(),)) def process_stream_error(self,err): """Process stream error element received. :Types: - `err`: `StreamErrorNode` :Parameters: - `err`: error received """ self.__logger.debug("Unhandled stream error: condition: %s %r" % (err.get_condition().name,err.serialize())) def check_to(self,to): """Check "to" attribute of received stream header. :return: `to` if it is equal to `self.me`, None otherwise. Should be overriden in derived classes which require other logic for handling that attribute.""" if to!=self.me: return None return to def generate_id(self): """Generate a random and unique stream ID. :return: the id string generated.""" return "%i-%i-%s" % (os.getpid(),time.time(),str(random.random())[2:]) def _got_features(self): """Process incoming element. [initiating entity only] The received features node is available in `self.features`.""" ctxt = self.doc_in.xpathNewContext() ctxt.setContextNode(self.features) ctxt.xpathRegisterNs("stream",STREAM_NS) ctxt.xpathRegisterNs("bind",BIND_NS) bind_n=None try: bind_n=ctxt.xpathEval("bind:bind") finally: ctxt.xpathFreeContext() if self.authenticated: if bind_n: self.bind(self.me.resource) else: self.state_change("authorized",self.me) def bind(self,resource): """Bind to a resource. [initiating entity only] :Parameters: - `resource`: the resource name to bind to. XMPP stream is authenticated for bare JID only. To use the full JID it must be bound to a resource. """ iq=Iq(stanza_type="set") q=iq.new_query(BIND_NS, u"bind") if resource: q.newTextChild(None,"resource",to_utf8(resource)) self.state_change("binding",resource) self.set_response_handlers(iq,self._bind_success,self._bind_error) self.send(iq) iq.free() def _bind_success(self,stanza): """Handle resource binding success. [initiating entity only] :Parameters: - `stanza`: stanza received. Set `self.me` to the full JID negotiated.""" jid_n=stanza.xpath_eval("bind:bind/bind:jid",{"bind":BIND_NS}) if jid_n: self.me=JID(jid_n[0].getContent().decode("utf-8")) self.state_change("authorized",self.me) def _bind_error(self,stanza): """Handle resource binding success. [initiating entity only] :raise FatalStreamError:""" raise FatalStreamError,"Resource binding failed" def connected(self): """Check if stream is connected. :return: True if stream connection is active.""" if self.doc_in and self.doc_out and not self.eof: return True else: return False # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/__init__.py0000644000175000017500000000544311560025462017167 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """ PyXMPP - Jabber/XMPP protocol implementation ============================================ Conventions ----------- PyXMPP is object-oriented, most of its fetures are implemented via classes, defined in various pyxmpp modules. The API is very asynchronous -- often requested objects are not returned immediately, but instead a callback is called when the object is available or an event occurs. As python is not a strongly-typed language so the parameter and attribute types shown in this documentation are not enforced, but those types are expected by the package and others may simply not work or stop working in future releases of PyXMPP. Module hierarchy ................ Base XMPP features (`RFC 3920 `__, `RFC 3921 `__) are implemented in direct submodules of `pyxmpp` package. Most `JSF `__ defined extensions are defined in `pyxmpp.jabber` package and modules for server components are placed in `pyxmpp.jabberd`. For convenience most important names (classes for application use) may be imported into `pyxmpp`, `pyxmpp.jabber` or `pyxmpp.jabberd` packages. To do that `pyxmpp.all`, `pyxmpp.jabber.all` or `pyxmpp.jabberd.all` must be imported. One doesn't have to remember any other module name then. Constructors ............ Most of PyXMPP object constructors are polymorphic. That means they accept different types and number of arguments to create object from various input. Usually the first argument may be an XML node to parse/wrap into the object or parameters needed to create a new object from scratch. E.g. `pyxmpp.stanza.Stanza` constructor accepts single `libxml2.xmlNode` argument with XML stanza or set of keyword arguments (from_jid, to_jid, stanza_type, etc.) to create such XML stanza. Most of the constructors will also accept instance of their own class to create a copy of it. Common methods .............. Most objects describing elements of the XMPP protocol or its extensions have method as_xml() providing their XML representations. """ __docformat__="restructuredtext en" # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/xmppstringprep.py0000644000175000017500000002060511560025462020527 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # pylint treats "import stringprep" like depreciated "import string" # pylint: disable-msg=W0402 """Nodeprep and resourceprep stringprep profiles. Normative reference: - `RFC 3920 `__ """ __docformat__="restructuredtext en" import stringprep import unicodedata from pyxmpp.exceptions import StringprepError class LookupFunction: """Class for looking up RFC 3454 tables using function. :Ivariables: - `lookup`: the lookup function.""" def __init__(self,function): """Initialize `LookupFunction` object. :Parameters: - `function`: function taking character code as input and returning `bool` value or the mapped for `code`.""" self.lookup=function class LookupTable: """Class for looking up RFC 3454 tables using a dictionary and/or list of ranges.""" def __init__(self,singles,ranges): """Initialize `LookupTable` object. :Parameters: - `singles`: dictionary mapping Unicode characters into other Unicode characters. - `ranges`: list of ``((start,end),value)`` tuples mapping codes in range (start,end) to the value.""" self.singles=singles self.ranges=ranges def lookup(self,c): """Do Unicode character lookup. :Parameters: - `c`: Unicode character to look up. :return: the mapped value.""" if self.singles.has_key(c): return self.singles[c] c=ord(c) for (start,end),value in self.ranges: if c=stringprep_cache_size: remove=self.cache_items[:-stringprep_cache_size/2] for profile,key in remove: try: del profile.cache[key] except KeyError: pass self.cache_items[:]=self.cache_items[-stringprep_cache_size/2:] self.cache_items.append((self,data)) self.cache[data]=s return s def prepare_query(self,s): """Complete string preparation procedure for 'query' strings. (without checks for unassigned codes) :Parameters: - `s`: Unicode string to prepare. :return: prepared string :raise StringprepError: if the preparation fails """ s=self.map(s) if self.normalization: s=self.normalization(s) s=self.prohibit(s) if self.bidi: s=self.check_bidi(s) if type(s) is list: s=u"".string.join(s) return s def map(self,s): """Mapping part of string preparation.""" r=[] for c in s: rc=None for t in self.mapping: rc=t.lookup(c) if rc is not None: break if rc is not None: r.append(rc) else: r.append(c) return r def prohibit(self,s): """Checks for prohibited characters.""" for c in s: for t in self.prohibited: if t.lookup(c): raise StringprepError,"Prohibited character: %r" % (c,) return s def check_unassigned(self,s): """Checks for unassigned character codes.""" for c in s: for t in self.unassigned: if t.lookup(c): raise StringprepError,"Unassigned character: %r" % (c,) return s def check_bidi(self,s): """Checks if sting is valid for bidirectional printing.""" has_l=0 has_ral=0 for c in s: if D_1.lookup(c): has_ral=1 elif D_2.lookup(c): has_l=1 if has_l and has_ral: raise StringprepError,"Both RandALCat and LCat characters present" if has_l and (D_1.lookup(s[0]) is None or D_1.lookup(s[-1]) is None): raise StringprepError,"The first and the last character must be RandALCat" return s nodeprep=Profile( unassigned=(A_1,), mapping=(B_1,B_2), normalization=nfkc, prohibited=(C_1_1,C_1_2,C_2_1,C_2_2,C_3,C_4,C_5,C_6,C_7,C_8,C_9, LookupTable({u'"':True,u'&':True,u"'":True,u"/":True, u":":True,u"<":True,u">":True,u"@":True},()) ), bidi=1) resourceprep=Profile( unassigned=(A_1,), mapping=(B_1,), normalization=nfkc, prohibited=(C_1_2,C_2_1,C_2_2,C_3,C_4,C_5,C_6,C_7,C_8,C_9), bidi=1) stringprep_cache_size=1000 def set_stringprep_cache_size(size): """Modify stringprep cache size. :Parameters: - `size`: new cache size""" global stringprep_cache_size stringprep_cache_size=size if len(Profile.cache_items)>size: remove=Profile.cache_items[:-size] for profile,key in remove: try: del profile.cache[key] except KeyError: pass Profile.cache_items=Profile.cache_items[-size:] # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/message.py0000644000175000017500000001254011560025462017050 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Message XMPP stanza handling Normative reference: - `RFC 3920 `__ """ __docformat__="restructuredtext en" import libxml2 from pyxmpp.stanza import Stanza from pyxmpp.utils import to_utf8,from_utf8 from pyxmpp.xmlextra import common_ns message_types=("normal","chat","headline","error","groupchat") class Message(Stanza): """Wraper object for stanzas.""" stanza_type="message" def __init__(self, xmlnode = None, from_jid = None, to_jid = None, stanza_type = None, stanza_id = None, subject = None, body = None, thread = None, error = None, error_cond = None, stream = None): """Initialize a `Message` object. :Parameters: - `xmlnode`: XML node to_jid be wrapped into the `Message` object or other Message object to be copied. If not given then new presence stanza is created using following parameters. - `from_jid`: sender JID. - `to_jid`: recipient JID. - `stanza_type`: staza type: one of: "get", "set", "result" or "error". - `stanza_id`: stanza id -- value of stanza's "id" attribute. If not given, then unique for the session value is generated. - `subject`: message subject, - `body`: message body. - `thread`: message thread id. - `error_cond`: error condition name. Ignored if `stanza_type` is not "error". :Types: - `xmlnode`: `unicode` or `libxml2.xmlNode` or `Stanza` - `from_jid`: `JID` - `to_jid`: `JID` - `stanza_type`: `unicode` - `stanza_id`: `unicode` - `subject`: `unicode` - `body`: `unicode` - `thread`: `unicode` - `error_cond`: `unicode`""" self.xmlnode=None if isinstance(xmlnode,Message): pass elif isinstance(xmlnode,Stanza): raise TypeError, "Couldn't make Message from other Stanza" elif isinstance(xmlnode,libxml2.xmlNode): pass elif xmlnode is not None: raise TypeError, "Couldn't make Message from %r" % (type(xmlnode),) if xmlnode is None: xmlnode="message" Stanza.__init__(self, xmlnode, from_jid = from_jid, to_jid = to_jid, stanza_type = stanza_type, stanza_id = stanza_id, error = error, error_cond = error_cond, stream = stream) if subject is not None: self.xmlnode.newTextChild(common_ns,"subject",to_utf8(subject)) if body is not None: self.xmlnode.newTextChild(common_ns,"body",to_utf8(body)) if thread is not None: self.xmlnode.newTextChild(common_ns,"thread",to_utf8(thread)) def get_subject(self): """Get the message subject. :return: the message subject or `None` if there is no subject. :returntype: `unicode`""" n=self.xpath_eval("ns:subject") if n: return from_utf8(n[0].getContent()) else: return None def get_thread(self): """Get the thread-id subject. :return: the thread-id or `None` if there is no thread-id. :returntype: `unicode`""" n=self.xpath_eval("ns:thread") if n: return from_utf8(n[0].getContent()) else: return None def copy(self): """Create a deep copy of the message stanza. :returntype: `Message`""" return Message(self) def get_body(self): """Get the body of the message. :return: the body of the message or `None` if there is no body. :returntype: `unicode`""" n=self.xpath_eval("ns:body") if n: return from_utf8(n[0].getContent()) else: return None def make_error_response(self,cond): """Create error response for any non-error message stanza. :Parameters: - `cond`: error condition name, as defined in XMPP specification. :return: new message stanza with the same "id" as self, "from" and "to" attributes swapped, type="error" and containing element plus payload of `self`. :returntype: `unicode`""" if self.get_type() == "error": raise ValueError, "Errors may not be generated in response to errors" m=Message(stanza_type="error",from_jid=self.get_to(),to_jid=self.get_from(), stanza_id=self.get_id(),error_cond=cond) if self.xmlnode.children: n=self.xmlnode.children while n: m.xmlnode.children.addPrevSibling(n.copyNode(1)) n=n.next return m # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/presence.py0000644000175000017500000002221111560025462017224 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Presence XMPP stanza handling Normative reference: - `RFC 3920 `__ """ __docformat__="restructuredtext en" import libxml2 from pyxmpp.utils import to_utf8,from_utf8 from pyxmpp.stanza import Stanza from pyxmpp.xmlextra import common_ns presence_types=("available","unavailable","probe","subscribe","unsubscribe","subscribed", "unsubscribed","invisible","error") accept_responses={ "subscribe": "subscribed", "subscribed": "subscribe", "unsubscribe": "unsubscribed", "unsubscribed": "unsubscribe", } deny_responses={ "subscribe": "unsubscribed", "subscribed": "unsubscribe", "unsubscribe": "subscribed", "unsubscribed": "subscribe", } class Presence(Stanza): """Wraper object for stanzas.""" stanza_type="presence" def __init__(self, xmlnode = None, from_jid = None, to_jid = None, stanza_type = None, stanza_id = None, show = None, status = None, priority = 0, error = None, error_cond = None, stream = None): """Initialize a `Presence` object. :Parameters: - `xmlnode`: XML node to_jid be wrapped into the `Presence` object or other Presence object to be copied. If not given then new presence stanza is created using following parameters. - `from_jid`: sender JID. - `to_jid`: recipient JID. - `stanza_type`: staza type: one of: None, "available", "unavailable", "subscribe", "subscribed", "unsubscribe", "unsubscribed" or "error". "available" is automaticaly changed to_jid None. - `stanza_id`: stanza id -- value of stanza's "id" attribute - `show`: "show" field of presence stanza. One of: None, "away", "xa", "dnd", "chat". - `status`: descriptive text for the presence stanza. - `priority`: presence priority. - `error_cond`: error condition name. Ignored if `stanza_type` is not "error" :Types: - `xmlnode`: `unicode` or `libxml2.xmlNode` or `Stanza` - `from_jid`: `JID` - `to_jid`: `JID` - `stanza_type`: `unicode` - `stanza_id`: `unicode` - `show`: `unicode` - `status`: `unicode` - `priority`: `unicode` - `error_cond`: `unicode`""" self.xmlnode=None if isinstance(xmlnode,Presence): pass elif isinstance(xmlnode,Stanza): raise TypeError,"Couldn't make Presence from other Stanza" elif isinstance(xmlnode,libxml2.xmlNode): pass elif xmlnode is not None: raise TypeError,"Couldn't make Presence from %r" % (type(xmlnode),) if stanza_type and stanza_type not in presence_types: raise ValueError, "Invalid presence type: %r" % (type,) if stanza_type=="available": stanza_type=None if xmlnode is None: xmlnode="presence" Stanza.__init__(self, xmlnode, from_jid = from_jid, to_jid = to_jid, stanza_type = stanza_type, stanza_id = stanza_id, error = error, error_cond = error_cond, stream = stream) if show: self.xmlnode.newTextChild(common_ns,"show",to_utf8(show)) if status: self.xmlnode.newTextChild(common_ns,"status",to_utf8(status)) if priority and priority!=0: self.xmlnode.newTextChild(common_ns,"priority",to_utf8(unicode(priority))) def copy(self): """Create a deep copy of the presence stanza. :returntype: `Presence`""" return Presence(self) def set_status(self,status): """Change presence status description. :Parameters: - `status`: descriptive text for the presence stanza. :Types: - `status`: `unicode`""" n=self.xpath_eval("ns:status") if not status: if n: n[0].unlinkNode() n[0].freeNode() else: return if n: n[0].setContent(to_utf8(status)) else: self.xmlnode.newTextChild(common_ns,"status",to_utf8(status)) def get_status(self): """Get presence status description. :return: value of stanza's field. :returntype: `unicode`""" n=self.xpath_eval("ns:status") if n: return from_utf8(n[0].getContent()) else: return None def get_show(self): """Get presence "show" field. :return: value of stanza's field. :returntype: `unicode`""" n=self.xpath_eval("ns:show") if n: return from_utf8(n[0].getContent()) else: return None def set_show(self,show): """Change presence "show" field. :Parameters: - `show`: new value for the "show" field of presence stanza. One of: None, "away", "xa", "dnd", "chat". :Types: - `show`: `unicode`""" n=self.xpath_eval("ns:show") if not show: if n: n[0].unlinkNode() n[0].freeNode() else: return if n: n[0].setContent(to_utf8(show)) else: self.xmlnode.newTextChild(common_ns,"show",to_utf8(show)) def get_priority(self): """Get presence priority. :return: value of stanza's priority. 0 if the stanza doesn't contain element. :returntype: `int`""" n=self.xpath_eval("ns:priority") if not n: return 0 try: prio=int(n[0].getContent()) except ValueError: return 0 return prio def set_priority(self,priority): """Change presence priority. :Parameters: - `priority`: new presence priority. :Types: - `priority`: `int`""" n=self.xpath_eval("ns:priority") if not priority: if n: n[0].unlinkNode() n[0].freeNode() else: return priority=int(priority) if priority<-128 or priority>127: raise ValueError, "Bad priority value" priority=str(priority) if n: n[0].setContent(priority) else: self.xmlnode.newTextChild(common_ns,"priority",priority) def make_accept_response(self): """Create "accept" response for the "subscribe"/"subscribed"/"unsubscribe"/"unsubscribed" presence stanza. :return: new stanza. :returntype: `Presence`""" if self.get_type() not in ("subscribe","subscribed","unsubscribe","unsubscribed"): raise ValueError, ("Results may only be generated for 'subscribe'," "'subscribed','unsubscribe' or 'unsubscribed' presence") pr=Presence(stanza_type=accept_responses[self.get_type()], from_jid=self.get_to(),to_jid=self.get_from(),stanza_id=self.get_id()) return pr def make_deny_response(self): """Create "deny" response for the "subscribe"/"subscribed"/"unsubscribe"/"unsubscribed" presence stanza. :return: new presence stanza. :returntype: `Presence`""" if self.get_type() not in ("subscribe","subscribed","unsubscribe","unsubscribed"): raise ValueError, ("Results may only be generated for 'subscribe'," "'subscribed','unsubscribe' or 'unsubscribed' presence") pr=Presence(stanza_type=deny_responses[self.get_type()], from_jid=self.get_to(),to_jid=self.get_from(),stanza_id=self.get_id()) return pr def make_error_response(self,cond): """Create error response for the any non-error presence stanza. :Parameters: - `cond`: error condition name, as defined in XMPP specification. :Types: - `cond`: `unicode` :return: new presence stanza. :returntype: `Presence`""" if self.get_type() == "error": raise ValueError, "Errors may not be generated in response to errors" p=Presence(stanza_type="error",from_jid=self.get_to(),to_jid=self.get_from(), stanza_id=self.get_id(),error_cond=cond) if self.xmlnode.children: n=self.xmlnode.children while n: p.xmlnode.children.addPrevSibling(n.copyNode(1)) n=n.next return p # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/interface_micro_impl.py0000644000175000017500000001073511560025462021602 0ustar jajcususers00000000000000# # (C) Copyright 2006 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Interface API, minimal implementation. This is minimal Zope Interfaces API implementation, as required by PyXMPP, not add another dependencies. If zope.interface package is available it will be used instead of this one. Never import this module directly.""" __docformat__="restructuredtext en" import sys from types import FunctionType def classImplements(cls, *interfaces): if not isinstance(cls, classobj): raise TypeError, "%r is not a class" for interface in interfaces: if not isinstance(interface, InterfaceClass): raise TypeError, "Only interfaces may be implemented" cls.__implemented__ = tuple(interfaces) def implements(*interfaces): for interface in interfaces: if not isinstance(interface, InterfaceClass): raise TypeError, "Only interfaces may be implemented" frame = sys._getframe(1) locals = frame.f_locals if (locals is frame.f_globals) or ('__module__' not in locals): raise TypeError, "implements() may only be used in a class definition" if "__implemented__" in locals: raise TypeError, "implements() may be used only once" locals["__implemented__"] = tuple(interfaces) def _whole_tree(cls): yield cls for base in cls.__bases__: for b in _whole_tree(base): yield b def implementedBy(cls): try: for interface in cls.__implemented__: for c in _whole_tree(interface): yield c except AttributeError: pass for base in cls.__bases__: for interface in implementedBy(base): yield interface def providedBy(ob): try: for interface in ob.__provides__: yield interface except AttributeError: try: for interface in implementedBy(ob.__class__): yield interface except AttributeError: return class InterfaceClass(object): def __init__(self, name, bases = (), attrs = None, __doc__ = None, __module__ = None): if __module__ is None: if (attrs is not None and ('__module__' in attrs) and isinstance(attrs['__module__'], str)): __module__ = attrs['__module__'] del attrs['__module__'] else: __module__ = sys._getframe(1).f_globals['__name__'] if __doc__ is not None: self.__doc__ = __doc__ if attrs is not None and "__doc__" in attrs: del attrs["__doc__"] self.__module__ = __module__ for base in bases: if not isinstance(base, InterfaceClass): raise TypeError, 'Interface bases must be Interfaces' if attrs is not None: for aname, attr in attrs.items(): if not isinstance(attr, Attribute) and type(attr) is not FunctionType: raise TypeError, 'Interface attributes must be Attributes o functions (%r found in %s)' % (attr, aname) self.__bases__ = bases self.__attrs = attrs self.__name__ = name self.__identifier__ = "%s.%s" % (self.__module__, self.__name__) def providedBy(self, ob): """Is the interface implemented by an object""" if self in providedBy(ob): return True return False def implementedBy(self, cls): """Do instances of the given class implement the interface?""" return self in implementedBy(cls) def __repr__(self): name = self.__name__ module = self.__module__ if module and module != "__main__": name = "%s.%s" % (module, name) return "<%s %s>" % (self.__class__.__name__, name) class Attribute(object): def __init__(self, doc): self.__doc__ = doc Interface = InterfaceClass("Interface", __module__ = "pyxmpp.inteface_micro_impl") # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/sasl/0000755000175000017500000000000011561501464016014 5ustar jajcususers00000000000000pyxmpp-1.1.2/pyxmpp/sasl/plain.py0000644000175000017500000001350411560025462017472 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """PLAIN authentication mechanism for PyXMPP SASL implementation. Normative reference: - `RFC 2595 `__ """ __docformat__="restructuredtext en" import logging from pyxmpp.utils import to_utf8,from_utf8 from pyxmpp.sasl.core import ClientAuthenticator,ServerAuthenticator from pyxmpp.sasl.core import Success,Failure,Challenge,Response class PlainClientAuthenticator(ClientAuthenticator): """Provides PLAIN SASL authentication for a client.""" def __init__(self,password_manager): """Initialize a `PlainClientAuthenticator` object. :Parameters: - `password_manager`: name of the password manager object providing authentication credentials. :Types: - `password_manager`: `PasswordManager`""" ClientAuthenticator.__init__(self,password_manager) self.username=None self.finished=None self.password=None self.authzid=None self.__logger=logging.getLogger("pyxmpp.sasl.PlainClientAuthenticator") def start(self,username,authzid): """Start the authentication process and return the initial response. :Parameters: - `username`: username (authentication id). - `authzid`: authorization id. :Types: - `username`: `unicode` - `authzid`: `unicode` :return: the initial response or a failure indicator. :returntype: `sasl.Response` or `sasl.Failure`""" self.username=username if authzid: self.authzid=authzid else: self.authzid="" self.finished=0 return self.challenge("") def challenge(self, challenge): """Process the challenge and return the response. :Parameters: - `challenge`: the challenge. :Types: - `challenge`: `str` :return: the response or a failure indicator. :returntype: `sasl.Response` or `sasl.Failure`""" _unused = challenge if self.finished: self.__logger.debug("Already authenticated") return Failure("extra-challenge") self.finished=1 if self.password is None: self.password,pformat=self.password_manager.get_password(self.username) if not self.password or pformat!="plain": self.__logger.debug("Couldn't retrieve plain password") return Failure("password-unavailable") return Response("%s\000%s\000%s" % ( to_utf8(self.authzid), to_utf8(self.username), to_utf8(self.password))) def finish(self,data): """Handle authentication succes information from the server. :Parameters: - `data`: the optional additional data returned with the success. :Types: - `data`: `str` :return: a success indicator. :returntype: `Success`""" _unused = data return Success(self.username,None,self.authzid) class PlainServerAuthenticator(ServerAuthenticator): """Provides PLAIN SASL authentication for a server.""" def __init__(self,password_manager): """Initialize a `PlainServerAuthenticator` object. :Parameters: - `password_manager`: name of the password manager object providing authentication credential verification. :Types: - `password_manager`: `PasswordManager`""" ServerAuthenticator.__init__(self,password_manager) self.__logger=logging.getLogger("pyxmpp.sasl.PlainServerAuthenticator") def start(self,response): """Start the authentication process. :Parameters: - `response`: the initial response from the client. :Types: - `response`: `str` :return: a challenge, a success indicator or a failure indicator. :returntype: `sasl.Challenge`, `sasl.Success` or `sasl.Failure`""" if not response: return Challenge("") return self.response(response) def response(self,response): """Process a client reponse. :Parameters: - `response`: the response from the client. :Types: - `response`: `str` :return: a challenge, a success indicator or a failure indicator. :returntype: `sasl.Challenge`, `sasl.Success` or `sasl.Failure`""" s=response.split("\000") if len(s)!=3: self.__logger.debug("Bad response: %r" % (response,)) return Failure("not-authorized") authzid,username,password=s authzid=from_utf8(authzid) username=from_utf8(username) password=from_utf8(password) if not self.password_manager.check_password(username,password): self.__logger.debug("Bad password. Response was: %r" % (response,)) return Failure("not-authorized") info={"mechanism":"PLAIN","username":username} if self.password_manager.check_authzid(authzid,info): return Success(username,None,authzid) else: self.__logger.debug("Authzid verification failed.") return Failure("invalid-authzid") # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/sasl/gssapi.py0000644000175000017500000000507611560025462017662 0ustar jajcususers00000000000000# # (C) Copyright 2008 Jelmer Vernooij # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """GSSAPI authentication mechanism for PyXMPP SASL implementation. Normative reference: - `RFC 4752 `__ """ __docformat__="restructuredtext en" import base64 import kerberos import logging from pyxmpp.sasl.core import (ClientAuthenticator,Failure,Response,Challenge,Success) class GSSAPIClientAuthenticator(ClientAuthenticator): """Provides client-side GSSAPI SASL (Kerberos 5) authentication.""" def __init__(self,password_manager): ClientAuthenticator.__init__(self, password_manager) self.password_manager = password_manager self.__logger = logging.getLogger("pyxmpp.sasl.gssapi.GSSAPIClientAuthenticator") def start(self, username, authzid): self.username = username self.authzid = authzid rc, self._gss = kerberos.authGSSClientInit(authzid or "%s@%s" % ("xmpp", self.password_manager.get_serv_host())) self.step = 0 return self.challenge("") def challenge(self, challenge): if self.step == 0: rc = kerberos.authGSSClientStep(self._gss, base64.b64encode(challenge)) if rc != kerberos.AUTH_GSS_CONTINUE: self.step = 1 elif self.step == 1: rc = kerberos.authGSSClientUnwrap(self._gss, base64.b64encode(challenge)) response = kerberos.authGSSClientResponse(self._gss) rc = kerberos.authGSSClientWrap(self._gss, response, self.username) response = kerberos.authGSSClientResponse(self._gss) if response is None: return Response("") else: return Response(base64.b64decode(response)) def finish(self, data): self.username = kerberos.authGSSClientUserName(self._gss) self.__logger.debug("Authenticated as %s" % kerberos.authGSSClientUserName(self._gss)) return Success(self.username,None,self.authzid) # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/sasl/__init__.py0000644000175000017500000000624311560025462020130 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """SASL authentication implementaion for PyXMPP. Normative reference: - `RFC 2222 `__ """ __docformat__="restructuredtext en" import random from pyxmpp.sasl.core import Reply,Response,Challenge,Success,Failure,PasswordManager from pyxmpp.sasl.plain import PlainClientAuthenticator,PlainServerAuthenticator from pyxmpp.sasl.digest_md5 import DigestMD5ClientAuthenticator,DigestMD5ServerAuthenticator from pyxmpp.sasl.external import ExternalClientAuthenticator safe_mechanisms_dict={"DIGEST-MD5":(DigestMD5ClientAuthenticator,DigestMD5ServerAuthenticator), "EXTERNAL":(ExternalClientAuthenticator, None)} try: from pyxmpp.sasl.gssapi import GSSAPIClientAuthenticator except ImportError: pass # Kerberos not available else: safe_mechanisms_dict["GSSAPI"] = (GSSAPIClientAuthenticator,None) unsafe_mechanisms_dict={"PLAIN":(PlainClientAuthenticator,PlainServerAuthenticator)} all_mechanisms_dict=safe_mechanisms_dict.copy() all_mechanisms_dict.update(unsafe_mechanisms_dict) safe_mechanisms=safe_mechanisms_dict.keys() unsafe_mechanisms=unsafe_mechanisms_dict.keys() all_mechanisms=safe_mechanisms+unsafe_mechanisms def client_authenticator_factory(mechanism,password_manager): """Create a client authenticator object for given SASL mechanism and password manager. :Parameters: - `mechanism`: name of the SASL mechanism ("PLAIN", "DIGEST-MD5" or "GSSAPI"). - `password_manager`: name of the password manager object providing authentication credentials. :Types: - `mechanism`: `str` - `password_manager`: `PasswordManager` :return: new authenticator. :returntype: `sasl.core.ClientAuthenticator`""" authenticator=all_mechanisms_dict[mechanism][0] return authenticator(password_manager) def server_authenticator_factory(mechanism,password_manager): """Create a server authenticator object for given SASL mechanism and password manager. :Parameters: - `mechanism`: name of the SASL mechanism ("PLAIN", "DIGEST-MD5" or "GSSAPI"). - `password_manager`: name of the password manager object to be used for authentication credentials verification. :Types: - `mechanism`: `str` - `password_manager`: `PasswordManager` :return: new authenticator. :returntype: `sasl.core.ServerAuthenticator`""" authenticator=all_mechanisms_dict[mechanism][1] return authenticator(password_manager) # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/sasl/external.py0000644000175000017500000000446111560025462020213 0ustar jajcususers00000000000000# # (C) Copyright 2009 Michal Witkowski # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """External SASL authentication mechanism for PyXMPP SASL implementation. Normative reference: - `RFC 3920bis `__ - `XEP-0178 __` """ __docformat__="restructuredtext en" import base64 import logging from pyxmpp.sasl.core import (ClientAuthenticator,Failure,Response,Challenge,Success) class ExternalClientAuthenticator(ClientAuthenticator): """Provides client-side External SASL (TLS-Identify) authentication.""" def __init__(self,password_manager): ClientAuthenticator.__init__(self, password_manager) self.password_manager = password_manager self.__logger = logging.getLogger("pyxmpp.sasl.external.ExternalClientAuthenticator") def start(self, username, authzid): self.username = username self.authzid = authzid # TODO: This isn't very XEP-0178'ish. # XEP-0178 says "=" should be sent when only one id-on-xmppAddr is # in the cert, but we don't know that. Still, this conforms to the # standard and works. return Response(self.authzid, encode = True) #return Response("=", encode = False) def finish(self,data): """Handle authentication success information from the server. :Parameters: - `data`: the optional additional data returned with the success. :Types: - `data`: `str` :return: a success indicator. :returntype: `Success`""" _unused = data return Success(self.username,None,self.authzid) # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/sasl/digest_md5.py0000644000175000017500000006250411561464217020425 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """DIGEST-MD5 authentication mechanism for PyXMPP SASL implementation. Normative reference: - `RFC 2831 `__ """ __docformat__="restructuredtext en" from binascii import b2a_hex import re import logging import hashlib from pyxmpp.sasl.core import ClientAuthenticator,ServerAuthenticator from pyxmpp.sasl.core import Failure,Response,Challenge,Success,Failure from pyxmpp.utils import to_utf8,from_utf8 quote_re=re.compile(r"(?[^=]+)\=(?P(\"(([^"\\]+)|(\\\")' r'|(\\\\))+\")|([^",]+))(\s*\,\s*(?P.*))?$') class DigestMD5ClientAuthenticator(ClientAuthenticator): """Provides PLAIN SASL authentication for a client. :Ivariables: - `password`: current authentication password - `pformat`: current authentication password format - `realm`: current authentication realm """ def __init__(self,password_manager): """Initialize a `DigestMD5ClientAuthenticator` object. :Parameters: - `password_manager`: name of the password manager object providing authentication credentials. :Types: - `password_manager`: `PasswordManager`""" ClientAuthenticator.__init__(self,password_manager) self.username=None self.rspauth_checked=None self.response_auth=None self.authzid=None self.pformat=None self.realm=None self.password=None self.nonce_count=None self.__logger=logging.getLogger("pyxmpp.sasl.DigestMD5ClientAuthenticator") def start(self,username,authzid): """Start the authentication process initializing client state. :Parameters: - `username`: username (authentication id). - `authzid`: authorization id. :Types: - `username`: `unicode` - `authzid`: `unicode` :return: the (empty) initial response :returntype: `sasl.Response` or `sasl.Failure`""" self.username=from_utf8(username) if authzid: self.authzid=from_utf8(authzid) else: self.authzid="" self.password=None self.pformat=None self.nonce_count=0 self.response_auth=None self.rspauth_checked=0 self.realm=None return Response() def challenge(self,challenge): """Process a challenge and return the response. :Parameters: - `challenge`: the challenge from server. :Types: - `challenge`: `str` :return: the response or a failure indicator. :returntype: `sasl.Response` or `sasl.Failure`""" if not challenge: self.__logger.debug("Empty challenge") return Failure("bad-challenge") challenge=challenge.split('\x00')[0] # workaround for some buggy implementations if self.response_auth: return self._final_challenge(challenge) realms=[] nonce=None charset="iso-8859-1" while challenge: m=_param_re.match(challenge) if not m: self.__logger.debug("Challenge syntax error: %r" % (challenge,)) return Failure("bad-challenge") challenge=m.group("rest") var=m.group("var") val=m.group("val") self.__logger.debug("%r: %r" % (var,val)) if var=="realm": realms.append(_unquote(val)) elif var=="nonce": if nonce: self.__logger.debug("Duplicate nonce") return Failure("bad-challenge") nonce=_unquote(val) elif var=="qop": qopl=_unquote(val).split(",") if "auth" not in qopl: self.__logger.debug("auth not supported") return Failure("not-implemented") elif var=="charset": if val!="utf-8": self.__logger.debug("charset given and not utf-8") return Failure("bad-challenge") charset="utf-8" elif var=="algorithm": if val!="md5-sess": self.__logger.debug("algorithm given and not md5-sess") return Failure("bad-challenge") if not nonce: self.__logger.debug("nonce not given") return Failure("bad-challenge") self._get_password() return self._make_response(charset,realms,nonce) def _get_password(self): """Retrieve user's password from the password manager. Set `self.password` to the password and `self.pformat` to its format name ('plain' or 'md5:user:realm:pass').""" if self.password is None: self.password,self.pformat=self.password_manager.get_password( self.username,["plain","md5:user:realm:pass"]) if not self.password or self.pformat not in ("plain","md5:user:realm:pass"): self.__logger.debug("Couldn't get plain password. Password: %r Format: %r" % (self.password,self.pformat)) return Failure("password-unavailable") def _make_response(self,charset,realms,nonce): """Make a response for the first challenge from the server. :Parameters: - `charset`: charset name from the challenge. - `realms`: realms list from the challenge. - `nonce`: nonce value from the challenge. :Types: - `charset`: `str` - `realms`: `str` - `nonce`: `str` :return: the response or a failure indicator. :returntype: `sasl.Response` or `sasl.Failure`""" params=[] realm=self._get_realm(realms,charset) if isinstance(realm,Failure): return realm elif realm: realm=_quote(realm) params.append('realm="%s"' % (realm,)) try: username=self.username.encode(charset) except UnicodeError: self.__logger.debug("Couldn't encode username to %r" % (charset,)) return Failure("incompatible-charset") username=_quote(username) params.append('username="%s"' % (username,)) cnonce=self.password_manager.generate_nonce() cnonce=_quote(cnonce) params.append('cnonce="%s"' % (cnonce,)) params.append('nonce="%s"' % (_quote(nonce),)) self.nonce_count+=1 nonce_count="%08x" % (self.nonce_count,) params.append('nc=%s' % (nonce_count,)) params.append('qop=auth') serv_type=self.password_manager.get_serv_type().encode("us-ascii") host=self.password_manager.get_serv_host().encode("idna") serv_name=self.password_manager.get_serv_name().encode("utf-8") if serv_name and serv_name != host: digest_uri="%s/%s/%s" % (serv_type,host,serv_name) else: digest_uri="%s/%s" % (serv_type,host) digest_uri=_quote(digest_uri) params.append('digest-uri="%s"' % (digest_uri,)) if self.authzid: try: authzid=self.authzid.encode(charset) except UnicodeError: self.__logger.debug("Couldn't encode authzid to %r" % (charset,)) return Failure("incompatible-charset") authzid=_quote(authzid) else: authzid="" if self.pformat=="md5:user:realm:pass": urp_hash=self.password else: urp_hash=_make_urp_hash(username,realm,self.password) response=_compute_response(urp_hash,nonce,cnonce,nonce_count, authzid,digest_uri) self.response_auth=_compute_response_auth(urp_hash,nonce,cnonce, nonce_count,authzid,digest_uri) params.append('response=%s' % (response,)) if authzid: params.append('authzid="%s"' % (authzid,)) return Response(",".join(params)) def _get_realm(self,realms,charset): """Choose a realm from the list specified by the server. :Parameters: - `realms`: the realm list. - `charset`: encoding of realms on the list. :Types: - `realms`: `list` of `str` - `charset`: `str` :return: the realm chosen or a failure indicator. :returntype: `str` or `Failure`""" if realms: realms=[unicode(r,charset) for r in realms] realm=self.password_manager.choose_realm(realms) else: realm=self.password_manager.choose_realm([]) if realm: if type(realm) is unicode: try: realm=realm.encode(charset) except UnicodeError: self.__logger.debug("Couldn't encode realm to %r" % (charset,)) return Failure("incompatible-charset") elif charset!="utf-8": try: realm=unicode(realm,"utf-8").encode(charset) except UnicodeError: self.__logger.debug("Couldn't encode realm from utf-8 to %r" % (charset,)) return Failure("incompatible-charset") self.realm=realm return realm def _final_challenge(self,challenge): """Process the second challenge from the server and return the response. :Parameters: - `challenge`: the challenge from server. :Types: - `challenge`: `str` :return: the response or a failure indicator. :returntype: `sasl.Response` or `sasl.Failure`""" if self.rspauth_checked: return Failure("extra-challenge") challenge=challenge.split('\x00')[0] rspauth=None while challenge: m=_param_re.match(challenge) if not m: self.__logger.debug("Challenge syntax error: %r" % (challenge,)) return Failure("bad-challenge") challenge=m.group("rest") var=m.group("var") val=m.group("val") self.__logger.debug("%r: %r" % (var,val)) if var=="rspauth": rspauth=val if not rspauth: self.__logger.debug("Final challenge without rspauth") return Failure("bad-success") if rspauth==self.response_auth: self.rspauth_checked=1 return Response("") else: self.__logger.debug("Wrong rspauth value - peer is cheating?") self.__logger.debug("my rspauth: %r" % (self.response_auth,)) return Failure("bad-success") def finish(self,data): """Process success indicator from the server. Process any addiitional data passed with the success. Fail if the server was not authenticated. :Parameters: - `data`: an optional additional data with success. :Types: - `data`: `str` :return: success or failure indicator. :returntype: `sasl.Success` or `sasl.Failure`""" if not self.response_auth: self.__logger.debug("Got success too early") return Failure("bad-success") if self.rspauth_checked: return Success(self.username,self.realm,self.authzid) else: r = self._final_challenge(data) if isinstance(r, Failure): return r if self.rspauth_checked: return Success(self.username,self.realm,self.authzid) else: self.__logger.debug("Something went wrong when processing additional data with success?") return Failure("bad-success") class DigestMD5ServerAuthenticator(ServerAuthenticator): """Provides DIGEST-MD5 SASL authentication for a server.""" def __init__(self,password_manager): """Initialize a `DigestMD5ServerAuthenticator` object. :Parameters: - `password_manager`: name of the password manager object providing authentication credential verification. :Types: - `password_manager`: `PasswordManager`""" ServerAuthenticator.__init__(self,password_manager) self.nonce=None self.username=None self.realm=None self.authzid=None self.done=None self.last_nonce_count=None self.__logger=logging.getLogger("pyxmpp.sasl.DigestMD5ServerAuthenticator") def start(self,response): """Start the authentication process. :Parameters: - `response`: the initial response from the client (empty for DIGEST-MD5). :Types: - `response`: `str` :return: a challenge, a success indicator or a failure indicator. :returntype: `sasl.Challenge`, `sasl.Success` or `sasl.Failure`""" _unused = response self.last_nonce_count=0 params=[] realms=self.password_manager.get_realms() if realms: self.realm=_quote(realms[0]) for r in realms: r=_quote(r) params.append('realm="%s"' % (r,)) else: self.realm=None nonce=_quote(self.password_manager.generate_nonce()) self.nonce=nonce params.append('nonce="%s"' % (nonce,)) params.append('qop="auth"') params.append('charset=utf-8') params.append('algorithm=md5-sess') self.authzid=None self.done=0 return Challenge(",".join(params)) def response(self,response): """Process a client reponse. :Parameters: - `response`: the response from the client. :Types: - `response`: `str` :return: a challenge, a success indicator or a failure indicator. :returntype: `sasl.Challenge`, `sasl.Success` or `sasl.Failure`""" if self.done: return Success(self.username,self.realm,self.authzid) if not response: return Failure("not-authorized") return self._parse_response(response) def _parse_response(self,response): """Parse a client reponse and pass to further processing. :Parameters: - `response`: the response from the client. :Types: - `response`: `str` :return: a challenge, a success indicator or a failure indicator. :returntype: `sasl.Challenge`, `sasl.Success` or `sasl.Failure`""" response=response.split('\x00')[0] # workaround for some SASL implementations if self.realm: realm=to_utf8(self.realm) realm=_quote(realm) else: realm=None username=None cnonce=None digest_uri=None response_val=None authzid=None nonce_count=None while response: m=_param_re.match(response) if not m: self.__logger.debug("Response syntax error: %r" % (response,)) return Failure("not-authorized") response=m.group("rest") var=m.group("var") val=m.group("val") self.__logger.debug("%r: %r" % (var,val)) if var=="realm": realm=val[1:-1] elif var=="cnonce": if cnonce: self.__logger.debug("Duplicate cnonce") return Failure("not-authorized") cnonce=val[1:-1] elif var=="qop": if val!='auth': self.__logger.debug("qop other then 'auth'") return Failure("not-authorized") elif var=="digest-uri": digest_uri=val[1:-1] elif var=="authzid": authzid=val[1:-1] elif var=="username": username=val[1:-1] elif var=="response": response_val=val elif var=="nc": nonce_count=val self.last_nonce_count+=1 if int(nonce_count)!=self.last_nonce_count: self.__logger.debug("bad nonce: %r != %r" % (nonce_count,self.last_nonce_count)) return Failure("not-authorized") return self._check_params(username,realm,cnonce,digest_uri, response_val,authzid,nonce_count) def _check_params(self,username,realm,cnonce,digest_uri, response_val,authzid,nonce_count): """Check parameters of a client reponse and pass them to further processing. :Parameters: - `username`: user name. - `realm`: realm. - `cnonce`: cnonce value. - `digest_uri`: digest-uri value. - `response_val`: response value computed by the client. - `authzid`: authorization id. - `nonce_count`: nonce count value. :Types: - `username`: `str` - `realm`: `str` - `cnonce`: `str` - `digest_uri`: `str` - `response_val`: `str` - `authzid`: `str` - `nonce_count`: `int` :return: a challenge, a success indicator or a failure indicator. :returntype: `sasl.Challenge`, `sasl.Success` or `sasl.Failure`""" if not cnonce: self.__logger.debug("Required 'cnonce' parameter not given") return Failure("not-authorized") if not response_val: self.__logger.debug("Required 'response' parameter not given") return Failure("not-authorized") if not username: self.__logger.debug("Required 'username' parameter not given") return Failure("not-authorized") if not digest_uri: self.__logger.debug("Required 'digest_uri' parameter not given") return Failure("not-authorized") if not nonce_count: self.__logger.debug("Required 'nc' parameter not given") return Failure("not-authorized") return self._make_final_challenge(username,realm,cnonce,digest_uri, response_val,authzid,nonce_count) def _make_final_challenge(self,username,realm,cnonce,digest_uri, response_val,authzid,nonce_count): """Send the second challenge in reply to the client response. :Parameters: - `username`: user name. - `realm`: realm. - `cnonce`: cnonce value. - `digest_uri`: digest-uri value. - `response_val`: response value computed by the client. - `authzid`: authorization id. - `nonce_count`: nonce count value. :Types: - `username`: `str` - `realm`: `str` - `cnonce`: `str` - `digest_uri`: `str` - `response_val`: `str` - `authzid`: `str` - `nonce_count`: `int` :return: a challenge, a success indicator or a failure indicator. :returntype: `sasl.Challenge`, `sasl.Success` or `sasl.Failure`""" username_uq=from_utf8(username.replace('\\','')) if authzid: authzid_uq=from_utf8(authzid.replace('\\','')) else: authzid_uq=None if realm: realm_uq=from_utf8(realm.replace('\\','')) else: realm_uq=None digest_uri_uq=digest_uri.replace('\\','') self.username=username_uq self.realm=realm_uq password,pformat=self.password_manager.get_password( username_uq,realm_uq,("plain","md5:user:realm:pass")) if pformat=="md5:user:realm:pass": urp_hash=password elif pformat=="plain": urp_hash=_make_urp_hash(username,realm,password) else: self.__logger.debug("Couldn't get password.") return Failure("not-authorized") valid_response=_compute_response(urp_hash,self.nonce,cnonce, nonce_count,authzid,digest_uri) if response_val!=valid_response: self.__logger.debug("Response mismatch: %r != %r" % (response_val,valid_response)) return Failure("not-authorized") s=digest_uri_uq.split("/") if len(s)==3: serv_type,host,serv_name=s elif len(s)==2: serv_type,host=s serv_name=None else: self.__logger.debug("Bad digest_uri: %r" % (digest_uri_uq,)) return Failure("not-authorized") info={} info["mechanism"]="DIGEST-MD5" info["username"]=username_uq info["serv-type"]=serv_type info["host"]=host info["serv-name"]=serv_name if self.password_manager.check_authzid(authzid_uq,info): rspauth=_compute_response_auth(urp_hash,self.nonce, cnonce,nonce_count,authzid,digest_uri) self.authzid=authzid self.done=1 return Challenge("rspauth="+rspauth) else: self.__logger.debug("Authzid check failed") return Failure("invalid_authzid") # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/sasl/core.py0000644000175000017500000003136111560025462017320 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Base classes for PyXMPP SASL implementation. Normative reference: - `RFC 2222 `__ """ __docformat__="restructuredtext en" import random import logging from binascii import b2a_base64 class PasswordManager: """Base class for password managers. Password manager is an object responsible for providing or verification of authentication credentials. All the methods of `PasswordManager` class may be overriden in derived classes for specific authentication and authorization policy.""" def __init__(self): """Initialize a `PasswordManager` object.""" pass def get_password(self,username,realm=None,acceptable_formats=("plain",)): """Get the password for user authentication. [both client or server] By default returns (None, None) providing no password. Should be overriden in derived classes. :Parameters: - `username`: the username for which the password is requested. - `realm`: the authentication realm for which the password is requested. - `acceptable_formats`: a sequence of acceptable formats of the password data. Could be "plain", "md5:user:realm:password" or any other mechanism-specific encoding. This allows non-plain-text storage of passwords. But only "plain" format will work with all password authentication mechanisms. :Types: - `username`: `unicode` - `realm`: `unicode` - `acceptable_formats`: sequence of `str` :return: the password and its encoding (format). :returntype: `unicode`,`str` tuple.""" _unused, _unused, _unused = username, realm, acceptable_formats return None,None def check_password(self,username,password,realm=None): """Check the password validity. [server only] Used by plain-text authentication mechanisms. Retrieve a "plain" password for the `username` and `realm` using `self.get_password` and compare it with the password provided. May be overrided e.g. to check the password against some external authentication mechanism (PAM, LDAP, etc.). :Parameters: - `username`: the username for which the password verification is requested. - `password`: the password to verify. - `realm`: the authentication realm for which the password verification is requested. :Types: - `username`: `unicode` - `password`: `unicode` - `realm`: `unicode` :return: `True` if the password is valid. :returntype: `bool`""" pw,format=self.get_password(username,realm,("plain",)) if pw and format=="plain" and pw==password: return True return False def get_realms(self): """Get available realms list. [server only] :return: a list of realms available for authentication. May be empty -- the client may choose its own realm then or use no realm at all. :returntype: `list` of `unicode`""" return [] def choose_realm(self,realm_list): """Choose an authentication realm from the list provided by the server. [client only] By default return the first realm from the list or `None` if the list is empty. :Parameters: - `realm_list`: the list of realms provided by a server. :Types: - `realm_list`: sequence of `unicode` :return: the realm chosen. :returntype: `unicode`""" if realm_list: return realm_list[0] else: return None def check_authzid(self,authzid,extra_info=None): """Check if the authenticated entity is allowed to use given authorization id. [server only] By default return `True` if the `authzid` is `None` or empty or it is equal to extra_info["username"] (if the latter is present). :Parameters: - `authzid`: an authorization id. - `extra_info`: information about an entity got during the authentication process. This is a mapping with arbitrary, mechanism-dependent items. Common keys are 'username' or 'realm'. :Types: - `authzid`: `unicode` - `extra_info`: mapping :return: `True` if the authenticated entity is authorized to use the provided authorization id. :returntype: `bool`""" if not extra_info: extra_info={} return (not authzid or extra_info.has_key("username") and extra_info["username"]==authzid) def get_serv_type(self): """Return the service type for DIGEST-MD5 'digest-uri' field. Should be overriden in derived classes. :return: the service type ("unknown" by default)""" return "unknown" def get_serv_host(self): """Return the host name for DIGEST-MD5 'digest-uri' field. Should be overriden in derived classes. :return: the host name ("unknown" by default)""" return "unknown" def get_serv_name(self): """Return the service name for DIGEST-MD5 'digest-uri' field. Should be overriden in derived classes. :return: the service name or `None` (which is the default).""" return None def generate_nonce(self): """Generate a random string for digest authentication challenges. The string should be cryptographicaly secure random pattern. :return: the string generated. :returntype: `str`""" # FIXME: use some better RNG (/dev/urandom maybe) r1=str(random.random())[2:] r2=str(random.random())[2:] return r1+r2 class Reply: """Base class for SASL authentication reply objects. :Ivariables: - `data`: optional reply data. - `encode`: whether to base64 encode the data or not :Types: - `data`: `str` - `encode`; `bool`""" def __init__(self,data="", encode=True): """Initialize the `Reply` object. :Parameters: - `data`: optional reply data. :Types: - `data`: `str`""" self.data=data self.encode=encode def base64(self): """Base64-encode the data contained in the reply. :return: base64-encoded data. :returntype: `str`""" if self.data is not None: ret=b2a_base64(self.data) if ret[-1]=='\n': ret=ret[:-1] return ret else: return None class Challenge(Reply): """The challenge SASL message (server's challenge for the client).""" def __init__(self,data): """Initialize the `Challenge` object.""" Reply.__init__(self,data) def __repr__(self): return "" % (self.data,) class Response(Reply): """The response SASL message (clients's reply the the server's challenge).""" def __init__(self,data="", encode=True): """Initialize the `Response` object.""" Reply.__init__(self,data, encode) def __repr__(self): return "" % (self.data,) class Failure(Reply): """The failure SASL message. :Ivariables: - `reason`: the failure reason. :Types: - `reason`: unicode.""" def __init__(self,reason,encode=True): """Initialize the `Failure` object. :Parameters: - `reason`: the failure reason. :Types: - `reason`: unicode.""" Reply.__init__(self,"",encode) self.reason=reason def __repr__(self): return "" % (self.reason,) class Success(Reply): """The success SASL message (sent by the server on authentication success).""" def __init__(self,username,realm=None,authzid=None,data=None): """Initialize the `Success` object. :Parameters: - `username`: authenticated username (authentication id). - `realm`: authentication realm used. - `authzid`: authorization id. - `data`: the success data to be sent to the client. :Types: - `username`: `unicode` - `realm`: `unicode` - `authzid`: `unicode` - `data`: `str` """ Reply.__init__(self,data) self.username=username self.realm=realm self.authzid=authzid def __repr__(self): return "" % (self.authzid,self.data) class ClientAuthenticator: """Base class for client authenticators. A client authenticator class is a client-side implementation of a SASL mechanism. One `ClientAuthenticator` object may be used for one client authentication process.""" def __init__(self,password_manager): """Initialize a `ClientAuthenticator` object. :Parameters: - `password_manager`: a password manager providing authentication credentials. :Types: - `password_manager`: `PasswordManager`""" self.password_manager=password_manager self.__logger=logging.getLogger("pyxmpp.sasl.ClientAuthenticator") def start(self,username,authzid): """Start the authentication process. :Parameters: - `username`: the username (authentication id). - `authzid`: the authorization id requester. :Types: - `username`: `unicode` - `authzid`: `unicode` :return: the initial response to send to the server or a failuer indicator. :returntype: `Response` or `Failure`""" _unused, _unused = username, authzid return Failure("Not implemented") def challenge(self,challenge): """Process the server's challenge. :Parameters: - `challenge`: the challenge. :Types: - `challenge`: `str` :return: the response or a failure indicator. :returntype: `Response` or `Failure`""" _unused = challenge return Failure("Not implemented") def finish(self,data): """Handle authentication succes information from the server. :Parameters: - `data`: the optional additional data returned with the success. :Types: - `data`: `str` :return: success or failure indicator. :returntype: `Success` or `Failure`""" _unused = data return Failure("Not implemented") class ServerAuthenticator: """Base class for server authenticators. A server authenticator class is a server-side implementation of a SASL mechanism. One `ServerAuthenticator` object may be used for one client authentication process.""" def __init__(self,password_manager): """Initialize a `ServerAuthenticator` object. :Parameters: - `password_manager`: a password manager providing authentication credential verfication. :Types: - `password_manager`: `PasswordManager`""" self.password_manager=password_manager self.__logger=logging.getLogger("pyxmpp.sasl.ServerAuthenticator") def start(self,initial_response): """Start the authentication process. :Parameters: - `initial_response`: the initial response send by the client with the authentication request. :Types: - `initial_response`: `str` :return: a challenge, a success or a failure indicator. :returntype: `Challenge` or `Failure` or `Success`""" _unused = initial_response return Failure("not-authorized") def response(self,response): """Process a response from a client. :Parameters: - `response`: the response from the client to our challenge. :Types: - `response`: `str` :return: a challenge, a success or a failure indicator. :returntype: `Challenge` or `Success` or `Failure`""" _unused = response return Failure("not-authorized") # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/utils.py0000644000175000017500000000535511560025462016572 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Utility functions for the pyxmpp package.""" __docformat__="restructuredtext en" import sys if sys.hexversion<0x02030000: raise ImportError,"Python 2.3 or newer is required" import time import datetime def to_utf8(s): """ Convevert `s` to UTF-8 if it is Unicode, leave unchanged if it is string or None and convert to string overwise """ if s is None: return None elif type(s) is unicode: return s.encode("utf-8") elif type(s) is str: return s else: return unicode(s).encode("utf-8") def from_utf8(s): """ Convert `s` to Unicode or leave unchanged if it is None. Regular strings are assumed to be UTF-8 encoded """ if s is None: return None elif type(s) is unicode: return s elif type(s) is str: return unicode(s,"utf-8") else: return unicode(s) minute=datetime.timedelta(minutes=1) nulldelta=datetime.timedelta() def datetime_utc_to_local(utc): """ An ugly hack to convert naive `datetime.datetime` object containing UTC time to a naive `datetime.datetime` object with local time. It seems standard Python 2.3 library doesn't provide any better way to do that. """ ts=time.time() cur=datetime.datetime.fromtimestamp(ts) cur_utc=datetime.datetime.utcfromtimestamp(ts) offset=cur-cur_utc t=utc d=datetime.timedelta(hours=2) while d>minute: local=t+offset tm=local.timetuple() tm=tm[0:8]+(0,) ts=time.mktime(tm) u=datetime.datetime.utcfromtimestamp(ts) diff=u-utc if diff-minute: break if diff>nulldelta: offset-=d else: offset+=d d/=2 return local def datetime_local_to_utc(local): """ Simple function to convert naive `datetime.datetime` object containing local time to a naive `datetime.datetime` object with UTC time. """ ts=time.mktime(local.timetuple()) return datetime.datetime.utcfromtimestamp(ts) # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/iq.py0000644000175000017500000001324711560025462016042 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Iq XMPP stanza handling Normative reference: - `RFC 3920 `__ """ __docformat__="restructuredtext en" import libxml2 from pyxmpp.xmlextra import get_node_ns_uri from pyxmpp.stanza import Stanza, gen_id class Iq(Stanza): """Wraper object for stanzas.""" stanza_type="iq" def __init__(self, xmlnode = None, from_jid = None, to_jid = None, stanza_type = None, stanza_id = None, error = None, error_cond=None, stream = None): """Initialize an `Iq` object. :Parameters: - `xmlnode`: XML node to_jid be wrapped into the `Iq` object or other Iq object to be copied. If not given then new presence stanza is created using following parameters. - `from_jid`: sender JID. - `to_jid`: recipient JID. - `stanza_type`: staza type: one of: "get", "set", "result" or "error". - `stanza_id`: stanza id -- value of stanza's "id" attribute. If not given, then unique for the session value is generated. - `error_cond`: error condition name. Ignored if `stanza_type` is not "error". :Types: - `xmlnode`: `unicode` or `libxml2.xmlNode` or `Iq` - `from_jid`: `JID` - `to_jid`: `JID` - `stanza_type`: `unicode` - `stanza_id`: `unicode` - `error_cond`: `unicode`""" self.xmlnode=None if isinstance(xmlnode,Iq): pass elif isinstance(xmlnode,Stanza): raise TypeError,"Couldn't make Iq from other Stanza" elif isinstance(xmlnode,libxml2.xmlNode): pass elif xmlnode is not None: raise TypeError,"Couldn't make Iq from %r" % (type(xmlnode),) elif not stanza_type: raise ValueError, "type is required for Iq" else: if not stanza_id and stanza_type in ("get", "set"): stanza_id=gen_id() if not xmlnode and stanza_type not in ("get","set","result","error"): raise ValueError, "Invalid Iq type: %r" % (stanza_type,) if xmlnode is None: xmlnode="iq" Stanza.__init__(self, xmlnode, from_jid = from_jid, to_jid = to_jid, stanza_type = stanza_type, stanza_id = stanza_id, error = error, error_cond = error_cond, stream = stream) def copy(self): """Create a deep copy of the iq stanza. :returntype: `Iq`""" return Iq(self) def make_error_response(self,cond): """Create error response for the a "get" or "set" iq stanza. :Parameters: - `cond`: error condition name, as defined in XMPP specification. :return: new `Iq` object with the same "id" as self, "from" and "to" attributes swapped, type="error" and containing element plus payload of `self`. :returntype: `Iq`""" if self.get_type() in ("result", "error"): raise ValueError, "Errors may not be generated for 'result' and 'error' iq" iq=Iq(stanza_type="error",from_jid=self.get_to(),to_jid=self.get_from(), stanza_id=self.get_id(),error_cond=cond) n=self.get_query() if n: n=n.copyNode(1) iq.xmlnode.children.addPrevSibling(n) return iq def make_result_response(self): """Create result response for the a "get" or "set" iq stanza. :return: new `Iq` object with the same "id" as self, "from" and "to" attributes replaced and type="result". :returntype: `Iq`""" if self.get_type() not in ("set","get"): raise ValueError, "Results may only be generated for 'set' or 'get' iq" iq=Iq(stanza_type="result", from_jid=self.get_to(), to_jid=self.get_from(), stanza_id=self.get_id()) return iq def new_query(self,ns_uri,name="query"): """Create new payload element for the stanza. :Parameters: - `ns_uri`: namespace URI of the element. - `name`: element name. :Types: - `ns_uri`: `str` - `name`: `unicode` :return: the new payload node. :returntype: `libxml2.xmlNode`""" return self.set_new_content(ns_uri,name) def get_query(self): """Get the payload element of the stanza. :return: the payload element or None if there is no payload. :returntype: `libxml2.xmlNode`""" c = self.xmlnode.children while c: try: if c.ns(): return c except libxml2.treeError: pass c = c.next return None def get_query_ns(self): """Get a namespace of the stanza payload. :return: XML namespace URI of the payload or None if there is no payload. :returntype: `str`""" q=self.get_query() if q: return get_node_ns_uri(q) else: return None # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/expdict.py0000644000175000017500000001116711560025462017070 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Dictionary with item expiration.""" __docformat__="restructuredtext en" import time import threading __all__ = ['ExpiringDictionary'] sentinel = object() class ExpiringDictionary(dict): """An extension to standard Python dictionary objects which implements item expiration. Each item in ExpiringDictionary has its expiration time assigned, after which the item is removed from the mapping. :Ivariables: - `_timeouts`: a dictionary with timeout values and timeout callback for stored objects. - `_default_timeout`: the default timeout value (in seconds from now). - `_lock`: access synchronization lock. :Types: - `_timeouts`: `dict` - `_default_timeout`: `int` - `_lock`: `threading.RLock`""" __slots__=['_timeouts','_default_timeout','_lock'] def __init__(self,default_timeout=300): """Initialize an `ExpiringDictionary` object. :Parameters: - `default_timeout`: default timeout value for stored objects. :Types: - `default_timeout`: `int`""" dict.__init__(self) self._timeouts={} self._default_timeout=default_timeout self._lock=threading.RLock() def __delitem__(self,key): self._lock.acquire() try: del self._timeouts[key] return dict.__delitem__(self,key) finally: self._lock.release() def __getitem__(self,key): self._lock.acquire() try: self._expire_item(key) return dict.__getitem__(self,key) finally: self._lock.release() def pop(self,key,default=sentinel): self._lock.acquire() try: self._expire_item(key) del self._timeouts[key] if default is not sentinel: return dict.pop(self,key,default) else: return dict.pop(self,key) finally: self._lock.release() def __setitem__(self,key,value): return self.set_item(key,value) def set_item(self,key,value,timeout=None,timeout_callback=None): """Set item of the dictionary. :Parameters: - `key`: the key. - `value`: the object to store. - `timeout`: timeout value for the object (in seconds from now). - `timeout_callback`: function to be called when the item expires. The callback should accept none, one (the key) or two (the key and the value) arguments. :Types: - `key`: any hashable value - `value`: any python object - `timeout`: `int` - `timeout_callback`: callable""" self._lock.acquire() try: if not timeout: timeout=self._default_timeout self._timeouts[key]=(time.time()+timeout,timeout_callback) return dict.__setitem__(self,key,value) finally: self._lock.release() def expire(self): """Do the expiration of dictionary items. Remove items that expired by now from the dictionary.""" self._lock.acquire() try: for k in self._timeouts.keys(): self._expire_item(k) finally: self._lock.release() def _expire_item(self,key): """Do the expiration of a dictionary item. Remove the item if it has expired by now. :Parameters: - `key`: key to the object. :Types: - `key`: any hashable value""" (timeout,callback)=self._timeouts[key] if timeout<=time.time(): item = dict.pop(self, key) del self._timeouts[key] if callback: try: callback(key,item) except TypeError: try: callback(key) except TypeError: callback() # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/stanzaprocessor.py0000644000175000017500000004601111560025462020664 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Handling of XMPP stanzas. Normative reference: - `RFC 3920 `__ """ __docformat__="restructuredtext en" import libxml2 import logging import threading from pyxmpp.expdict import ExpiringDictionary from pyxmpp.exceptions import ProtocolError, BadRequestProtocolError, FeatureNotImplementedProtocolError from pyxmpp.stanza import Stanza class StanzaProcessor: """Universal stanza handler/router class. Provides facilities to set up custom handlers for various types of stanzas. :Ivariables: - `lock`: lock object used to synchronize access to the `StanzaProcessor` object. - `me`: local JID. - `peer`: remote stream endpoint JID. - `process_all_stanzas`: when `True` then all stanzas received are considered local. - `initiator`: `True` if local stream endpoint is the initiating entity. """ def __init__(self): """Initialize a `StanzaProcessor` object.""" self.me=None self.peer=None self.initiator=None self.peer_authenticated=False self.process_all_stanzas=True self._iq_response_handlers=ExpiringDictionary() self._iq_get_handlers={} self._iq_set_handlers={} self._message_handlers=[] self._presence_handlers=[] self.__logger=logging.getLogger("pyxmpp.Stream") self.lock=threading.RLock() def process_response(self, response): """Examines out the response returned by a stanza handler and sends all stanzas provided. :Returns: - `True`: if `response` is `Stanza`, iterable or `True` (meaning the stanza was processed). - `False`: when `response` is `False` or `None` :returntype: `bool` """ if response is None or response is False: return False if isinstance(response, Stanza): self.send(response) return True try: response = iter(response) except TypeError: return bool(response) for stanza in response: if isinstance(stanza, Stanza): self.send(stanza) return True def process_iq(self, stanza): """Process IQ stanza received. :Parameters: - `stanza`: the stanza received If a matching handler is available pass the stanza to it. Otherwise ignore it if it is "error" or "result" stanza or return "feature-not-implemented" error.""" sid=stanza.get_id() fr=stanza.get_from() typ=stanza.get_type() if typ in ("result","error"): if fr: ufr=fr.as_unicode() else: ufr=None res_handler = err_handler = None try: res_handler, err_handler = self._iq_response_handlers.pop((sid,ufr)) except KeyError: if ( (fr==self.peer or fr==self.me or fr==self.me.bare()) ): try: res_handler, err_handler = self._iq_response_handlers.pop((sid,None)) except KeyError: pass if None is res_handler is err_handler: return False if typ=="result": response = res_handler(stanza) else: response = err_handler(stanza) self.process_response(response) return True q=stanza.get_query() if not q: raise BadRequestProtocolError, "Stanza with no child element" el=q.name ns=q.ns().getContent() if typ=="get": if self._iq_get_handlers.has_key((el,ns)): response = self._iq_get_handlers[(el,ns)](stanza) self.process_response(response) return True else: raise FeatureNotImplementedProtocolError, "Not implemented" elif typ=="set": if self._iq_set_handlers.has_key((el,ns)): response = self._iq_set_handlers[(el,ns)](stanza) self.process_response(response) return True else: raise FeatureNotImplementedProtocolError, "Not implemented" else: raise BadRequestProtocolError, "Unknown IQ stanza type" def __try_handlers(self,handler_list,typ,stanza): """ Search the handler list for handlers matching given stanza type and payload namespace. Run the handlers found ordering them by priority until the first one which returns `True`. :Parameters: - `handler_list`: list of available handlers - `typ`: stanza type (value of its "type" attribute) - `stanza`: the stanza to handle :return: result of the last handler or `False` if no handler was found.""" namespaces=[] if stanza.xmlnode.children: c=stanza.xmlnode.children while c: try: ns=c.ns() except libxml2.treeError: ns=None if ns is None: c=c.next continue ns_uri=ns.getContent() if ns_uri not in namespaces: namespaces.append(ns_uri) c=c.next for handler_entry in handler_list: t=handler_entry[1] ns=handler_entry[2] handler=handler_entry[3] if t!=typ: continue if ns is not None and ns not in namespaces: continue response = handler(stanza) if self.process_response(response): return True return False def process_message(self,stanza): """Process message stanza. Pass it to a handler of the stanza's type and payload namespace. If no handler for the actual stanza type succeeds then hadlers for type "normal" are used. :Parameters: - `stanza`: message stanza to be handled """ if not self.initiator and not self.peer_authenticated: self.__logger.debug("Ignoring message - peer not authenticated yet") return True typ=stanza.get_type() if self.__try_handlers(self._message_handlers,typ,stanza): return True if typ!="error": return self.__try_handlers(self._message_handlers,"normal",stanza) return False def process_presence(self,stanza): """Process presence stanza. Pass it to a handler of the stanza's type and payload namespace. :Parameters: - `stanza`: presence stanza to be handled """ if not self.initiator and not self.peer_authenticated: self.__logger.debug("Ignoring presence - peer not authenticated yet") return True typ=stanza.get_type() if not typ: typ="available" return self.__try_handlers(self._presence_handlers,typ,stanza) def route_stanza(self,stanza): """Process stanza not addressed to us. Return "recipient-unavailable" return if it is not "error" nor "result" stanza. This method should be overriden in derived classes if they are supposed to handle stanzas not addressed directly to local stream endpoint. :Parameters: - `stanza`: presence stanza to be processed """ if stanza.get_type() not in ("error","result"): r = stanza.make_error_response("recipient-unavailable") self.send(r) return True def process_stanza(self,stanza): """Process stanza received from the stream. First "fix" the stanza with `self.fix_in_stanza()`, then pass it to `self.route_stanza()` if it is not directed to `self.me` and `self.process_all_stanzas` is not True. Otherwise stanza is passwd to `self.process_iq()`, `self.process_message()` or `self.process_presence()` appropriately. :Parameters: - `stanza`: the stanza received. :returns: `True` when stanza was handled """ self.fix_in_stanza(stanza) to=stanza.get_to() if not self.process_all_stanzas and to and to!=self.me and to.bare()!=self.me.bare(): return self.route_stanza(stanza) try: if stanza.stanza_type=="iq": if self.process_iq(stanza): return True elif stanza.stanza_type=="message": if self.process_message(stanza): return True elif stanza.stanza_type=="presence": if self.process_presence(stanza): return True except ProtocolError, e: typ = stanza.get_type() if typ != 'error' and (typ != 'result' or stanza.stanza_type != 'iq'): r = stanza.make_error_response(e.xmpp_name) self.send(r) e.log_reported() else: e.log_ignored() self.__logger.debug("Unhandled %r stanza: %r" % (stanza.stanza_type,stanza.serialize())) return False def check_to(self,to): """Check "to" attribute of received stream header. :return: `to` if it is equal to `self.me`, None otherwise. Should be overriden in derived classes which require other logic for handling that attribute.""" if to!=self.me: return None return to def set_response_handlers(self,iq,res_handler,err_handler,timeout_handler=None,timeout=300): """Set response handler for an IQ "get" or "set" stanza. This should be called before the stanza is sent. :Parameters: - `iq`: an IQ stanza - `res_handler`: result handler for the stanza. Will be called when matching is received. Its only argument will be the stanza received. The handler may return a stanza or list of stanzas which should be sent in response. - `err_handler`: error handler for the stanza. Will be called when matching is received. Its only argument will be the stanza received. The handler may return a stanza or list of stanzas which should be sent in response but this feature should rather not be used (it is better not to respond to 'error' stanzas). - `timeout_handler`: timeout handler for the stanza. Will be called when no matching or is received in next `timeout` seconds. The handler should accept two arguments and ignore them. - `timeout`: timeout value for the stanza. After that time if no matching nor stanza is received, then timeout_handler (if given) will be called. """ self.lock.acquire() try: self._set_response_handlers(iq,res_handler,err_handler,timeout_handler,timeout) finally: self.lock.release() def _set_response_handlers(self,iq,res_handler,err_handler,timeout_handler=None,timeout=300): """Same as `Stream.set_response_handlers` but assume `self.lock` is acquired.""" self.fix_out_stanza(iq) to=iq.get_to() if to: to=to.as_unicode() if timeout_handler: self._iq_response_handlers.set_item((iq.get_id(),to), (res_handler,err_handler), timeout,timeout_handler) else: self._iq_response_handlers.set_item((iq.get_id(),to), (res_handler,err_handler),timeout) def set_iq_get_handler(self,element,namespace,handler): """Set handler. :Parameters: - `element`: payload element name - `namespace`: payload element namespace URI - `handler`: function to be called when a stanza with defined element is received. Its only argument will be the stanza received. The handler may return a stanza or list of stanzas which should be sent in response. Only one handler may be defined per one namespaced element. If a handler for the element was already set it will be lost after calling this method. """ self.lock.acquire() try: self._iq_get_handlers[(element,namespace)]=handler finally: self.lock.release() def unset_iq_get_handler(self,element,namespace): """Remove handler. :Parameters: - `element`: payload element name - `namespace`: payload element namespace URI """ self.lock.acquire() try: if self._iq_get_handlers.has_key((element,namespace)): del self._iq_get_handlers[(element,namespace)] finally: self.lock.release() def set_iq_set_handler(self,element,namespace,handler): """Set handler. :Parameters: - `element`: payload element name - `namespace`: payload element namespace URI - `handler`: function to be called when a stanza with defined element is received. Its only argument will be the stanza received. The handler may return a stanza or list of stanzas which should be sent in response. Only one handler may be defined per one namespaced element. If a handler for the element was already set it will be lost after calling this method.""" self.lock.acquire() try: self._iq_set_handlers[(element,namespace)]=handler finally: self.lock.release() def unset_iq_set_handler(self,element,namespace): """Remove handler. :Parameters: - `element`: payload element name. - `namespace`: payload element namespace URI.""" self.lock.acquire() try: if self._iq_set_handlers.has_key((element,namespace)): del self._iq_set_handlers[(element,namespace)] finally: self.lock.release() def __add_handler(self,handler_list,typ,namespace,priority,handler): """Add a handler function to a prioritized handler list. :Parameters: - `handler_list`: a handler list. - `typ`: stanza type. - `namespace`: stanza payload namespace. - `priority`: handler priority. Must be >=0 and <=100. Handlers with lower priority list will be tried first.""" if priority<0 or priority>100: raise ValueError,"Bad handler priority (must be in 0:100)" handler_list.append((priority,typ,namespace,handler)) handler_list.sort() def set_message_handler(self, typ, handler, namespace=None, priority=100): """Set a handler for stanzas. :Parameters: - `typ`: message type. `None` will be treated the same as "normal", and will be the default for unknown types (those that have no handler associated). - `namespace`: payload namespace. If `None` that message with any payload (or even with no payload) will match. - `priority`: priority value for the handler. Handlers with lower priority value are tried first. - `handler`: function to be called when a message stanza with defined type and payload namespace is received. Its only argument will be the stanza received. The handler may return a stanza or list of stanzas which should be sent in response. Multiple handlers with the same type/namespace/priority may be set. Order of calling handlers with the same priority is not defined. Handlers will be called in priority order until one of them returns True or any stanza(s) to send (even empty list will do). """ self.lock.acquire() try: if not typ: typ = "normal" self.__add_handler(self._message_handlers,typ,namespace,priority,handler) finally: self.lock.release() def set_presence_handler(self,typ,handler,namespace=None,priority=100): """Set a handler for stanzas. :Parameters: - `typ`: presence type. "available" will be treated the same as `None`. - `namespace`: payload namespace. If `None` that presence with any payload (or even with no payload) will match. - `priority`: priority value for the handler. Handlers with lower priority value are tried first. - `handler`: function to be called when a presence stanza with defined type and payload namespace is received. Its only argument will be the stanza received. The handler may return a stanza or list of stanzas which should be sent in response. Multiple handlers with the same type/namespace/priority may be set. Order of calling handlers with the same priority is not defined. Handlers will be called in priority order until one of them returns True or any stanza(s) to send (even empty list will do). """ self.lock.acquire() try: if not typ: typ="available" self.__add_handler(self._presence_handlers,typ,namespace,priority,handler) finally: self.lock.release() def fix_in_stanza(self,stanza): """Modify incoming stanza before processing it. This implementation does nothig. It should be overriden in derived classes if needed.""" pass def fix_out_stanza(self,stanza): """Modify outgoing stanza before sending into the stream. This implementation does nothig. It should be overriden in derived classes if needed.""" pass def send(self,stanza): """Send a stanza somwhere. This one does nothing. Should be overriden in derived classes. :Parameters: - `stanza`: the stanza to send. :Types: - `stanza`: `pyxmpp.stanza.Stanza`""" raise NotImplementedError,"This method must be overriden in derived classes.""" # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/objects.py0000644000175000017500000001401611560025462017055 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # pylint: disable-msg=W0232, E0201 """General base classes for PyXMPP objects.""" __docformat__="restructuredtext en" import libxml2 from pyxmpp.xmlextra import common_doc class StanzaPayloadObject(object): """Base class for objects that may be used as XMPP stanza payload and don't keep internal XML representation, only parsed values. Provides `as_xml` method. Derived classes must override `xml_element_name` and `xml_element_namespace` class attributes and the `complete_xml_element` method. Please note that not all classes derived from `StanzaPayloadObject` should be used directly as stanza payload. Some of them are parts of higher level objects. :Cvariables: - `xml_element_name`: name for the XML element provided by the class. - `xml_element_namespace`: namespace URI for the XML element provided by the class. :Types: - `xml_element_name`: `unicode` - `xml_element_namespace`: `unicode` """ xml_element_name = None xml_element_namespace = None def as_xml(self, parent = None, doc = None): """Get the XML representation of `self`. New document will be created if no `parent` and no `doc` is given. :Parameters: - `parent`: the parent for the XML element. - `doc`: the document where the element should be created. If not given and `parent` is provided then autodetection is attempted. If that fails, then `common_doc` is used. :Types: - `parent`: `libxml2.xmlNode` - `doc`: `libxml2.xmlDoc` :return: the new XML element or document created. :returntype: `libxml2.xmlNode` or `libxml2.xmlDoc`""" if parent: if not doc: n = parent while n: if n.type == "xml_document": doc = n break n = n.parent if not doc: doc = common_doc try: ns = parent.searchNsByHref(doc, self.xml_element_namespace) except libxml2.treeError: ns = None xmlnode = parent.newChild(ns,self.xml_element_name,None) if not ns: ns = xmlnode.newNs(self.xml_element_namespace,None) xmlnode.setNs(ns) doc1 = doc else: if doc: doc1 = doc else: doc1 = libxml2.newDoc("1.0") xmlnode = doc1.newChild(None,self.xml_element_name, None) ns = xmlnode.newNs(self.xml_element_namespace, None) xmlnode.setNs(ns) self.complete_xml_element(xmlnode, doc1) if doc or parent: return xmlnode doc1.setRootElement(xmlnode) return doc1 def complete_xml_element(self, xmlnode, doc): """Complete the XML node with `self` content. Should be overriden in classes derived from `StanzaPayloadObject`. :Parameters: - `xmlnode`: XML node with the element being built. It has already right name and namespace, but no attributes or content. - `doc`: document to which the element belongs. :Types: - `xmlnode`: `libxml2.xmlNode` - `doc`: `libxml2.xmlDoc`""" pass class StanzaPayloadWrapperObject(object): """Base class for objects that may be used as XMPP stanza payload and maintain an internal XML representation of self. Provides `as_xml` method. Objects of derived classes must have the `xmlnode` attribute. Please note that not all classes derived from `StanzaPayloadWrapperObject` should be used directly as stanza payload. Some of them are parts of higher level objects. :Ivariables: - `xmlnode`: XML node of the object. :Types: - `xmlnode`: `libxml2.xmlNode` """ def as_xml(self, parent = None, doc = None): """Get the XML representation of `self`. New document will be created if no `parent` and no `doc` is given. :Parameters: - `parent`: the parent for the XML element. - `doc`: the document where the element should be created. If not given and `parent` is provided then autodetection is attempted. If that fails, then `common_doc` is used. :Types: - `parent`: `libxml2.xmlNode` - `doc`: `libxml2.xmlDoc` :return: the new XML element (copy of `self.xmlnode`) or document created (containg the copy as the root element). :returntype: `libxml2.xmlNode` or `libxml2.xmlDoc`""" if parent: if not doc: n = parent while n: if n.type == "xml_document": doc = n break n = n.parent if not doc: doc = common_doc copy=self.xmlnode.docCopyNode(doc,True) parent.addChild(copy) return copy else: if not doc: doc1=libxml2.newDoc("1.0") else: doc1=doc xmlnode=doc1.addChild(self.xmlnode.docCopyNode(doc,True)) doc1.setRootElement(xmlnode) if doc: return xmlnode return doc1 # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/stanza.py0000644000175000017500000003114711560025462016730 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """General XMPP Stanza handling. Normative reference: - `RFC 3920 `__ """ __docformat__="restructuredtext en" import libxml2 import random from pyxmpp import xmlextra from pyxmpp.utils import from_utf8,to_utf8 from pyxmpp.jid import JID from pyxmpp.xmlextra import common_doc, common_ns, COMMON_NS from pyxmpp.exceptions import ProtocolError, JIDMalformedProtocolError random.seed() last_id=random.randrange(1000000) def gen_id(): """Generate stanza id unique for the session. :return: the new id.""" global last_id last_id+=1 return str(last_id) class Stanza: """Base class for all XMPP stanzas. :Ivariables: - `xmlnode`: stanza XML node. - `_error`: `pyxmpp.error.StanzaErrorNode` describing the error associated with the stanza of type "error". - `stream`: stream on which the stanza was received or `None`. May be used to send replies or get some session-related parameters. :Types: - `xmlnode`: `libxml2.xmlNode` - `_error`: `pyxmpp.error.StanzaErrorNode`""" stanza_type="Unknown" def __init__(self, name_or_xmlnode, from_jid=None, to_jid=None, stanza_type=None, stanza_id=None, error=None, error_cond=None, stream = None): """Initialize a Stanza object. :Parameters: - `name_or_xmlnode`: XML node to be wrapped into the Stanza object or other Presence object to be copied. If not given then new presence stanza is created using following parameters. - `from_jid`: sender JID. - `to_jid`: recipient JID. - `stanza_type`: staza type: one of: "get", "set", "result" or "error". - `stanza_id`: stanza id -- value of stanza's "id" attribute. If not given, then unique for the session value is generated. - `error`: error object. Ignored if `stanza_type` is not "error". - `error_cond`: error condition name. Ignored if `stanza_type` is not "error" or `error` is not None. :Types: - `name_or_xmlnode`: `unicode` or `libxml2.xmlNode` or `Stanza` - `from_jid`: `JID` - `to_jid`: `JID` - `stanza_type`: `unicode` - `stanza_id`: `unicode` - `error`: `pyxmpp.error.StanzaErrorNode` - `error_cond`: `unicode`""" self._error=None self.xmlnode=None if isinstance(name_or_xmlnode,Stanza): self.xmlnode=name_or_xmlnode.xmlnode.docCopyNode(common_doc, True) common_doc.addChild(self.xmlnode) self.xmlnode.reconciliateNs(common_doc) elif isinstance(name_or_xmlnode,libxml2.xmlNode): self.xmlnode=name_or_xmlnode.docCopyNode(common_doc,1) common_doc.addChild(self.xmlnode) try: ns = self.xmlnode.ns() except libxml2.treeError: ns = None if not ns or not ns.name: xmlextra.replace_ns(self.xmlnode, ns, common_ns) self.xmlnode.reconciliateNs(common_doc) else: self.xmlnode=common_doc.newChild(common_ns,name_or_xmlnode,None) if from_jid is not None: if not isinstance(from_jid,JID): from_jid=JID(from_jid) self.xmlnode.setProp("from",from_jid.as_utf8()) if to_jid is not None: if not isinstance(to_jid,JID): to_jid=JID(to_jid) self.xmlnode.setProp("to",to_jid.as_utf8()) if stanza_type: self.xmlnode.setProp("type",stanza_type) if stanza_id: self.xmlnode.setProp("id",stanza_id) if self.get_type()=="error": from pyxmpp.error import StanzaErrorNode if error: self._error=StanzaErrorNode(error,parent=self.xmlnode,copy=1) elif error_cond: self._error=StanzaErrorNode(error_cond,parent=self.xmlnode) self.stream = stream def __del__(self): if self.xmlnode: self.free() def free(self): """Free the node associated with this `Stanza` object.""" if self._error: self._error.free_borrowed() self.xmlnode.unlinkNode() self.xmlnode.freeNode() self.xmlnode=None def copy(self): """Create a deep copy of the stanza. :returntype: `Stanza`""" return Stanza(self) def serialize(self): """Serialize the stanza into an UTF-8 encoded XML string. :return: serialized stanza. :returntype: `str`""" return self.xmlnode.serialize(encoding="utf-8") def get_node(self): """Return the XML node wrapped into `self`. :returntype: `libxml2.xmlNode`""" return self.xmlnode def get_from(self): """Get "from" attribute of the stanza. :return: value of the "from" attribute (sender JID) or None. :returntype: `JID`""" if self.xmlnode.hasProp("from"): try: return JID(from_utf8(self.xmlnode.prop("from"))) except JIDError: raise JIDMalformedProtocolError, "Bad JID in the 'from' attribute" else: return None get_from_jid=get_from def get_to(self): """Get "to" attribute of the stanza. :return: value of the "to" attribute (recipient JID) or None. :returntype: `JID`""" if self.xmlnode.hasProp("to"): try: return JID(from_utf8(self.xmlnode.prop("to"))) except JIDError: raise JIDMalformedProtocolError, "Bad JID in the 'to' attribute" else: return None get_to_jid=get_to def get_type(self): """Get "type" attribute of the stanza. :return: value of the "type" attribute (stanza type) or None. :returntype: `unicode`""" if self.xmlnode.hasProp("type"): return from_utf8(self.xmlnode.prop("type")) else: return None get_stanza_type=get_type def get_id(self): """Get "id" attribute of the stanza. :return: value of the "id" attribute (stanza identifier) or None. :returntype: `unicode`""" if self.xmlnode.hasProp("id"): return from_utf8(self.xmlnode.prop("id")) else: return None get_stanza_id=get_id def get_error(self): """Get stanza error information. :return: object describing the error. :returntype: `pyxmpp.error.StanzaErrorNode`""" if self._error: return self._error n=self.xpath_eval(u"ns:error") if not n: raise ProtocolError, (None, "This stanza contains no error: %r" % (self.serialize(),)) from pyxmpp.error import StanzaErrorNode self._error=StanzaErrorNode(n[0],copy=0) return self._error def set_from(self,from_jid): """Set "from" attribute of the stanza. :Parameters: - `from_jid`: new value of the "from" attribute (sender JID). :Types: - `from_jid`: `JID`""" if from_jid: return self.xmlnode.setProp("from", JID(from_jid).as_utf8()) else: return self.xmlnode.unsetProp("from") def set_to(self,to_jid): """Set "to" attribute of the stanza. :Parameters: - `to_jid`: new value of the "to" attribute (recipient JID). :Types: - `to_jid`: `JID`""" if to_jid: return self.xmlnode.setProp("to", JID(to_jid).as_utf8()) else: return self.xmlnode.unsetProp("to") def set_type(self,stanza_type): """Set "type" attribute of the stanza. :Parameters: - `stanza_type`: new value of the "type" attribute (stanza type). :Types: - `stanza_type`: `unicode`""" if stanza_type: return self.xmlnode.setProp("type",to_utf8(stanza_type)) else: return self.xmlnode.unsetProp("type") def set_id(self,stanza_id): """Set "id" attribute of the stanza. :Parameters: - `stanza_id`: new value of the "id" attribute (stanza identifier). :Types: - `stanza_id`: `unicode`""" if stanza_id: return self.xmlnode.setProp("id",to_utf8(stanza_id)) else: return self.xmlnode.unsetProp("id") def set_content(self,content): """Set stanza content to an XML node. :Parameters: - `content`: XML node to be included in the stanza. :Types: - `content`: `libxml2.xmlNode` or unicode, or UTF-8 `str` """ while self.xmlnode.children: self.xmlnode.children.unlinkNode() if hasattr(content,"as_xml"): content.as_xml(parent=self.xmlnode,doc=common_doc) elif isinstance(content,libxml2.xmlNode): self.xmlnode.addChild(content.docCopyNode(common_doc,1)) elif isinstance(content,unicode): self.xmlnode.setContent(to_utf8(content)) else: self.xmlnode.setContent(content) def add_content(self,content): """Add an XML node to the stanza's payload. :Parameters: - `content`: XML node to be added to the payload. :Types: - `content`: `libxml2.xmlNode`, UTF-8 `str` or unicode, or an object with "as_xml()" method. """ if hasattr(content, "as_xml"): content.as_xml(parent = self.xmlnode, doc = common_doc) elif isinstance(content,libxml2.xmlNode): self.xmlnode.addChild(content.docCopyNode(common_doc,1)) elif isinstance(content,unicode): self.xmlnode.addContent(to_utf8(content)) else: self.xmlnode.addContent(content) def set_new_content(self,ns_uri,name): """Set stanza payload to a new XML element. :Parameters: - `ns_uri`: XML namespace URI of the element. - `name`: element name. :Types: - `ns_uri`: `str` - `name`: `str` or `unicode` """ while self.xmlnode.children: self.xmlnode.children.unlinkNode() return self.add_new_content(ns_uri,name) def add_new_content(self,ns_uri,name): """Add a new XML element to the stanza payload. :Parameters: - `ns_uri`: XML namespace URI of the element. - `name`: element name. :Types: - `ns_uri`: `str` - `name`: `str` or `unicode` """ c=self.xmlnode.newChild(None,to_utf8(name),None) if ns_uri: ns=c.newNs(ns_uri,None) c.setNs(ns) return c def xpath_eval(self,expr,namespaces=None): """Evaluate an XPath expression on the stanza XML node. The expression will be evaluated in context where the common namespace (the one used for stanza elements, mapped to 'jabber:client', 'jabber:server', etc.) is bound to prefix "ns" and other namespaces are bound accordingly to the `namespaces` list. :Parameters: - `expr`: XPath expression. - `namespaces`: mapping from namespace prefixes to URIs. :Types: - `expr`: `unicode` - `namespaces`: `dict` or other mapping """ ctxt = common_doc.xpathNewContext() ctxt.setContextNode(self.xmlnode) ctxt.xpathRegisterNs("ns",COMMON_NS) if namespaces: for prefix,uri in namespaces.items(): ctxt.xpathRegisterNs(unicode(prefix),uri) ret=ctxt.xpathEval(unicode(expr)) ctxt.xpathFreeContext() return ret def __eq__(self,other): if not isinstance(other,Stanza): return False return self.xmlnode.serialize()==other.xmlnode.serialize() def __ne__(self,other): if not isinstance(other,Stanza): return True return self.xmlnode.serialize()!=other.xmlnode.serialize() # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/xmlextra.py0000644000175000017500000004127511560025462017277 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # pylint: disable-msg=C0103, W0132, W0611 """Extension to libxml2 for XMPP stream and stanza processing""" __docformat__="restructuredtext en" import sys import libxml2 import threading import re import logging from pyxmpp.exceptions import StreamParseError common_doc = libxml2.newDoc("1.0") common_root = common_doc.newChild(None,"root",None) COMMON_NS = "http://pyxmpp.jajcus.net/xmlns/common" common_ns = common_root.newNs(COMMON_NS, None) common_root.setNs(common_ns) common_doc.setRootElement(common_root) logger = logging.getLogger("pyxmpp.xmlextra") class StreamHandler: """Base class for stream handler.""" def __init__(self): pass def _stream_start(self,_doc): """Process stream start.""" doc=libxml2.xmlDoc(_doc) self.stream_start(doc) def _stream_end(self,_doc): """Process stream end.""" doc=libxml2.xmlDoc(_doc) self.stream_end(doc) def _stanza(self,_doc,_node): """Process complete stanza.""" doc=libxml2.xmlDoc(_doc) node=libxml2.xmlNode(_node) self.stanza(doc,node) def stream_start(self,doc): """Called when the start tag of root element is encountered in the stream. :Parameters: - `doc`: the document being parsed. :Types: - `doc`: `libxml2.xmlDoc`""" print >>sys.stderr,"Unhandled stream start:",`doc.serialize()` def stream_end(self,doc): """Called when the end tag of root element is encountered in the stream. :Parameters: - `doc`: the document being parsed. :Types: - `doc`: `libxml2.xmlDoc`""" print >>sys.stderr,"Unhandled stream end",`doc.serialize()` def stanza(self, _unused, node): """Called when the end tag of a direct child of the root element is encountered in the stream. Please note, that node will be removed from the document and freed after this method returns. If it is needed after that a copy must be made before the method returns. :Parameters: - `_unused`: the document being parsed. - `node`: the (complete) element being processed :Types: - `_unused`: `libxml2.xmlDoc` - `node`: `libxml2.xmlNode`""" print >>sys.stderr,"Unhandled stanza",`node.serialize()` def error(self,descr): """Called when an error is encountered in the stream. :Parameters: - `descr`: description of the error :Types: - `descr`: `str`""" raise StreamParseError,descr def warning(self,desc): """Called when an warning is encountered in the stream. :Parameters: - `descr`: description of the warning :Types: - `descr`: `str`""" # we know vcard-temp is bad... if desc.startswith('xmlns: URI vcard-temp is not absolute'): return # this is also bad... if desc.startswith('xmlns: http://www.xmpp.org/extensions/xep-0084.html#'): return logger.warning("XML STREAM WARNING: {0}".format(desc)) try: ######################################################################### # C-extension based workarounds for libxml2 limitations #------------------------------------------------------- from pyxmpp import _xmlextra from pyxmpp._xmlextra import error _create_reader = _xmlextra.sax_reader_new def replace_ns(node, old_ns,new_ns): """Replace namespaces in a whole subtree. The old namespace declaration will be removed if present on the `node`. :Parameters: - `node`: the root of the subtree where namespaces should be replaced. - `old_ns`: the namespace to replace. - `new_ns`: the namespace to be used instead of old_ns. :Types: - `node`: `libxml2.xmlNode` - `old_ns`: `libxml2.xmlNs` - `new_ns`: `libxml2.xmlNs` Both old_ns and new_ns may be None meaning no namespace set.""" if old_ns is None: old_ns__o = None else: old_ns__o = old_ns._o if new_ns is None: new_ns__o = None else: new_ns__o = new_ns._o if node is None: node__o = None else: node__o = node._o _xmlextra.replace_ns(node__o, old_ns__o, new_ns__o) if old_ns__o: _xmlextra.remove_ns(node__o, old_ns__o) pure_python = False except ImportError: ######################################################################### # Pure python implementation (slow workarounds for libxml2 limitations) #----------------------------------------------------------------------- class error(Exception): """Exception raised on a stream parse error.""" pass def _escape(data): """Escape data for XML""" data=data.replace("&","&") data=data.replace("<","<") data=data.replace(">",">") data=data.replace("'","'") data=data.replace('"',""") return data class _SAXCallback(libxml2.SAXCallback): """SAX events handler for the python-only stream parser.""" def __init__(self, handler): """Initialize the SAX handler. :Parameters: - `handler`: Object to handle stream start, end and stanzas. :Types: - `handler`: `StreamHandler` """ self._handler = handler self._head = "" self._tail = "" self._current = "" self._level = 0 self._doc = None self._root = None def cdataBlock(self, data): "" if self._level>1: self._current += _escape(data) def characters(self, data): "" if self._level>1: self._current += _escape(data) def comment(self, content): "" pass def endDocument(self): "" pass def endElement(self, tag): "" self._current+="" % (tag,) self._level -= 1 if self._level > 1: return if self._level==1: xml=self._head+self._current+self._tail doc=libxml2.parseDoc(xml) try: node = doc.getRootElement().children try: node1 = node.docCopyNode(self._doc, 1) try: self._root.addChild(node1) self._handler.stanza(self._doc, node1) except Exception, e: node1.unlinkNode() node1.freeNode() del node1 raise e finally: del node finally: doc.freeDoc() else: xml=self._head+self._tail doc=libxml2.parseDoc(xml) try: self._handler.stream_end(self._doc) self._doc.freeDoc() self._doc = None self._root = None finally: doc.freeDoc() def error(self, msg): "" self._handler.error(msg) fatalError = error ignorableWhitespace = characters def reference(self, name): "" self._current += "&" + name + ";" def startDocument(self): "" pass def startElement(self, tag, attrs): "" s = "<"+tag if attrs: for a,v in attrs.items(): s+=" %s='%s'" % (a,_escape(v)) s += ">" if self._level == 0: self._head = s self._tail = "" % (tag,) xml=self._head+self._tail self._doc = libxml2.parseDoc(xml) self._handler.stream_start(self._doc) self._root = self._doc.getRootElement() elif self._level == 1: self._current = s else: self._current += s self._level += 1 def warning(self): "" pass class _PythonReader: """Python-only stream reader.""" def __init__(self,handler): """Initialize the reader. :Parameters: - `handler`: Object to handle stream start, end and stanzas. :Types: - `handler`: `StreamHandler` """ self.handler = handler self.sax = _SAXCallback(handler) self.parser = libxml2.createPushParser(self.sax, '', 0, 'stream') def feed(self, data): """Feed the parser with a chunk of data. Apropriate methods of `self.handler` will be called whenever something interesting is found. :Parameters: - `data`: the chunk of data to parse. :Types: - `data`: `str`""" return self.parser.parseChunk(data, len(data), 0) _create_reader = _PythonReader def _get_ns(node): """Get namespace of node. :return: the namespace object or `None` if the node has no namespace assigned. :returntype: `libxml2.xmlNs`""" try: return node.ns() except libxml2.treeError: return None def replace_ns(node, old_ns, new_ns): """Replace namespaces in a whole subtree. :Parameters: - `node`: the root of the subtree where namespaces should be replaced. - `old_ns`: the namespace to replace. - `new_ns`: the namespace to be used instead of old_ns. :Types: - `node`: `libxml2.xmlNode` - `old_ns`: `libxml2.xmlNs` - `new_ns`: `libxml2.xmlNs` Both old_ns and new_ns may be None meaning no namespace set.""" if old_ns is not None: old_ns_uri = old_ns.content old_ns_prefix = old_ns.name else: old_ns_uri = None old_ns_prefix = None ns = _get_ns(node) if ns is None and old_ns is None: node.setNs(new_ns) elif ns and ns.content == old_ns_uri and ns.name == old_ns_prefix: node.setNs(new_ns) p = node.properties while p: ns = _get_ns(p) if ns is None and old_ns is None: p.setNs(new_ns) if ns and ns.content == old_ns_uri and ns.name == old_ns_prefix: p.setNs(new_ns) p = p.next n = node.children while n: if n.type == 'element': skip_element = False try: nsd = n.nsDefs() except libxml2.treeError: nsd = None while nsd: if nsd.name == old_ns_prefix: skip_element = True break nsd = nsd.next if not skip_element: replace_ns(n, old_ns, new_ns) n = n.next pure_python = True ########################################################### # Common code #------------- def get_node_ns(xmlnode): """Namespace of an XML node. :Parameters: - `xmlnode`: the XML node to query. :Types: - `xmlnode`: `libxml2.xmlNode` :return: namespace of the node or `None` :returntype: `libxml2.xmlNs`""" try: return xmlnode.ns() except libxml2.treeError: return None def get_node_ns_uri(xmlnode): """Return namespace URI of an XML node. :Parameters: - `xmlnode`: the XML node to query. :Types: - `xmlnode`: `libxml2.xmlNode` :return: namespace URI of the node or `None` :returntype: `unicode`""" ns=get_node_ns(xmlnode) if ns: return unicode(ns.getContent(),"utf-8") else: return None def xml_node_iter(nodelist): """Iterate over sibling XML nodes. All types of nodes will be returned (not only the elements). Usually used to iterade over node's children like this:: xml_node_iter(node.children) :Parameters: - `nodelist`: start node of the list. :Types: - `nodelist`: `libxml2.xmlNode` """ node = nodelist while node: yield node node = node.next def xml_element_iter(nodelist): """Iterate over sibling XML elements. Non-element nodes will be skipped. Usually used to iterade over node's children like this:: xml_node_iter(node.children) :Parameters: - `nodelist`: start node of the list. :Types: - `nodelist`: `libxml2.xmlNode` """ node = nodelist while node: if node.type == "element": yield node node = node.next def xml_element_ns_iter(nodelist, ns_uri): """Iterate over sibling XML elements. Only elements in the given namespace will be returned. Usually used to iterade over node's children like this:: xml_node_iter(node.children) :Parameters: - `nodelist`: start node of the list. :Types: - `nodelist`: `libxml2.xmlNode` """ node = nodelist while node: if node.type == "element" and get_node_ns_uri(node)==ns_uri: yield node node = node.next evil_characters_re=re.compile(r"[\000-\010\013\014\016-\037]",re.UNICODE) utf8_replacement_char=u"\ufffd".encode("utf-8") def remove_evil_characters(s): """Remove control characters (not allowed in XML) from a string.""" if isinstance(s,unicode): return evil_characters_re.sub(u"\ufffd",s) else: return evil_characters_re.sub(utf8_replacement_char,s) bad_nsdef_replace_re=re.compile(r"^([^<]*\<[^><]*\s+)(xmlns=((\"[^\"]*\")|(\'[^\']*\')))") def safe_serialize(xmlnode): """Serialize an XML element making sure the result is sane. Remove control characters and invalid namespace declarations from the result string. :Parameters: - `xmlnode`: the XML element to serialize. :Types: - `xmlnode`: `libxml2.xmlNode` :return: UTF-8 encoded serialized and sanitized element. :returntype: `string`""" try: ns = xmlnode.ns() except libxml2.treeError: ns = None try: nsdef = xmlnode.nsDefs() except libxml2.treeError: nsdef = None s=xmlnode.serialize(encoding="UTF-8") while nsdef: if nsdef.name is None and (not ns or (nsdef.name, nsdef.content)!=(ns.name, ns.content)): s = bad_nsdef_replace_re.sub("\\1",s,1) break nsdef = nsdef.next s=remove_evil_characters(s) return s class StreamReader: """A simple push-parser interface for XML streams.""" def __init__(self,handler): """Initialize `StreamReader` object. :Parameters: - `handler`: handler object for the stream content :Types: - `handler`: `StreamHandler` derived class """ self.reader=_create_reader(handler) self.lock=threading.RLock() self.in_use=0 def doc(self): """Get the document being parsed. :return: the document. :returntype: `libxml2.xmlNode`""" ret=self.reader.doc() if ret: return libxml2.xmlDoc(ret) else: return None def feed(self,s): """Pass a string to the stream parser. Parameters: - `s`: string to parse. Types: - `s`: `str` :return: `None` on EOF, `False` when whole input was parsed and `True` if there is something still left in the buffer.""" self.lock.acquire() if self.in_use: self.lock.release() raise StreamParseError,"StreamReader.feed() is not reentrant!" self.in_use=1 try: return self.reader.feed(s) finally: self.in_use=0 self.lock.release() # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/clientstream.py0000644000175000017500000003226011560025462020117 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # pylint: disable-msg=W0221 """Client stream handling. Normative reference: - `RFC 3920 `__ """ __docformat__="restructuredtext en" import logging from pyxmpp.stream import Stream from pyxmpp.streambase import BIND_NS from pyxmpp.streamsasl import SASLNotAvailable,SASLMechanismNotAvailable from pyxmpp.jid import JID from pyxmpp.utils import to_utf8 from pyxmpp.exceptions import StreamError,StreamAuthenticationError,FatalStreamError from pyxmpp.exceptions import ClientStreamError, FatalClientStreamError class ClientStream(Stream): """Handles XMPP-IM client connection stream. Both client and server side of the connection is supported. This class handles client SASL authentication, authorisation and resource binding. This class is not ready for handling of legacy Jabber servers, as it doesn't provide legacy authentication. :Ivariables: - `my_jid`: requested local JID. Please notice that this may differ from `me`, which is actual authorized JID after the resource binding. - `server`: server to use. - `port`: port number to use. - `password`: user's password. - `auth_methods`: allowed authentication methods. :Types: - `my_jid`: `pyxmpp.JID` - `server`: `str` - `port`: `int` - `password`: `str` - `auth_methods`: `list` of `str` """ def __init__(self, jid, password=None, server=None, port=None, auth_methods = ("sasl:DIGEST-MD5",), tls_settings = None, keepalive = 0, owner = None): """Initialize the ClientStream object. :Parameters: - `jid`: local JID. - `password`: user's password. - `server`: server to use. If not given then address will be derived form the JID. - `port`: port number to use. If not given then address will be derived form the JID. - `auth_methods`: sallowed authentication methods. SASL authentication mechanisms in the list should be prefixed with "sasl:" string. - `tls_settings`: settings for StartTLS -- `TLSSettings` instance. - `keepalive`: keepalive output interval. 0 to disable. - `owner`: `Client`, `Component` or similar object "owning" this stream. :Types: - `jid`: `pyxmpp.JID` - `password`: `unicode` - `server`: `unicode` - `port`: `int` - `auth_methods`: sequence of `str` - `tls_settings`: `pyxmpp.TLSSettings` - `keepalive`: `int` """ sasl_mechanisms=[] for m in auth_methods: if not m.startswith("sasl:"): continue m=m[5:].upper() sasl_mechanisms.append(m) Stream.__init__(self, "jabber:client", sasl_mechanisms = sasl_mechanisms, tls_settings = tls_settings, keepalive = keepalive, owner = owner) self.server=server self.port=port self.password=password self.auth_methods=auth_methods self.my_jid=jid self.me = None self._auth_methods_left = None self.__logger=logging.getLogger("pyxmpp.ClientStream") def _reset(self): """Reset `ClientStream` object state, making the object ready to handle new connections.""" Stream._reset(self) self._auth_methods_left=[] def connect(self,server=None,port=None): """Establish a client connection to a server. [client only] :Parameters: - `server`: name or address of the server to use. Not recommended -- proper value should be derived automatically from the JID. - `port`: port number of the server to use. Not recommended -- proper value should be derived automatically from the JID. :Types: - `server`: `unicode` - `port`: `int`""" self.lock.acquire() try: self._connect(server,port) finally: self.lock.release() def _connect(self,server=None,port=None): """Same as `ClientStream.connect` but assume `self.lock` is acquired.""" if not self.my_jid.node or not self.my_jid.resource: raise ClientStreamError,"Client JID must have username and resource" if not server: server=self.server if not port: port=self.port if server: self.__logger.debug("server: %r", (server,)) service=None else: service="xmpp-client" if port is None: port=5222 if server is None: server=self.my_jid.domain self.me=self.my_jid Stream._connect(self,server,port,service,self.my_jid.domain) def accept(self,sock): """Accept an incoming client connection. [server only] :Parameters: - `sock`: a listening socket.""" Stream.accept(self,sock,self.my_jid) def _post_connect(self): """Initialize authentication when the connection is established and we are the initiator.""" if self.initiator: self._auth_methods_left=list(self.auth_methods) self._try_auth() def _try_auth(self): """Try to authenticate using the first one of allowed authentication methods left. [client only]""" if not self.doc_out: self.__logger.debug("try_auth: disconnecting already?") return if self.authenticated: self.__logger.debug("try_auth: already authenticated") return self.__logger.debug("trying auth: %r", (self._auth_methods_left,)) if not self._auth_methods_left: raise StreamAuthenticationError,"No allowed authentication methods available" method=self._auth_methods_left[0] if method.startswith("sasl:"): if self.version: self._auth_methods_left.pop(0) try: mechanism = method[5:].upper() # A bit hackish, but I'm not sure whether giving authzid won't mess something up if mechanism != "EXTERNAL": self._sasl_authenticate(self.my_jid.node, None, mechanism=mechanism) else: self._sasl_authenticate(self.my_jid.node, self.my_jid.bare().as_utf8(), mechanism=mechanism) except (SASLMechanismNotAvailable,SASLNotAvailable): self.__logger.debug("Skipping unavailable auth method: %s", (method,) ) return self._try_auth() else: self._auth_methods_left.pop(0) self.__logger.debug("Skipping auth method %s as legacy protocol is in use", (method,) ) return self._try_auth() else: self._auth_methods_left.pop(0) self.__logger.debug("Skipping unknown auth method: %s", method) return self._try_auth() def _get_stream_features(self): """Include resource binding feature in the stream features list. [server only]""" features=Stream._get_stream_features(self) if self.peer_authenticated: bind=features.newChild(None,"bind",None) ns=bind.newNs(BIND_NS,None) bind.setNs(ns) self.set_iq_set_handler("bind",BIND_NS,self.do_bind) return features def do_bind(self,stanza): """Do the resource binding requested by a client connected. [server only] :Parameters: - `stanza`: resource binding request stanza. :Types: - `stanza`: `pyxmpp.Iq`""" fr=stanza.get_from() if fr and fr!=self.peer: r=stanza.make_error_response("forbidden") self.send(r) r.free() return resource_n=stanza.xpath_eval("bind:bind/bind:resource",{"bind":BIND_NS}) if resource_n: resource=resource_n[0].getContent() else: resource="auto" if not resource: r=stanza.make_error_response("bad-request") else: self.unset_iq_set_handler("bind",BIND_NS) r=stanza.make_result_response() self.peer.set_resource(resource) q=r.new_query(BIND_NS,"bind") q.newTextChild(None,"jid",to_utf8(self.peer.as_unicode())) self.state_change("authorized",self.peer) r.set_to(None) self.send(r) r.free() def get_password(self, username, realm=None, acceptable_formats=("plain",)): """Get a user password for the SASL authentication. :Parameters: - `username`: username used for authentication. - `realm`: realm used for authentication. - `acceptable_formats`: acceptable password encoding formats requested. :Types: - `username`: `unicode` - `realm`: `unicode` - `acceptable_formats`: `list` of `str` :return: The password and the format name ('plain'). :returntype: (`unicode`,`str`)""" _unused = realm if self.initiator and self.my_jid.node==username and "plain" in acceptable_formats: return self.password,"plain" else: return None,None def get_realms(self): """Get realms available for client authentication. [server only] :return: list of realms. :returntype: `list` of `unicode`""" return [self.my_jid.domain] def choose_realm(self,realm_list): """Choose authentication realm from the list provided by the server. [client only] Use domain of the own JID if no realm list was provided or the domain is on the list or the first realm on the list otherwise. :Parameters: - `realm_list`: realm list provided by the server. :Types: - `realm_list`: `list` of `unicode` :return: the realm chosen. :returntype: `unicode`""" if not realm_list: return self.my_jid.domain if self.my_jid.domain in realm_list: return self.my_jid.domain return realm_list[0] def check_authzid(self,authzid,extra_info=None): """Check authorization id provided by the client. [server only] :Parameters: - `authzid`: authorization id provided. - `extra_info`: additional information about the user from the authentication backend. This mapping will usually contain at least 'username' item. :Types: - `authzid`: unicode - `extra_info`: mapping :return: `True` if user is authorized to use that `authzid`. :returntype: `bool`""" if not extra_info: extra_info={} if not authzid: return 1 if not self.initiator: jid=JID(authzid) if not extra_info.has_key("username"): ret=0 elif jid.node!=extra_info["username"]: ret=0 elif jid.domain!=self.my_jid.domain: ret=0 elif not jid.resource: ret=0 else: ret=1 else: ret=0 return ret def get_serv_type(self): """Get the server name for SASL authentication. :return: 'xmpp'.""" return "xmpp" def get_serv_name(self): """Get the service name for SASL authentication. :return: domain of the own JID.""" return self.my_jid.domain def get_serv_host(self): """Get the service host name for SASL authentication. :return: domain of the own JID.""" # FIXME: that should be the hostname choosen from SRV records found. return self.my_jid.domain def fix_out_stanza(self,stanza): """Fix outgoing stanza. On a client clear the sender JID. On a server set the sender address to the own JID if the address is not set yet.""" if self.initiator: stanza.set_from(None) else: if not stanza.get_from(): stanza.set_from(self.my_jid) def fix_in_stanza(self,stanza): """Fix an incoming stanza. Ona server replace the sender address with authorized client JID.""" if self.initiator: Stream.fix_in_stanza(self,stanza) else: stanza.set_from(self.peer) # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/jabber/0000755000175000017500000000000011561501464016277 5ustar jajcususers00000000000000pyxmpp-1.1.2/pyxmpp/jabber/vcard.py0000644000175000017500000015427411560025462017763 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # pylint: disable-msg=W0302 """Jabber vCard and MIME (RFC 2426) vCard implementation. Normative reference: - `JEP 54 `__ - `RFC 2425 `__ - `RFC 2426 `__ """ __docformat__="restructuredtext en" import base64 import binascii import libxml2 import re import pyxmpp.jid from pyxmpp.utils import to_utf8,from_utf8 from pyxmpp.xmlextra import get_node_ns from pyxmpp.objects import StanzaPayloadObject from pyxmpp.exceptions import BadRequestProtocolError, JIDMalformedProtocolError, JIDError VCARD_NS="vcard-temp" class Empty(Exception): """Exception raised when parsing empty vcard element. Such element will be ignored.""" pass valid_string_re=re.compile(r"^[\w\d \t]*$") non_quoted_semicolon_re=re.compile(r'(? `str` - `charset`: `str` :return: the encoded RFC2425 line (possibly folded) :returntype: `str`""" if not parameters: parameters={} if type(value) is unicode: value=value.replace(u"\r\n",u"\\n") value=value.replace(u"\n",u"\\n") value=value.replace(u"\r",u"\\n") value=value.encode(charset,"replace") elif type(value) is not str: raise TypeError,"Bad type for rfc2425 value" elif not valid_string_re.match(value): parameters["encoding"]="b" value=binascii.b2a_base64(value) ret=str(name).lower() for k,v in parameters.items(): ret+=";%s=%s" % (str(k),str(v)) ret+=":" while(len(value)>70): ret+=value[:70]+"\r\n " value=value[70:] ret+=value+"\r\n" return ret class VCardField: """Base class for vCard fields. :Ivariables: - `name`: name of the field. """ def __init__(self,name): """Initialize a `VCardField` object. Set its name. :Parameters: - `name`: field name :Types: - `name`: `str`""" self.name=name def __repr__(self): return "<%s %r>" % (self.__class__,self.rfc2426()) def rfc2426(self): """RFC2426-encode the field content. :return: the field in the RFC 2426 format. :returntype: `str`""" return "" class VCardString(VCardField): """Generic class for all standard text fields in the vCard. :Ivariables: - `value`: field value. :Types: - `value`: `unicode`""" def __init__(self,name, value, rfc2425parameters = None, empty_ok = False): """Initialize a `VCardString` object. :Parameters: - `name`: field name - `value`: field value as string or an XML node - `rfc2425parameters`: optional RFC 2425 parameters :Types: - `name`: `str` - `value`: `str` or `libxml2.xmlNode` - `rfc2425parameters`: `dict`""" _unused = rfc2425parameters VCardField.__init__(self,name) if isinstance(value,libxml2.xmlNode): value=value.getContent() if value: self.value=unicode(value,"utf-8","replace").strip() else: self.value=u"" else: self.value=value if not self.value and not empty_ok: raise Empty,"Empty string value" def rfc2426(self): """RFC2426-encode the field content. :return: the field in the RFC 2426 format. :returntype: `str`""" return rfc2425encode(self.name,self.value) def as_xml(self,parent): """Create vcard-tmp XML representation of the field. :Parameters: - `parent`: parent node for the element :Types: - `parent`: `libxml2.xmlNode` :return: xml node with the field data. :returntype: `libxml2.xmlNode`""" return parent.newTextChild(None, to_utf8(self.name.upper()), to_utf8(self.value)) def __unicode__(self): return self.value def __str__(self): return self.value.encode("utf-8") class VCardXString(VCardString): """Generic class for all text vCard fields not defined in RFC 2426. In the RFC 2425 representation field name will be prefixed with 'x-'. :Ivariables: - `value`: field value. :Types: - `value`: `unicode`""" def rfc2426(self): """RFC2426-encode the field content. :return: the field in the RFC 2426 format. :returntype: `str`""" return rfc2425encode("x-"+self.name,self.value) class VCardJID(VCardField): """JID vCard field. This field is not defined in RFC 2426, so it will be named 'x-jabberid' in RFC 2425 output. :Ivariables: - `value`: field value. :Types: - `value`: `JID`""" def __init__(self,name,value,rfc2425parameters=None): """Initialize a `VCardJID` object. :Parameters: - `name`: field name - `value`: field value as string or an XML node - `rfc2425parameters`: optional RFC 2425 parameters :Types: - `name`: `str` - `value`: `str` or `libxml2.xmlNode` - `rfc2425parameters`: `dict`""" _unused = rfc2425parameters VCardField.__init__(self,name) if isinstance(value,libxml2.xmlNode): try: self.value=pyxmpp.jid.JID(value.getContent()) except JIDError: raise JIDMalformedProtocolError, "JID malformed" else: self.value=pyxmpp.jid.JID(value) if not self.value: raise Empty,"Empty JID value" def rfc2426(self): """RFC2426-encode the field content. :return: the field in the RFC 2426 format. :returntype: `str`""" return rfc2425encode("x-jabberid",self.value.as_unicode()) def as_xml(self,parent): """Create vcard-tmp XML representation of the field. :Parameters: - `parent`: parent node for the element :Types: - `parent`: `libxml2.xmlNode` :return: xml node with the field data. :returntype: `libxml2.xmlNode`""" name=to_utf8(self.name.upper()) content=self.value.as_utf8() return parent.newTextChild(None, name, content) def __unicode__(self): return self.value.as_unicode() def __str__(self): return self.value.as_string() class VCardName(VCardField): """Name vCard field. :Ivariables: - `family`: family name. - `given`: given name. - `middle`: middle name. - `prefix`: name prefix. - `suffix`: name suffix. :Types: - `family`: `unicode` - `given`: `unicode` - `middle`: `unicode` - `prefix`: `unicode` - `suffix`: `unicode`""" def __init__(self,name,value,rfc2425parameters=None): """Initialize a `VCardName` object. :Parameters: - `name`: field name - `value`: field value as string or an XML node - `rfc2425parameters`: optional RFC 2425 parameters :Types: - `name`: `str` - `value`: `str` or `libxml2.xmlNode` - `rfc2425parameters`: `dict`""" _unused = rfc2425parameters VCardField.__init__(self,name) if self.name.upper()!="N": raise RuntimeError,"VCardName handles only 'N' type" if isinstance(value,libxml2.xmlNode): self.family,self.given,self.middle,self.prefix,self.suffix=[u""]*5 empty=1 n=value.children vns=get_node_ns(value) while n: if n.type!='element': n=n.next continue ns=get_node_ns(n) if (ns and vns and ns.getContent()!=vns.getContent()): n=n.next continue if n.name=='FAMILY': self.family=unicode(n.getContent(),"utf-8") empty=0 if n.name=='GIVEN': self.given=unicode(n.getContent(),"utf-8") empty=0 if n.name=='MIDDLE': self.middle=unicode(n.getContent(),"utf-8") empty=0 if n.name=='PREFIX': self.prefix=unicode(n.getContent(),"utf-8") empty=0 if n.name=='SUFFIX': self.suffix=unicode(n.getContent(),"utf-8") empty=0 n=n.next if empty: raise Empty, "Empty N value" else: v=non_quoted_semicolon_re.split(value) value=[u""]*5 value[:len(v)]=v self.family,self.given,self.middle,self.prefix,self.suffix=( unquote_semicolon(val) for val in value) def rfc2426(self): """RFC2426-encode the field content. :return: the field in the RFC 2426 format. :returntype: `str`""" return rfc2425encode("n",u';'.join(quote_semicolon(val) for val in (self.family,self.given,self.middle,self.prefix,self.suffix))) def as_xml(self,parent): """Create vcard-tmp XML representation of the field. :Parameters: - `parent`: parent node for the element :Types: - `parent`: `libxml2.xmlNode` :return: xml node with the field data. :returntype: `libxml2.xmlNode`""" n=parent.newChild(None,"N",None) n.newTextChild(None,"FAMILY",to_utf8(self.family)) n.newTextChild(None,"GIVEN",to_utf8(self.given)) n.newTextChild(None,"MIDDLE",to_utf8(self.middle)) n.newTextChild(None,"PREFIX",to_utf8(self.prefix)) n.newTextChild(None,"SUFFIX",to_utf8(self.suffix)) return n def __unicode__(self): r=[] if self.prefix: r.append(self.prefix.replace(u",",u" ")) if self.given: r.append(self.given.replace(u",",u" ")) if self.middle: r.append(self.middle.replace(u",",u" ")) if self.family: r.append(self.family.replace(u",",u" ")) if self.suffix: r.append(self.suffix.replace(u",",u" ")) return u" ".join(r) def __str__(self): return self.__unicode__().encode("utf-8") class VCardImage(VCardField): """Image vCard field. :Ivariables: - `image`: image binary data (when `uri` is None) - `uri`: image URI (when `image` is None) - `type`: optional image type :Types: - `image`: `str` - `uri`: `unicode` - `type`: `unicode`""" def __init__(self,name,value,rfc2425parameters=None): """Initialize a `VCardImage` object. :Parameters: - `name`: field name - `value`: field value as string or an XML node - `rfc2425parameters`: optional RFC 2425 parameters :Types: - `name`: `str` - `value`: `str` or `libxml2.xmlNode` - `rfc2425parameters`: `dict`""" VCardField.__init__(self,name) if not rfc2425parameters: rfc2425parameters={} self.uri,self.type,self.image=[None]*3 if isinstance(value,libxml2.xmlNode): n=value.children vns=get_node_ns(value) while n: if n.type!='element': n=n.next continue ns=get_node_ns(n) if (ns and vns and ns.getContent()!=vns.getContent()): n=n.next continue if n.name=='TYPE': self.type=unicode(n.getContent(),"utf-8","replace") if n.name=='BINVAL': self.image=base64.decodestring(n.getContent()) if n.name=='EXTVAL': self.uri=unicode(n.getContent(),"utf-8","replace") n=n.next if (self.uri and self.image) or (not self.uri and not self.image): raise ValueError,"Bad %s value in vcard" % (name,) if (not self.uri and not self.image): raise Empty,"Bad %s value in vcard" % (name,) else: if rfc2425parameters.get("value", "").lower()=="uri": self.uri=value self.type=None else: self.type=rfc2425parameters.get("type") self.image=value def rfc2426(self): """RFC2426-encode the field content. :return: the field in the RFC 2426 format. :returntype: `str`""" if self.uri: return rfc2425encode(self.name,self.uri,{"value":"uri"}) elif self.image: if self.type: p={"type":self.type} else: p={} return rfc2425encode(self.name,self.image,p) def as_xml(self,parent): """Create vcard-tmp XML representation of the field. :Parameters: - `parent`: parent node for the element :Types: - `parent`: `libxml2.xmlNode` :return: xml node with the field data. :returntype: `libxml2.xmlNode`""" n=parent.newChild(None,self.name.upper(),None) if self.uri: n.newTextChild(None,"EXTVAL",to_utf8(self.uri)) else: if self.type: n.newTextChild(None,"TYPE",self.type) n.newTextChild(None,"BINVAL",binascii.b2a_base64(self.image)) return n def __unicode__(self): if self.uri: return self.uri if self.type: return u"(%s data)" % (self.type,) return u"(binary data)" def __str__(self): return self.__unicode__().encode("utf-8") class VCardAdr(VCardField): """Address vCard field. :Ivariables: - `type`: type of the address. - `pobox`: the post office box. - `extadr`: the extended address. - `street`: the street address. - `locality`: the locality (e.g. city). - `region`: the region. - `pcode`: the postal code. - `ctry`: the country. :Types: - `type`: `list` of "home","work","postal","parcel","dom","intl" or "pref" - `pobox`: `unicode` - `extadr`: `unicode` - `street`: `unicode` - `locality`: `unicode` - `region`: `unicode` - `pcode`: `unicode` - `ctry`: `unicode`""" def __init__(self,name,value,rfc2425parameters=None): """Initialize a `VCardAdr` object. :Parameters: - `name`: field name - `value`: field value as string or an XML node - `rfc2425parameters`: optional RFC 2425 parameters :Types: - `name`: `str` - `value`: `str` or `libxml2.xmlNode` - `rfc2425parameters`: `dict`""" VCardField.__init__(self,name) if not rfc2425parameters: rfc2425parameters={} if self.name.upper()!="ADR": raise RuntimeError,"VCardAdr handles only 'ADR' type" (self.pobox,self.extadr,self.street,self.locality, self.region,self.pcode,self.ctry)=[""]*7 self.type=[] if isinstance(value,libxml2.xmlNode): self.__from_xml(value) else: t=rfc2425parameters.get("type") if t: self.type=t.split(",") else: self.type=["intl","postal","parcel","work"] v=non_quoted_semicolon_re.split(value) value=[""]*7 value[:len(v)]=v (self.pobox,self.extadr,self.street,self.locality, self.region,self.pcode,self.ctry)=( unquote_semicolon(val) for val in value) def __from_xml(self,value): """Initialize a `VCardAdr` object from and XML element. :Parameters: - `value`: field value as an XML node :Types: - `value`: `libxml2.xmlNode`""" n=value.children vns=get_node_ns(value) while n: if n.type!='element': n=n.next continue ns=get_node_ns(n) if (ns and vns and ns.getContent()!=vns.getContent()): n=n.next continue if n.name=='POBOX': self.pobox=unicode(n.getContent(),"utf-8","replace") elif n.name in ('EXTADR', 'EXTADD'): self.extadr=unicode(n.getContent(),"utf-8","replace") elif n.name=='STREET': self.street=unicode(n.getContent(),"utf-8","replace") elif n.name=='LOCALITY': self.locality=unicode(n.getContent(),"utf-8","replace") elif n.name=='REGION': self.region=unicode(n.getContent(),"utf-8","replace") elif n.name=='PCODE': self.pcode=unicode(n.getContent(),"utf-8","replace") elif n.name=='CTRY': self.ctry=unicode(n.getContent(),"utf-8","replace") elif n.name in ("HOME","WORK","POSTAL","PARCEL","DOM","INTL", "PREF"): self.type.append(n.name.lower()) n=n.next if self.type==[]: self.type=["intl","postal","parcel","work"] elif "dom" in self.type and "intl" in self.type: raise ValueError,"Both 'dom' and 'intl' specified in vcard ADR" def rfc2426(self): """RFC2426-encode the field content. :return: the field in the RFC 2426 format. :returntype: `str`""" return rfc2425encode("adr",u';'.join(quote_semicolon(val) for val in (self.pobox,self.extadr,self.street,self.locality, self.region,self.pcode,self.ctry)), {"type":",".join(self.type)}) def as_xml(self,parent): """Create vcard-tmp XML representation of the field. :Parameters: - `parent`: parent node for the element :Types: - `parent`: `libxml2.xmlNode` :return: xml node with the field data. :returntype: `libxml2.xmlNode`""" n=parent.newChild(None,"ADR",None) for t in ("home","work","postal","parcel","dom","intl","pref"): if t in self.type: n.newChild(None,t.upper(),None) n.newTextChild(None,"POBOX",to_utf8(self.pobox)) n.newTextChild(None,"EXTADD",to_utf8(self.extadr)) n.newTextChild(None,"STREET",to_utf8(self.street)) n.newTextChild(None,"LOCALITY",to_utf8(self.locality)) n.newTextChild(None,"REGION",to_utf8(self.region)) n.newTextChild(None,"PCODE",to_utf8(self.pcode)) n.newTextChild(None,"CTRY",to_utf8(self.ctry)) return n class VCardLabel(VCardField): """Address label vCard field. :Ivariables: - `lines`: list of label text lines. - `type`: type of the label. :Types: - `lines`: `list` of `unicode` - `type`: `list` of "home","work","postal","parcel","dom","intl" or "pref" """ def __init__(self,name,value,rfc2425parameters=None): """Initialize a `VCardLabel` object. :Parameters: - `name`: field name - `value`: field value as string or an XML node - `rfc2425parameters`: optional RFC 2425 parameters :Types: - `name`: `str` - `value`: `str` or `libxml2.xmlNode` - `rfc2425parameters`: `dict`""" VCardField.__init__(self,name) if not rfc2425parameters: rfc2425parameters={} if self.name.upper()!="LABEL": raise RuntimeError,"VCardAdr handles only 'LABEL' type" if isinstance(value,libxml2.xmlNode): self.lines=[] self.type=[] n=value.children vns=get_node_ns(value) while n: if n.type!='element': n=n.next continue ns=get_node_ns(n) if (ns and vns and ns.getContent()!=vns.getContent()): n=n.next continue if n.name=='LINE': l=unicode(n.getContent(),"utf-8","replace").strip() l=l.replace("\n"," ").replace("\r"," ") self.lines.append(l) elif n.name in ("HOME","WORK","POSTAL","PARCEL","DOM","INTL", "PREF"): self.type.append(n.name.lower()) n=n.next if self.type==[]: self.type=["intl","postal","parcel","work"] elif "dom" in self.type and "intl" in self.type: raise ValueError,"Both 'dom' and 'intl' specified in vcard LABEL" if not self.lines: self.lines=[""] else: t=rfc2425parameters.get("type") if t: self.type=t.split(",") else: self.type=["intl","postal","parcel","work"] self.lines=value.split("\\n") def rfc2426(self): """RFC2426-encode the field content. :return: the field in the RFC 2426 format. :returntype: `str`""" return rfc2425encode("label",u"\n".join(self.lines), {"type":",".join(self.type)}) def as_xml(self,parent): """Create vcard-tmp XML representation of the field. :Parameters: - `parent`: parent node for the element :Types: - `parent`: `libxml2.xmlNode` :return: xml node with the field data. :returntype: `libxml2.xmlNode`""" n=parent.newChild(None,"ADR",None) for t in ("home","work","postal","parcel","dom","intl","pref"): if t in self.type: n.newChild(None,t.upper(),None) for l in self.lines: n.newTextChild(None,"LINE",l) return n class VCardTel(VCardField): """Telephone vCard field. :Ivariables: - `number`: phone number. - `type`: type of the phone number. :Types: - `number`: `unicode` - `type`: `list` of "home","work","voice","fax","pager","msg","cell","video","bbs","modem","isdn","pcs" or "pref". """ def __init__(self,name,value,rfc2425parameters=None): """Initialize a `VCardTel` object. :Parameters: - `name`: field name - `value`: field value as string or an XML node - `rfc2425parameters`: optional RFC 2425 parameters :Types: - `name`: `str` - `value`: `str` or `libxml2.xmlNode` - `rfc2425parameters`: `dict`""" VCardField.__init__(self,name) if not rfc2425parameters: rfc2425parameters={} if self.name.upper()!="TEL": raise RuntimeError,"VCardTel handles only 'TEL' type" if isinstance(value,libxml2.xmlNode): self.number=None self.type=[] n=value.children vns=get_node_ns(value) while n: if n.type!='element': n=n.next continue ns=get_node_ns(n) if (ns and vns and ns.getContent()!=vns.getContent()): n=n.next continue if n.name=='NUMBER': self.number=unicode(n.getContent(),"utf-8","replace") elif n.name in ("HOME","WORK","VOICE","FAX","PAGER","MSG", "CELL","VIDEO","BBS","MODEM","ISDN","PCS", "PREF"): self.type.append(n.name.lower()) n=n.next if self.type==[]: self.type=["voice"] if not self.number: raise Empty,"No tel number" else: t=rfc2425parameters.get("type") if t: self.type=t.split(",") else: self.type=["voice"] self.number=value def rfc2426(self): """RFC2426-encode the field content. :return: the field in the RFC 2426 format. :returntype: `str`""" return rfc2425encode("tel",self.number,{"type":",".join(self.type)}) def as_xml(self,parent): """Create vcard-tmp XML representation of the field. :Parameters: - `parent`: parent node for the element :Types: - `parent`: `libxml2.xmlNode` :return: xml node with the field data. :returntype: `libxml2.xmlNode`""" n=parent.newChild(None,"TEL",None) for t in ("home","work","voice","fax","pager","msg","cell","video", "bbs","modem","isdn","pcs","pref"): if t in self.type: n.newChild(None,t.upper(),None) n.newTextChild(None,"NUMBER",to_utf8(self.number)) return n class VCardEmail(VCardField): """E-mail vCard field. :Ivariables: - `address`: e-mail address. - `type`: type of the address. :Types: - `address`: `unicode` - `type`: `list` of "home","work","internet" or "x400". """ def __init__(self,name,value,rfc2425parameters=None): """Initialize a `VCardEmail` object. :Parameters: - `name`: field name - `value`: field value as string or an XML node - `rfc2425parameters`: optional RFC 2425 parameters :Types: - `name`: `str` - `value`: `str` or `libxml2.xmlNode` - `rfc2425parameters`: `dict`""" VCardField.__init__(self,name) if not rfc2425parameters: rfc2425parameters={} if self.name.upper()!="EMAIL": raise RuntimeError,"VCardEmail handles only 'EMAIL' type" if isinstance(value,libxml2.xmlNode): self.address=None self.type=[] n=value.children vns=get_node_ns(value) while n: if n.type!='element': n=n.next continue ns=get_node_ns(n) if (ns and vns and ns.getContent()!=vns.getContent()): n=n.next continue if n.name=='USERID': self.address=unicode(n.getContent(),"utf-8","replace") elif n.name in ("HOME","WORK","INTERNET","X400"): self.type.append(n.name.lower()) n=n.next if self.type==[]: self.type=["internet"] if not self.address: raise Empty,"No USERID" else: t=rfc2425parameters.get("type") if t: self.type=t.split(",") else: self.type=["internet"] self.address=value def rfc2426(self): """RFC2426-encode the field content. :return: the field in the RFC 2426 format. :returntype: `str`""" return rfc2425encode("email",self.address,{"type":",".join(self.type)}) def as_xml(self,parent): """Create vcard-tmp XML representation of the field. :Parameters: - `parent`: parent node for the element :Types: - `parent`: `libxml2.xmlNode` :return: xml node with the field data. :returntype: `libxml2.xmlNode`""" n=parent.newChild(None,"EMAIL",None) for t in ("home","work","internet","x400"): if t in self.type: n.newChild(None,t.upper(),None) n.newTextChild(None,"USERID",to_utf8(self.address)) return n class VCardGeo(VCardField): """Geographical location vCard field. :Ivariables: - `lat`: the latitude. - `lon`: the longitude. :Types: - `lat`: `unicode` - `lon`: `unicode` """ def __init__(self,name,value,rfc2425parameters=None): """Initialize a `VCardGeo` object. :Parameters: - `name`: field name - `value`: field value as string or an XML node - `rfc2425parameters`: optional RFC 2425 parameters :Types: - `name`: `str` - `value`: `str` or `libxml2.xmlNode` - `rfc2425parameters`: `dict`""" _unused = rfc2425parameters VCardField.__init__(self,name) if self.name.upper()!="GEO": raise RuntimeError,"VCardName handles only 'GEO' type" if isinstance(value,libxml2.xmlNode): self.lat,self.lon=[None]*2 n=value.children vns=get_node_ns(value) while n: if n.type!='element': n=n.next continue ns=get_node_ns(n) if (ns and vns and ns.getContent()!=vns.getContent()): n=n.next continue if n.name=='LAT': self.lat=unicode(n.getContent(),"utf-8") if n.name=='LON': self.lon=unicode(n.getContent(),"utf-8") n=n.next if not self.lat or not self.lon: raise ValueError,"Bad vcard GEO value" else: self.lat,self.lon=(unquote_semicolon(val) for val in non_quoted_semicolon_re.split(value)) def rfc2426(self): """RFC2426-encode the field content. :return: the field in the RFC 2426 format. :returntype: `str`""" return rfc2425encode("geo",u';'.join(quote_semicolon(val) for val in (self.lat,self.lon))) def as_xml(self,parent): """Create vcard-tmp XML representation of the field. :Parameters: - `parent`: parent node for the element :Types: - `parent`: `libxml2.xmlNode` :return: xml node with the field data. :returntype: `libxml2.xmlNode`""" n=parent.newChild(None,"GEO",None) n.newTextChild(None,"LAT",to_utf8(self.lat)) n.newTextChild(None,"LON",to_utf8(self.lon)) return n class VCardOrg(VCardField): """Organization vCard field. :Ivariables: - `name`: organization name. - `unit`: organizational unit. :Types: - `name`: `unicode` - `unit`: `unicode` """ def __init__(self,name,value,rfc2425parameters=None): """Initialize a `VCardOrg` object. :Parameters: - `name`: field name - `value`: field value as string or an XML node - `rfc2425parameters`: optional RFC 2425 parameters :Types: - `name`: `str` - `value`: `str` or `libxml2.xmlNode` - `rfc2425parameters`: `dict`""" _unused = rfc2425parameters VCardField.__init__(self,name) if self.name.upper()!="ORG": raise RuntimeError,"VCardName handles only 'ORG' type" if isinstance(value,libxml2.xmlNode): self.name,self.unit=None,"" n=value.children vns=get_node_ns(value) while n: if n.type!='element': n=n.next continue ns=get_node_ns(n) if (ns and vns and ns.getContent()!=vns.getContent()): n=n.next continue if n.name=='ORGNAME': self.name=unicode(n.getContent(),"utf-8") if n.name=='ORGUNIT': self.unit=unicode(n.getContent(),"utf-8") n=n.next if not self.name: raise Empty,"Bad vcard ORG value" else: sp=non_quoted_semicolon_re.split(value,1) if len(sp)>1: self.name,self.unit=(unquote_semicolon(val) for val in sp) else: self.name=unquote_semicolon(sp[0]) self.unit=None def rfc2426(self): """RFC2426-encode the field content. :return: the field in the RFC 2426 format. :returntype: `str`""" if self.unit: return rfc2425encode("org",u';'.join(quote_semicolon(val) for val in (self.name,self.unit))) else: return rfc2425encode("org",unicode(quote_semicolon(self.name))) def as_xml(self,parent): """Create vcard-tmp XML representation of the field. :Parameters: - `parent`: parent node for the element :Types: - `parent`: `libxml2.xmlNode` :return: xml node with the field data. :returntype: `libxml2.xmlNode`""" n=parent.newChild(None,"ORG",None) n.newTextChild(None,"ORGNAME",to_utf8(self.name)) n.newTextChild(None,"ORGUNIT",to_utf8(self.unit)) return n class VCardCategories(VCardField): """Categories vCard field. :Ivariables: - `keywords`: category keywords. :Types: - `keywords`: `list` of `unicode` """ def __init__(self,name,value,rfc2425parameters=None): """Initialize a `VCardCategories` object. :Parameters: - `name`: field name - `value`: field value as string or an XML node - `rfc2425parameters`: optional RFC 2425 parameters :Types: - `name`: `str` - `value`: `str` or `libxml2.xmlNode` - `rfc2425parameters`: `dict`""" _unused = rfc2425parameters VCardField.__init__(self,name) self.name=name if self.name.upper()!="CATEGORIES": raise RuntimeError,"VCardName handles only 'CATEGORIES' type" if isinstance(value,libxml2.xmlNode): self.keywords=[] n=value.children vns=get_node_ns(value) while n: if n.type!='element': n=n.next continue ns=get_node_ns(n) if (ns and vns and ns.getContent()!=vns.getContent()): n=n.next continue if n.name=='KEYWORD': self.keywords.append(unicode(n.getContent(),"utf-8")) n=n.next if not self.keywords: raise Empty,"Bad vcard CATEGORIES value" else: self.keywords=value.split(",") def rfc2426(self): """RFC2426-encode the field content. :return: the field in the RFC 2426 format. :returntype: `str`""" return rfc2425encode("keywords",u",".join(self.keywords)) def as_xml(self,parent): """Create vcard-tmp XML representation of the field. :Parameters: - `parent`: parent node for the element :Types: - `parent`: `libxml2.xmlNode` :return: xml node with the field data. :returntype: `libxml2.xmlNode`""" n=parent.newChild(None,"CATEGORIES",None) for k in self.keywords: n.newTextChild(None,"KEYWORD",to_utf8(k)) return n class VCardSound(VCardField): """Sound vCard field. :Ivariables: - `sound`: binary sound data (when `uri` is None) - `uri`: sound URI (when `sound` is None) - `phonetic`: phonetic transcription :Types: - `sound`: `str` - `uri`: `unicode` - `phonetic`: `unicode`""" def __init__(self,name,value,rfc2425parameters=None): """Initialize a `VCardSound` object. :Parameters: - `name`: field name - `value`: field value as string or an XML node - `rfc2425parameters`: optional RFC 2425 parameters :Types: - `name`: `str` - `value`: `str` or `libxml2.xmlNode` - `rfc2425parameters`: `dict`""" VCardField.__init__(self,name) if not rfc2425parameters: rfc2425parameters={} self.uri,self.sound,self.phonetic=[None]*3 if isinstance(value,libxml2.xmlNode): n=value.children vns=get_node_ns(value) while n: if n.type!='element': n=n.next continue ns=get_node_ns(n) if (ns and vns and ns.getContent()!=vns.getContent()): n=n.next continue if n.name=='BINVAL': if (self.phonetic or self.uri): raise ValueError,"Bad SOUND value in vcard" self.sound=base64.decodestring(n.getContent()) if n.name=='PHONETIC': if (self.sound or self.uri): raise ValueError,"Bad SOUND value in vcard" self.phonetic=unicode(n.getContent(),"utf-8","replace") if n.name=='EXTVAL': if (self.phonetic or self.sound): raise ValueError,"Bad SOUND value in vcard" self.uri=unicode(n.getContent(),"utf-8","replace") n=n.next if (not self.phonetic and not self.uri and not self.sound): raise Empty,"Bad SOUND value in vcard" else: if rfc2425parameters.get("value", "").lower()=="uri": self.uri=value self.sound=None self.phonetic=None else: self.sound=value self.uri=None self.phonetic=None def rfc2426(self): """RFC2426-encode the field content. :return: the field in the RFC 2426 format. :returntype: `str`""" if self.uri: return rfc2425encode(self.name,self.uri,{"value":"uri"}) elif self.sound: return rfc2425encode(self.name,self.sound) def as_xml(self,parent): """Create vcard-tmp XML representation of the field. :Parameters: - `parent`: parent node for the element :Types: - `parent`: `libxml2.xmlNode` :return: xml node with the field data. :returntype: `libxml2.xmlNode`""" n=parent.newChild(None,self.name.upper(),None) if self.uri: n.newTextChild(None,"EXTVAL",to_utf8(self.uri)) elif self.phonetic: n.newTextChild(None,"PHONETIC",to_utf8(self.phonetic)) else: n.newTextChild(None,"BINVAL",binascii.b2a_base64(self.sound)) return n class VCardPrivacy(VCardField): """Privacy vCard field. :Ivariables: - `value`: privacy information about the vcard data ("public", "private" or "confidental") :Types: - `value`: `str` """ def __init__(self,name,value,rfc2425parameters=None): """Initialize a `VCardPrivacy` object. :Parameters: - `name`: field name - `value`: field value as string or an XML node - `rfc2425parameters`: optional RFC 2425 parameters :Types: - `name`: `str` - `value`: `str` or `libxml2.xmlNode` - `rfc2425parameters`: `dict`""" _unused = rfc2425parameters VCardField.__init__(self,name) if isinstance(value,libxml2.xmlNode): self.value=None n=value.children vns=get_node_ns(value) while n: if n.type!='element': n=n.next continue ns=get_node_ns(n) if (ns and vns and ns.getContent()!=vns.getContent()): n=n.next continue if n.name=='PUBLIC': self.value="public" elif n.name=='PRIVATE': self.value="private" elif n.name=='CONFIDENTAL': self.value="confidental" n=n.next if not self.value: raise Empty else: self.value=value def rfc2426(self): """RFC2426-encode the field content. :return: the field in the RFC 2426 format. :returntype: `str`""" return rfc2425encode(self.name,self.value) def as_xml(self,parent): """Create vcard-tmp XML representation of the field. :Parameters: - `parent`: parent node for the element :Types: - `parent`: `libxml2.xmlNode` :return: xml node with the field data. :returntype: `libxml2.xmlNode`""" if self.value in ("public","private","confidental"): n=parent.newChild(None,self.name.upper(),None) n.newChild(None,self.value.upper(),None) return n return None class VCardKey(VCardField): """Key vCard field. :Ivariables: - `type`: key type. - `cred`: key data. :Types: - `type`: `unicode` - `cred`: `str` """ def __init__(self,name,value,rfc2425parameters=None): """Initialize a `VCardKey` object. :Parameters: - `name`: field name - `value`: field value as string or an XML node - `rfc2425parameters`: optional RFC 2425 parameters :Types: - `name`: `str` - `value`: `str` or `libxml2.xmlNode` - `rfc2425parameters`: `dict`""" VCardField.__init__(self,name) if not rfc2425parameters: rfc2425parameters={} if isinstance(value,libxml2.xmlNode): self.type,self.cred=None,None n=value.children vns=get_node_ns(value) while n: if n.type!='element': n=n.next continue ns=get_node_ns(n) if (ns and vns and ns.getContent()!=vns.getContent()): n=n.next continue if n.name=='TYPE': self.type=unicode(n.getContent(),"utf-8","replace") if n.name=='CRED': self.cred=base64.decodestring(n.getContent()) n=n.next if not self.cred: raise Empty,"Bad %s value in vcard" % (name,) else: self.type=rfc2425parameters.get("type") self.cred=value def rfc2426(self): """RFC2426-encode the field content. :return: the field in the RFC 2426 format. :returntype: `str`""" if self.type: p={"type":self.type} else: p={} return rfc2425encode(self.name,self.cred,p) def as_xml(self,parent): """Create vcard-tmp XML representation of the field. :Parameters: - `parent`: parent node for the element :Types: - `parent`: `libxml2.xmlNode` :return: xml node with the field data. :returntype: `libxml2.xmlNode`""" n=parent.newChild(None,self.name.upper(),None) if self.type: n.newTextChild(None,"TYPE",self.type) n.newTextChild(None,"CRED",binascii.b2a_base64(self.cred)) return n class VCard(StanzaPayloadObject): """Jabber (vcard-temp) or RFC2426 vCard. :Ivariables: - `fn`: full name. - `n`: structural name. - `nickname`: nickname(s). - `photo`: photo(s). - `bday`: birthday date(s). - `adr`: address(es). - `label`: address label(s). - `tel`: phone number(s). - `email`: e-mail address(es). - `jabberid`: JID(s). - `mailer`: mailer(s). - `tz`: timezone(s). - `geo`: geolocation(s). - `title`: title(s). - `role`: role(s). - `logo`: logo(s). - `org`: organization(s). - `categories`: categories. - `note`: note(s). - `prodid`: product id(s). - `rev`: revision(s). - `sort-string`: sort string(s). - `sound`: sound(s). - `uid`: user identifier(s). - `url`: URL(s). - `class`: class(es). - `key`: key(s). - `desc`: description. :Types: - `fn`: `VCardString`, - `n`: `VCardName`, - `nickname`: `list` of `VCardString` - `photo`: `list` of `VCardImage` - `bday`: `list` of `VCardString` - `adr`: `list` of `VCardAdr` - `label`: `list` of `VCardLabel` - `tel`: `list` of `VCardTel` - `email`: `list` of `VCardEmail` - `jabberid`: `list` of `VCardJID` - `mailer`: `list` of `VCardString` - `tz`: `list` of `VCardString` - `geo`: `list` of `VCardGeo` - `title`: `list` of `VCardString` - `role`: `list` of `VCardString` - `logo`: `list` of `VCardImage` - `org`: `list` of `VCardOrg` - `categories`: `list` of `VCardCategories` - `note`: `list` of `VCardString` - `prodid`: `list` of `VCardString` - `rev`: `list` of `VCardString` - `sort-string`: `list` of `VCardString` - `sound`: `list` of `VCardSound` - `uid`: `list` of `VCardString` - `url`: `list` of `VCardString` - `class`: `list` of `VCardString` - `key`: `list` of `VCardKey` - `desc`: `list` of `VCardXString` """ xml_element_name = "vCard" xml_element_namespace = VCARD_NS components={ #"VERSION": (VCardString,"optional"), "FN": (VCardString,"required"), "N": (VCardName,"required"), "NICKNAME": (VCardString,"multi"), "PHOTO": (VCardImage,"multi"), "BDAY": (VCardString,"multi"), "ADR": (VCardAdr,"multi"), "LABEL": (VCardLabel,"multi"), "TEL": (VCardTel,"multi"), "EMAIL": (VCardEmail,"multi"), "JABBERID": (VCardJID,"multi"), "MAILER": (VCardString,"multi"), "TZ": (VCardString,"multi"), "GEO": (VCardGeo,"multi"), "TITLE": (VCardString,"multi"), "ROLE": (VCardString,"multi"), "LOGO": (VCardImage,"multi"), "AGENT": ("VCardAgent","ignore"), #FIXME: agent field "ORG": (VCardOrg,"multi"), "CATEGORIES": (VCardCategories,"multi"), "NOTE": (VCardString,"multi"), "PRODID": (VCardString,"multi"), "REV": (VCardString,"multi"), "SORT-STRING": (VCardString,"multi"), "SOUND": (VCardSound,"multi"), "UID": (VCardString,"multi"), "URL": (VCardString,"multi"), "CLASS": (VCardString,"multi"), "KEY": (VCardKey,"multi"), "DESC": (VCardXString,"multi"), }; def __init__(self,data): """Initialize a VCard object from data which may be XML node or an RFC2426 string. :Parameters: - `data`: vcard to parse. :Types: - `data`: `libxml2.xmlNode`, `unicode` or `str`""" # to make pylint happy self.n = None del self.n self.content={} if isinstance(data,libxml2.xmlNode): self.__from_xml(data) else: self.__from_rfc2426(data) if not self.content.get("N") and self.content.get("FN"): s=self.content['FN'].value.replace(";",",") s=s.split(None,2) if len(s)==2: s=u"%s;%s;;;" % (s[1],s[0]) elif len(s)==3: s=u"%s;%s;%s" % (s[2],s[0],s[1]) else: s=u"%s;;;;" % (s[0],) self.content["N"]=VCardName("N",s) elif not self.content.get("FN") and self.content.get("N"): self.__make_fn() for c, (_unused, tp) in self.components.items(): if self.content.has_key(c): continue if tp=="required": raise ValueError,"%s is missing" % (c,) elif tp=="multi": self.content[c]=[] elif tp=="optional": self.content[c]=None else: continue def __make_fn(self): """Initialize the mandatory `self.fn` from `self.n`. This is a workaround for buggy clients which set only one of them.""" s=[] if self.n.prefix: s.append(self.n.prefix) if self.n.given: s.append(self.n.given) if self.n.middle: s.append(self.n.middle) if self.n.family: s.append(self.n.family) if self.n.suffix: s.append(self.n.suffix) s=u" ".join(s) self.content["FN"]=VCardString("FN", s, empty_ok = True) def __from_xml(self,data): """Initialize a VCard object from XML node. :Parameters: - `data`: vcard to parse. :Types: - `data`: `libxml2.xmlNode`""" ns=get_node_ns(data) if ns and ns.getContent()!=VCARD_NS: raise ValueError, "Not in the %r namespace" % (VCARD_NS,) if data.name!="vCard": raise ValueError, "Bad root element name: %r" % (data.name,) n=data.children dns=get_node_ns(data) while n: if n.type!='element': n=n.next continue ns=get_node_ns(n) if (ns and dns and ns.getContent()!=dns.getContent()): n=n.next continue if not self.components.has_key(n.name): n=n.next continue cl,tp=self.components[n.name] if tp in ("required","optional"): if self.content.has_key(n.name): raise ValueError,"Duplicate %s" % (n.name,) try: self.content[n.name]=cl(n.name,n) except Empty: pass elif tp=="multi": if not self.content.has_key(n.name): self.content[n.name]=[] try: self.content[n.name].append(cl(n.name,n)) except Empty: pass n=n.next def __from_rfc2426(self,data): """Initialize a VCard object from an RFC2426 string. :Parameters: - `data`: vcard to parse. :Types: - `data`: `libxml2.xmlNode`, `unicode` or `str`""" data=from_utf8(data) lines=data.split("\n") started=0 current=None for l in lines: if not l: continue if l[-1]=="\r": l=l[:-1] if not l: continue if l[0] in " \t": if current is None: continue current+=l[1:] continue if not started and current and current.upper().strip()=="BEGIN:VCARD": started=1 elif started and current.upper().strip()=="END:VCARD": current=None break elif current and started: self._process_rfc2425_record(current) current=l if started and current: self._process_rfc2425_record(current) def _process_rfc2425_record(self,data): """Parse single RFC2425 record and update attributes of `self`. :Parameters: - `data`: the record (probably multiline) :Types: - `data`: `unicode`""" label,value=data.split(":",1) value=value.replace("\\n","\n").replace("\\N","\n") psplit=label.lower().split(";") name=psplit[0] params=psplit[1:] if u"." in name: name=name.split(".",1)[1] name=name.upper() if name in (u"X-DESC",u"X-JABBERID"): name=name[2:] if not self.components.has_key(name): return if params: params=dict([p.split("=",1) for p in params]) cl,tp=self.components[name] if tp in ("required","optional"): if self.content.has_key(name): raise ValueError,"Duplicate %s" % (name,) try: self.content[name]=cl(name,value,params) except Empty: pass elif tp=="multi": if not self.content.has_key(name): self.content[name]=[] try: self.content[name].append(cl(name,value,params)) except Empty: pass else: return def __repr__(self): return "" % (self.content["FN"].value,) def rfc2426(self): """Get the RFC2426 representation of `self`. :return: the UTF-8 encoded RFC2426 representation. :returntype: `str`""" ret="begin:VCARD\r\n" ret+="version:3.0\r\n" for _unused, value in self.content.items(): if value is None: continue if type(value) is list: for v in value: ret+=v.rfc2426() else: v=value.rfc2426() ret+=v return ret+"end:VCARD\r\n" def complete_xml_element(self, xmlnode, _unused): """Complete the XML node with `self` content. Should be overriden in classes derived from `StanzaPayloadObject`. :Parameters: - `xmlnode`: XML node with the element being built. It has already right name and namespace, but no attributes or content. - `_unused`: document to which the element belongs. :Types: - `xmlnode`: `libxml2.xmlNode` - `_unused`: `libxml2.xmlDoc`""" for _unused1, value in self.content.items(): if value is None: continue if type(value) is list: for v in value: v.as_xml(xmlnode) else: value.as_xml(xmlnode) def __getattr__(self,name): try: return self.content[name.upper().replace("_","-")] except KeyError: raise AttributeError,"Attribute %r not found" % (name,) def __getitem__(self,name): return self.content[name.upper()] # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/jabber/client.py0000644000175000017500000002632211560025462020132 0ustar jajcususers00000000000000# (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Basic Jabber client functionality implementation. Extends `pyxmpp.client` interface with legacy authentication and basic Service Discovery handling. Normative reference: - `JEP 78 `__ - `JEP 30 `__ """ __docformat__="restructuredtext en" import logging from pyxmpp.jabber.clientstream import LegacyClientStream from pyxmpp.jabber.disco import DISCO_ITEMS_NS,DISCO_INFO_NS from pyxmpp.jabber.disco import DiscoInfo,DiscoItems,DiscoIdentity from pyxmpp.jabber import disco from pyxmpp.client import Client from pyxmpp.stanza import Stanza from pyxmpp.cache import CacheSuite from pyxmpp.utils import from_utf8 from pyxmpp.interfaces import IFeaturesProvider class JabberClient(Client): """Base class for a Jabber client. :Ivariables: - `disco_items`: default Disco#items reply for a query to an empty node. - `disco_info`: default Disco#info reply for a query to an empty node -- provides information about the client and its supported fetures. - `disco_identity`: default identity of the default `disco_info`. - `register`: when `True` than registration will be started instead of authentication. :Types: - `disco_items`: `DiscoItems` - `disco_info`: `DiscoInfo` - `register`: `bool` """ def __init__(self,jid=None, password=None, server=None, port=5222, auth_methods=("sasl:DIGEST-MD5","digest"), tls_settings=None, keepalive=0, disco_name=u"pyxmpp based Jabber client", disco_category=u"client", disco_type=u"pc"): """Initialize a JabberClient object. :Parameters: - `jid`: user full JID for the connection. - `password`: user password. - `server`: server to use. If not given then address will be derived form the JID. - `port`: port number to use. If not given then address will be derived form the JID. - `auth_methods`: sallowed authentication methods. SASL authentication mechanisms in the list should be prefixed with "sasl:" string. - `tls_settings`: settings for StartTLS -- `TLSSettings` instance. - `keepalive`: keepalive output interval. 0 to disable. - `disco_name`: name of the client identity in the disco#info replies. - `disco_category`: category of the client identity in the disco#info replies. The default of u'client' should be the right choice in most cases. - `disco_type`: type of the client identity in the disco#info replies. Use `the types registered by Jabber Registrar `__ :Types: - `jid`: `pyxmpp.JID` - `password`: `unicode` - `server`: `unicode` - `port`: `int` - `auth_methods`: sequence of `str` - `tls_settings`: `pyxmpp.TLSSettings` - `keepalive`: `int` - `disco_name`: `unicode` - `disco_category`: `unicode` - `disco_type`: `unicode` """ Client.__init__(self,jid,password,server,port,auth_methods,tls_settings,keepalive) self.stream_class = LegacyClientStream self.disco_items=DiscoItems() self.disco_info=DiscoInfo() self.disco_identity=DiscoIdentity(self.disco_info, disco_name, disco_category, disco_type) self.register_feature(u"dnssrv") self.register_feature(u"stringprep") self.register_feature(u"urn:ietf:params:xml:ns:xmpp-sasl#c2s") self.cache = CacheSuite(max_items = 1000) self.__logger = logging.getLogger("pyxmpp.jabber.JabberClient") # public methods def connect(self, register = False): """Connect to the server and set up the stream. Set `self.stream` and notify `self.state_changed` when connection succeeds. Additionally, initialize Disco items and info of the client. """ Client.connect(self, register) if register: self.stream.registration_callback = self.process_registration_form def register_feature(self, feature_name): """Register a feature to be announced by Service Discovery. :Parameters: - `feature_name`: feature namespace or name. :Types: - `feature_name`: `unicode`""" self.disco_info.add_feature(feature_name) def unregister_feature(self, feature_name): """Unregister a feature to be announced by Service Discovery. :Parameters: - `feature_name`: feature namespace or name. :Types: - `feature_name`: `unicode`""" self.disco_info.remove_feature(feature_name) def submit_registration_form(self, form): """Submit a registration form :Parameters: - `form`: the form to submit :Types: - `form`: `pyxmpp.jabber.dataforms.Form`""" self.stream.submit_registration_form(form) # private methods def __disco_info(self,iq): """Handle a disco#info request. `self.disco_get_info` method will be used to prepare the query response. :Parameters: - `iq`: the IQ stanza received. :Types: - `iq`: `pyxmpp.iq.Iq`""" q=iq.get_query() if q.hasProp("node"): node=from_utf8(q.prop("node")) else: node=None info=self.disco_get_info(node,iq) if isinstance(info,DiscoInfo): resp=iq.make_result_response() self.__logger.debug("Disco-info query: %s preparing response: %s with reply: %s" % (iq.serialize(),resp.serialize(),info.xmlnode.serialize())) resp.set_content(info.xmlnode.copyNode(1)) elif isinstance(info,Stanza): resp=info else: resp=iq.make_error_response("item-not-found") self.__logger.debug("Disco-info response: %s" % (resp.serialize(),)) self.stream.send(resp) def __disco_items(self,iq): """Handle a disco#items request. `self.disco_get_items` method will be used to prepare the query response. :Parameters: - `iq`: the IQ stanza received. :Types: - `iq`: `pyxmpp.iq.Iq`""" q=iq.get_query() if q.hasProp("node"): node=from_utf8(q.prop("node")) else: node=None items=self.disco_get_items(node,iq) if isinstance(items,DiscoItems): resp=iq.make_result_response() self.__logger.debug("Disco-items query: %s preparing response: %s with reply: %s" % (iq.serialize(),resp.serialize(),items.xmlnode.serialize())) resp.set_content(items.xmlnode.copyNode(1)) elif isinstance(items,Stanza): resp=items else: resp=iq.make_error_response("item-not-found") self.__logger.debug("Disco-items response: %s" % (resp.serialize(),)) self.stream.send(resp) def _session_started(self): """Called when session is started. Activates objects from `self.interface_provides` by installing their disco features.""" Client._session_started(self) for ob in self.interface_providers: if IFeaturesProvider.providedBy(ob): for ns in ob.get_features(): self.register_feature(ns) # methods to override def authorized(self): """Handle "authorized" event. May be overriden in derived classes. By default: request an IM session and setup Disco handlers.""" Client.authorized(self) self.stream.set_iq_get_handler("query",DISCO_ITEMS_NS,self.__disco_items) self.stream.set_iq_get_handler("query",DISCO_INFO_NS,self.__disco_info) disco.register_disco_cache_fetchers(self.cache,self.stream) def disco_get_info(self,node,iq): """Return Disco#info data for a node. :Parameters: - `node`: the node queried. - `iq`: the request stanza received. :Types: - `node`: `unicode` - `iq`: `pyxmpp.iq.Iq` :return: self.disco_info if `node` is empty or `None` otherwise. :returntype: `DiscoInfo`""" to=iq.get_to() if to and to!=self.jid: return iq.make_error_response("recipient-unavailable") if not node and self.disco_info: return self.disco_info return None def disco_get_items(self,node,iq): """Return Disco#items data for a node. :Parameters: - `node`: the node queried. - `iq`: the request stanza received. :Types: - `node`: `unicode` - `iq`: `pyxmpp.iq.Iq` :return: self.disco_info if `node` is empty or `None` otherwise. :returntype: `DiscoInfo`""" to=iq.get_to() if to and to!=self.jid: return iq.make_error_response("recipient-unavailable") if not node and self.disco_items: return self.disco_items return None def process_registration_form(self, stanza, form): """Fill-in the registration form provided by the server. This default implementation fills-in "username" and "passwords" fields only and instantly submits the form. :Parameters: - `stanza`: the stanza received. - `form`: the registration form. :Types: - `stanza`: `pyxmpp.iq.Iq` - `form`: `pyxmpp.jabber.dataforms.Form` """ _unused = stanza self.__logger.debug(u"default registration callback started. auto-filling-in the form...") if not 'FORM_TYPE' in form or 'jabber:iq:register' not in form['FORM_TYPE'].values: raise RuntimeError, "Unknown form type: %r %r" % (form, form['FORM_TYPE']) for field in form: if field.name == u"username": self.__logger.debug(u"Setting username to %r" % (self.jid.node,)) field.value = self.jid.node elif field.name == u"password": self.__logger.debug(u"Setting password to %r" % (self.password,)) field.value = self.password elif field.required: self.__logger.debug(u"Unknown required field: %r" % (field.name,)) raise RuntimeError, "Unsupported required registration form field %r" % (field.name,) else: self.__logger.debug(u"Unknown field: %r" % (field.name,)) self.submit_registration_form(form) # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/jabber/delay.py0000644000175000017500000001502411560025462017747 0ustar jajcususers00000000000000# # (C) Copyright 2003-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Delayed delivery mark (jabber:x:delay) handling. Normative reference: - `JEP 91 `__ """ __docformat__="restructuredtext en" import libxml2 import time import datetime from pyxmpp.jid import JID from pyxmpp.utils import to_utf8,from_utf8 from pyxmpp.xmlextra import get_node_ns_uri from pyxmpp.utils import datetime_utc_to_local,datetime_local_to_utc from pyxmpp.objects import StanzaPayloadObject from pyxmpp.exceptions import BadRequestProtocolError, JIDMalformedProtocolError, JIDError DELAY_NS="jabber:x:delay" class Delay(StanzaPayloadObject): """ Delayed delivery tag. Represents 'jabber:x:delay' (JEP-0091) element of a Jabber stanza. :Ivariables: - `delay_from`: the "from" value of the delay element - `reason`: the "reason" (content) of the delay element - `timestamp`: the UTC timestamp as naive datetime object """ xml_element_name = "x" xml_element_namespace = DELAY_NS def __init__(self,node_or_datetime,delay_from=None,reason=None,utc=True): """ Initialize the Delay object. :Parameters: - `node_or_datetime`: an XML node to parse or the timestamp. - `delay_from`: JID of the entity which adds the delay mark (when `node_or_datetime` is a timestamp). - `reason`: reason of the delay (when `node_or_datetime` is a timestamp). - `utc`: if `True` then the timestamp is assumed to be UTC, otherwise it is assumed to be local time. :Types: - `node_or_datetime`: `libxml2.xmlNode` or `datetime.datetime` - `delay_from`: `pyxmpp.JID` - `reason`: `unicode` - `utc`: `bool`""" if isinstance(node_or_datetime,libxml2.xmlNode): self.from_xml(node_or_datetime) else: if utc: self.timestamp=node_or_datetime else: self.timestamp=datetime_local_to_utc(node_or_datetime) self.delay_from=JID(delay_from) self.reason=unicode(reason) def from_xml(self,xmlnode): """Initialize Delay object from an XML node. :Parameters: - `xmlnode`: the jabber:x:delay XML element. :Types: - `xmlnode`: `libxml2.xmlNode`""" if xmlnode.type!="element": raise ValueError,"XML node is not a jabber:x:delay element (not an element)" ns=get_node_ns_uri(xmlnode) if ns and ns!=DELAY_NS or xmlnode.name!="x": raise ValueError,"XML node is not a jabber:x:delay element" stamp=xmlnode.prop("stamp") if stamp.endswith("Z"): stamp=stamp[:-1] if "-" in stamp: stamp=stamp.split("-",1)[0] try: tm = time.strptime(stamp, "%Y%m%dT%H:%M:%S") except ValueError: raise BadRequestProtocolError, "Bad timestamp" tm=tm[0:8]+(0,) self.timestamp=datetime.datetime.fromtimestamp(time.mktime(tm)) delay_from=from_utf8(xmlnode.prop("from")) if delay_from: try: self.delay_from = JID(delay_from) except JIDError: raise JIDMalformedProtocolError, "Bad JID in the jabber:x:delay 'from' attribute" else: self.delay_from = None self.reason = from_utf8(xmlnode.getContent()) def complete_xml_element(self, xmlnode, _unused): """Complete the XML node with `self` content. Should be overriden in classes derived from `StanzaPayloadObject`. :Parameters: - `xmlnode`: XML node with the element being built. It has already right name and namespace, but no attributes or content. - `_unused`: document to which the element belongs. :Types: - `xmlnode`: `libxml2.xmlNode` - `_unused`: `libxml2.xmlDoc`""" tm=self.timestamp.strftime("%Y%m%dT%H:%M:%S") xmlnode.setProp("stamp",tm) if self.delay_from: xmlnode.setProp("from",self.delay_from.as_utf8()) if self.reason: xmlnode.setContent(to_utf8(self.reason)) def get_datetime_local(self): """Get the timestamp as a local time. :return: the timestamp of the delay element represented in the local timezone. :returntype: `datetime.datetime`""" r=datetime_utc_to_local(self.timestamp) return r def get_datetime_utc(self): """Get the timestamp as a UTC. :return: the timestamp of the delay element represented in UTC. :returntype: `datetime.datetime`""" return self.timestamp def __str__(self): n=self.as_xml() r=n.serialize() n.freeNode() return r def __cmp__(self,other): return cmp(timestamp, other.timestamp) def get_delays(stanza): """Get jabber:x:delay elements from the stanza. :Parameters: - `stanza`: a, probably delayed, stanza. :Types: - `stanza`: `pyxmpp.stanza.Stanza` :return: list of delay tags sorted by the timestamp. :returntype: `list` of `Delay`""" delays=[] n=stanza.xmlnode.children while n: if n.type=="element" and get_node_ns_uri(n)==DELAY_NS and n.name=="x": delays.append(Delay(n)) n=n.next delays.sort() return delays def get_delay(stanza): """Get the oldest jabber:x:delay elements from the stanza. :Parameters: - `stanza`: a, probably delayed, stanza. :Types: - `stanza`: `pyxmpp.stanza.Stanza` The return value, if not `None`, contains a quite reliable timestamp of a delayed (e.g. from offline storage) message. :return: the oldest delay tag of the stanza or `None`. :returntype: `Delay`""" delays=get_delays(stanza) if not delays: return None return get_delays(stanza)[0] # vi: sts=4 et sw=4 pyxmpp-1.1.2/pyxmpp/jabber/dataforms.py0000644000175000017500000006237611560025462020645 0ustar jajcususers00000000000000# # (C) Copyright 2005-2010 Jacek Konieczny # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License Version # 2.1 as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """Jabber Data Forms support. Normative reference: - `JEP 4 `__ """ __docformat__="restructuredtext en" import copy import libxml2 import warnings from pyxmpp.objects import StanzaPayloadObject from pyxmpp.utils import from_utf8, to_utf8 from pyxmpp.xmlextra import xml_element_ns_iter from pyxmpp.jid import JID from pyxmpp.exceptions import BadRequestProtocolError DATAFORM_NS = "jabber:x:data" class Option(StanzaPayloadObject): """One of optional data form field values. :Ivariables: - `label`: option label. - `value`: option value. :Types: - `label`: `unicode` - `value`: `unicode` """ xml_element_name = "option" xml_element_namespace = DATAFORM_NS def __init__(self, value = None, label = None, values = None): """Initialize an `Option` object. :Parameters: - `value`: option value (mandatory). - `label`: option label (human-readable description). - `values`: for backward compatibility only. :Types: - `label`: `unicode` - `value`: `unicode` """ self.label = label if value: self.value = value elif values: warnings.warn("Option constructor accepts only single value now.", DeprecationWarning, stacklevel=1) self.value = values[0] else: raise TypeError, "value argument to pyxmpp.dataforms.Option is required" @property def values(self): """Return list of option values (always single element). Obsolete. For backwards compatibility only.""" return [self.value] def complete_xml_element(self, xmlnode, doc): """Complete the XML node with `self` content. :Parameters: - `xmlnode`: XML node with the element being built. It has already right name and namespace, but no attributes or content. - `doc`: document to which the element belongs. :Types: - `xmlnode`: `libxml2.xmlNode` - `doc`: `libxml2.xmlDoc`""" _unused = doc if self.label is not None: xmlnode.setProp("label", self.label.encode("utf-8")) xmlnode.newTextChild(xmlnode.ns(), "value", self.value.encode("utf-8")) return xmlnode @classmethod def _new_from_xml(cls, xmlnode): """Create a new `Option` object from an XML element. :Parameters: - `xmlnode`: the XML element. :Types: - `xmlnode`: `libxml2.xmlNode` :return: the object created. :returntype: `Option` """ label = from_utf8(xmlnode.prop("label")) child = xmlnode.children value = None for child in xml_element_ns_iter(xmlnode.children, DATAFORM_NS): if child.name == "value": value = from_utf8(child.getContent()) break if value is None: raise BadRequestProtocolError, "No value in