Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mrDoctorWho/xmpppy.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/xmpp
diff options
context:
space:
mode:
authoralkorgun <alkorgun@gmail.com>2013-12-07 00:05:22 +0400
committeralkorgun <alkorgun@gmail.com>2013-12-07 00:05:22 +0400
commit85e01717e7c9866bbe574fbf5ad89689b6b2fe93 (patch)
treebcfd33447a46721ff4683c59924192352f05890c /xmpp
parent20eea0ce73e9e5a29a2fb5f6b0e5f3535117cbec (diff)
initial commit
Diffstat (limited to 'xmpp')
-rw-r--r--xmpp/LICENSE675
-rw-r--r--xmpp/__init__.py42
-rw-r--r--xmpp/auth.py413
-rw-r--r--xmpp/browser.py261
-rw-r--r--xmpp/client.py374
-rw-r--r--xmpp/commands.py448
-rw-r--r--xmpp/debug.py314
-rw-r--r--xmpp/dispatcher.py477
-rw-r--r--xmpp/features.py230
-rw-r--r--xmpp/filetransfer.py226
-rw-r--r--xmpp/plugin.py69
-rw-r--r--xmpp/protocol.py1404
-rw-r--r--xmpp/roster.py280
-rw-r--r--xmpp/simplexml.py704
-rw-r--r--xmpp/transports.py403
15 files changed, 6320 insertions, 0 deletions
diff --git a/xmpp/LICENSE b/xmpp/LICENSE
new file mode 100644
index 0000000..ecbfcd1
--- /dev/null
+++ b/xmpp/LICENSE
@@ -0,0 +1,675 @@
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+ software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+ to take away your freedom to share and change the works. By contrast,
+ the GNU General Public License is intended to guarantee your freedom to
+ share and change all versions of a program--to make sure it remains free
+ software for all its users. We, the Free Software Foundation, use the
+ GNU General Public License for most of our software; it applies also to
+ any other work released this way by its authors. You can apply it to
+ your programs, too.
+
+ When we speak of free software, we are referring to freedom, 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
+ them if you wish), that you receive source code or can get it if you
+ want it, that you can change the software or use pieces of it in new
+ free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+ these rights or asking you to surrender the rights. Therefore, you have
+ certain responsibilities if you distribute copies of the software, or if
+ you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+ gratis or for a fee, you must pass on to the recipients the same
+ freedoms that you received. You must make sure that they, too, receive
+ or can get the source code. And you must show them these terms so they
+ know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+ (1) assert copyright on the software, and (2) offer you this License
+ giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+ that there is no warranty for this free software. For both users' and
+ authors' sake, the GPL requires that modified versions be marked as
+ changed, so that their problems will not be attributed erroneously to
+ authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+ modified versions of the software inside them, although the manufacturer
+ can do so. This is fundamentally incompatible with the aim of
+ protecting users' freedom to change the software. The systematic
+ pattern of such abuse occurs in the area of products for individuals to
+ use, which is precisely where it is most unacceptable. Therefore, we
+ have designed this version of the GPL to prohibit the practice for those
+ products. If such problems arise substantially in other domains, we
+ stand ready to extend this provision to those domains in future versions
+ of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+ States should not allow patents to restrict development and use of
+ software on general-purpose computers, but in those that do, we wish to
+ avoid the special danger that patents applied to a free program could
+ make it effectively proprietary. To prevent this, the GPL assures that
+ patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+ modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+ works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+ License. Each licensee is addressed as "you". "Licensees" and
+ "recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+ in a fashion requiring copyright permission, other than the making of an
+ exact copy. The resulting work is called a "modified version" of the
+ earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+ on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+ permission, would make you directly or secondarily liable for
+ infringement under applicable copyright law, except executing it on a
+ computer or modifying a private copy. Propagation includes copying,
+ distribution (with or without modification), making available to the
+ public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+ parties to make or receive copies. Mere interaction with a user through
+ a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+ to the extent that it includes a convenient and prominently visible
+ feature that (1) displays an appropriate copyright notice, and (2)
+ tells the user that there is no warranty for the work (except to the
+ extent that warranties are provided), that licensees may convey the
+ work under this License, and how to view a copy of this License. If
+ the interface presents a list of user commands or options, such as a
+ menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+ for making modifications to it. "Object code" means any non-source
+ form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+ standard defined by a recognized standards body, or, in the case of
+ interfaces specified for a particular programming language, one that
+ is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+ than the work as a whole, that (a) is included in the normal form of
+ packaging a Major Component, but which is not part of that Major
+ Component, and (b) serves only to enable use of the work with that
+ Major Component, or to implement a Standard Interface for which an
+ implementation is available to the public in source code form. A
+ "Major Component", in this context, means a major essential component
+ (kernel, window system, and so on) of the specific operating system
+ (if any) on which the executable work runs, or a compiler used to
+ produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+ the source code needed to generate, install, and (for an executable
+ work) run the object code and to modify the work, including scripts to
+ control those activities. However, it does not include the work's
+ System Libraries, or general-purpose tools or generally available free
+ programs which are used unmodified in performing those activities but
+ which are not part of the work. For example, Corresponding Source
+ includes interface definition files associated with source files for
+ the work, and the source code for shared libraries and dynamically
+ linked subprograms that the work is specifically designed to require,
+ such as by intimate data communication or control flow between those
+ subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+ can regenerate automatically from other parts of the Corresponding
+ Source.
+
+ The Corresponding Source for a work in source code form is that
+ same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+ copyright on the Program, and are irrevocable provided the stated
+ conditions are met. This License explicitly affirms your unlimited
+ permission to run the unmodified Program. The output from running a
+ covered work is covered by this License only if the output, given its
+ content, constitutes a covered work. This License acknowledges your
+ rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+ convey, without conditions so long as your license otherwise remains
+ in force. You may convey covered works to others for the sole purpose
+ of having them make modifications exclusively for you, or provide you
+ with facilities for running those works, provided that you comply with
+ the terms of this License in conveying all material for which you do
+ not control copyright. Those thus making or running the covered works
+ for you must do so exclusively on your behalf, under your direction
+ and control, on terms that prohibit them from making any copies of
+ your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+ the conditions stated below. Sublicensing is not allowed; section 10
+ makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+ measure under any applicable law fulfilling obligations under article
+ 11 of the WIPO copyright treaty adopted on 20 December 1996, or
+ similar laws prohibiting or restricting circumvention of such
+ measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+ circumvention of technological measures to the extent such circumvention
+ is effected by exercising rights under this License with respect to
+ the covered work, and you disclaim any intention to limit operation or
+ modification of the work as a means of enforcing, against the work's
+ users, your or third parties' legal rights to forbid circumvention of
+ technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+ receive it, in any medium, provided that you conspicuously and
+ appropriately publish on each copy an appropriate copyright notice;
+ keep intact all notices stating that this License and any
+ non-permissive terms added in accord with section 7 apply to the code;
+ keep intact all notices of the absence of any warranty; and give all
+ recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+ and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+ produce it from the Program, in the form of source code under the
+ terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+ works, which are not by their nature extensions of the covered work,
+ and which are not combined with it such as to form a larger program,
+ in or on a volume of a storage or distribution medium, is called an
+ "aggregate" if the compilation and its resulting copyright are not
+ used to limit the access or legal rights of the compilation's users
+ beyond what the individual works permit. Inclusion of a covered work
+ in an aggregate does not cause this License to apply to the other
+ parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+ of sections 4 and 5, provided that you also convey the
+ machine-readable Corresponding Source under the terms of this License,
+ in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+ from the Corresponding Source as a System Library, need not be
+ included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+ tangible personal property which is normally used for personal, family,
+ or household purposes, or (2) anything designed or sold for incorporation
+ into a dwelling. In determining whether a product is a consumer product,
+ doubtful cases shall be resolved in favor of coverage. For a particular
+ product received by a particular user, "normally used" refers to a
+ typical or common use of that class of product, regardless of the status
+ of the particular user or of the way in which the particular user
+ actually uses, or expects or is expected to use, the product. A product
+ is a consumer product regardless of whether the product has substantial
+ commercial, industrial or non-consumer uses, unless such uses represent
+ the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+ procedures, authorization keys, or other information required to install
+ and execute modified versions of a covered work in that User Product from
+ a modified version of its Corresponding Source. The information must
+ suffice to ensure that the continued functioning of the modified object
+ code is in no case prevented or interfered with solely because
+ modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+ specifically for use in, a User Product, and the conveying occurs as
+ part of a transaction in which the right of possession and use of the
+ User Product is transferred to the recipient in perpetuity or for a
+ fixed term (regardless of how the transaction is characterized), the
+ Corresponding Source conveyed under this section must be accompanied
+ by the Installation Information. But this requirement does not apply
+ if neither you nor any third party retains the ability to install
+ modified object code on the User Product (for example, the work has
+ been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+ requirement to continue to provide support service, warranty, or updates
+ for a work that has been modified or installed by the recipient, or for
+ the User Product in which it has been modified or installed. Access to a
+ network may be denied when the modification itself materially and
+ adversely affects the operation of the network or violates the rules and
+ protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+ in accord with this section must be in a format that is publicly
+ documented (and with an implementation available to the public in
+ source code form), and must require no special password or key for
+ unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+ License by making exceptions from one or more of its conditions.
+ Additional permissions that are applicable to the entire Program shall
+ be treated as though they were included in this License, to the extent
+ that they are valid under applicable law. If additional permissions
+ apply only to part of the Program, that part may be used separately
+ under those permissions, but the entire Program remains governed by
+ this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+ remove any additional permissions from that copy, or from any part of
+ it. (Additional permissions may be written to require their own
+ removal in certain cases when you modify the work.) You may place
+ additional permissions on material, added by you to a covered work,
+ for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+ add to a covered work, you may (if authorized by the copyright holders of
+ that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+ restrictions" within the meaning of section 10. If the Program as you
+ received it, or any part of it, contains a notice stating that it is
+ governed by this License along with a term that is a further
+ restriction, you may remove that term. If a license document contains
+ a further restriction but permits relicensing or conveying under this
+ License, you may add to a covered work material governed by the terms
+ of that license document, provided that the further restriction does
+ not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+ must place, in the relevant source files, a statement of the
+ additional terms that apply to those files, or a notice indicating
+ where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+ form of a separately written license, or stated as exceptions;
+ the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+ provided under this License. Any attempt otherwise to propagate or
+ modify it is void, and will automatically terminate your rights under
+ this License (including any patent licenses granted under the third
+ paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+ license from a particular copyright holder is reinstated (a)
+ provisionally, unless and until the copyright holder explicitly and
+ finally terminates your license, and (b) permanently, if the copyright
+ holder fails to notify you of the violation by some reasonable means
+ prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+ reinstated permanently if the copyright holder notifies you of the
+ violation by some reasonable means, this is the first time you have
+ received notice of violation of this License (for any work) from that
+ copyright holder, and you cure the violation prior to 30 days after
+ your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+ licenses of parties who have received copies or rights from you under
+ this License. If your rights have been terminated and not permanently
+ reinstated, you do not qualify to receive new licenses for the same
+ material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+ run a copy of the Program. Ancillary propagation of a covered work
+ occurring solely as a consequence of using peer-to-peer transmission
+ to receive a copy likewise does not require acceptance. However,
+ nothing other than this License grants you permission to propagate or
+ modify any covered work. These actions infringe copyright if you do
+ not accept this License. Therefore, by modifying or propagating a
+ covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+ receives a license from the original licensors, to run, modify and
+ propagate that work, subject to this License. You are not responsible
+ for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+ organization, or substantially all assets of one, or subdividing an
+ organization, or merging organizations. If propagation of a covered
+ work results from an entity transaction, each party to that
+ transaction who receives a copy of the work also receives whatever
+ licenses to the work the party's predecessor in interest had or could
+ give under the previous paragraph, plus a right to possession of the
+ Corresponding Source of the work from the predecessor in interest, if
+ the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+ rights granted or affirmed under this License. For example, you may
+ not impose a license fee, royalty, or other charge for exercise of
+ rights granted under this License, and you may not initiate litigation
+ (including a cross-claim or counterclaim in a lawsuit) alleging that
+ any patent claim is infringed by making, using, selling, offering for
+ sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+ License of the Program or a work on which the Program is based. The
+ work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+ owned or controlled by the contributor, whether already acquired or
+ hereafter acquired, that would be infringed by some manner, permitted
+ by this License, of making, using, or selling its contributor version,
+ but do not include claims that would be infringed only as a
+ consequence of further modification of the contributor version. For
+ purposes of this definition, "control" includes the right to grant
+ patent sublicenses in a manner consistent with the requirements of
+ this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+ patent license under the contributor's essential patent claims, to
+ make, use, sell, offer for sale, import and otherwise run, modify and
+ propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+ agreement or commitment, however denominated, not to enforce a patent
+ (such as an express permission to practice a patent or covenant not to
+ sue for patent infringement). To "grant" such a patent license to a
+ party means to make such an agreement or commitment not to enforce a
+ patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+ and the Corresponding Source of the work is not available for anyone
+ to copy, free of charge and under the terms of this License, through a
+ publicly available network server or other readily accessible means,
+ then you must either (1) cause the Corresponding Source to be so
+ available, or (2) arrange to deprive yourself of the benefit of the
+ patent license for this particular work, or (3) arrange, in a manner
+ consistent with the requirements of this License, to extend the patent
+ license to downstream recipients. "Knowingly relying" means you have
+ actual knowledge that, but for the patent license, your conveying the
+ covered work in a country, or your recipient's use of the covered work
+ in a country, would infringe one or more identifiable patents in that
+ country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+ arrangement, you convey, or propagate by procuring conveyance of, a
+ covered work, and grant a patent license to some of the parties
+ receiving the covered work authorizing them to use, propagate, modify
+ or convey a specific copy of the covered work, then the patent license
+ you grant is automatically extended to all recipients of the covered
+ work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+ the scope of its coverage, prohibits the exercise of, or is
+ conditioned on the non-exercise of one or more of the rights that are
+ specifically granted under this License. You may not convey a covered
+ work if you are a party to an arrangement with a third party that is
+ in the business of distributing software, under which you make payment
+ to the third party based on the extent of your activity of conveying
+ the work, and under which the third party grants, to any of the
+ parties who would receive the covered work from you, a discriminatory
+ patent license (a) in connection with copies of the covered work
+ conveyed by you (or copies made from those copies), or (b) primarily
+ for and in connection with specific products or compilations that
+ contain the covered work, unless you entered into that arrangement,
+ or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+ any implied license or other defenses to infringement that may
+ otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If 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 convey a
+ covered work so as to satisfy simultaneously your obligations under this
+ License and any other pertinent obligations, then as a consequence you may
+ not convey it at all. For example, if you agree to terms that obligate you
+ to collect a royalty for further conveying from those to whom you convey
+ the Program, the only way you could satisfy both those terms and this
+ License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+ permission to link or combine any covered work with a work licensed
+ under version 3 of the GNU Affero General Public License into a single
+ combined work, and to convey the resulting work. The terms of this
+ License will continue to apply to the part which is the covered work,
+ but the special requirements of the GNU Affero General Public License,
+ section 13, concerning interaction through a network will apply to the
+ combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+ the GNU 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
+ Program specifies that a certain numbered version of the GNU General
+ Public License "or any later version" applies to it, you have the
+ option of following the terms and conditions either of that numbered
+ version or of any later version published by the Free Software
+ Foundation. If the Program does not specify a version number of the
+ GNU General Public License, you may choose any version ever published
+ by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+ versions of the GNU General Public License can be used, that proxy's
+ public statement of acceptance of a version permanently authorizes you
+ to choose that version for the Program.
+
+ Later license versions may give you additional or different
+ permissions. However, no additional obligations are imposed on any
+ author or copyright holder as a result of your choosing to follow a
+ later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+ APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+ HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM
+ IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+ THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+ EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+ SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+ above cannot be given local legal effect according to their terms,
+ reviewing courts shall apply local law that most closely approximates
+ an absolute waiver of all civil liability in connection with the
+ Program, unless a warranty or assumption of liability accompanies a
+ copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+ possible use to the public, the best way to achieve this is to make it
+ free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+ to attach them to the start of each source file to most effectively
+ state the exclusion of warranty; and each file should have at least
+ the "copyright" line and a pointer to where the full notice is found.
+
+ xmpp library for Python.
+ xmpppy Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+ Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+ notice like this when it starts in an interactive mode:
+
+ xmpppy Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+ The hypothetical commands `show w' and `show c' should show the appropriate
+ parts of the General Public License. Of course, your program's commands
+ might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+ if any, to sign a "copyright disclaimer" for the program, if necessary.
+ For more information on this, and how to apply and follow the GNU GPL, see
+ <http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+ into proprietary programs. If your program is a subroutine library, you
+ may consider it more useful to permit linking proprietary applications with
+ the library. If this is what you want to do, use the GNU Lesser General
+ Public License instead of this License. But first, please read
+ <http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/xmpp/__init__.py b/xmpp/__init__.py
new file mode 100644
index 0000000..831b7c7
--- /dev/null
+++ b/xmpp/__init__.py
@@ -0,0 +1,42 @@
+# $Id: __init__.py, v1.10 2013/10/21 alkorgun Exp $
+
+"""
+All features of xmpppy library contained within separate modules.
+At present there are modules:
+simplexml - XML handling routines
+protocol - jabber-objects (I.e. JID and different stanzas and sub-stanzas) handling routines.
+debug - Jacob Lundquist's debugging module. Very handy if you like colored debug.
+auth - Non-SASL and SASL stuff. You will need it to auth as a client or transport.
+transports - low level connection handling. TCP and TLS currently. HTTP support planned.
+roster - simple roster for use in clients.
+dispatcher - decision-making logic. Handles all hooks. The first who takes control over fresh stanzas.
+features - different stuff that didn't worths separating into modules
+browser - DISCO server framework. Allows to build dynamic disco tree.
+filetransfer - Currently contains only IBB stuff. Can be used for bot-to-bot transfers.
+
+Most of the classes that is defined in all these modules is an ancestors of
+class PlugIn so they share a single set of methods allowing you to compile
+a featured XMPP client. For every instance of PlugIn class the 'owner' is the class
+in what the plug was plugged. While plugging in such instance usually sets some
+methods of owner to it's own ones for easy access. All session specific info stored
+either in instance of PlugIn or in owner's instance. This is considered unhandy
+and there are plans to port 'Session' class from xmppd.py project for storing all
+session-related info. Though if you are not accessing instances variables directly
+and use only methods for access all values you should not have any problems.
+"""
+
+import auth
+import browser
+import commands
+import debug
+import dispatcher
+import features
+import filetransfer
+import plugin
+import protocol
+import roster
+import simplexml
+import transports
+
+from client import *
+from protocol import *
diff --git a/xmpp/auth.py b/xmpp/auth.py
new file mode 100644
index 0000000..95842f4
--- /dev/null
+++ b/xmpp/auth.py
@@ -0,0 +1,413 @@
+## auth.py
+##
+## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## 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 General Public License for more details.
+
+# $Id: auth.py, v1.42 2013/10/21 alkorgun Exp $
+
+"""
+Provides library with all Non-SASL and SASL authentication mechanisms.
+Can be used both for client and transport authentication.
+"""
+
+import dispatcher
+import sha
+
+from base64 import encodestring, decodestring
+from hashlib import md5 as __md5
+from plugin import PlugIn
+from protocol import *
+from random import random as _random
+from re import findall as re_findall
+
+def HH(some):
+ return __md5(some).hexdigest()
+
+def H(some):
+ return __md5(some).digest()
+
+def C(some):
+ return ":".join(some)
+
+class NonSASL(PlugIn):
+ """
+ Implements old Non-SASL (JEP-0078) authentication used in jabberd1.4 and transport authentication.
+ """
+ def __init__(self, user, password, resource):
+ """
+ Caches username, password and resource for auth.
+ """
+ PlugIn.__init__(self)
+ self.DBG_LINE = "gen_auth"
+ self.user = user
+ self.password = password
+ self.resource = resource
+
+ def plugin(self, owner):
+ """
+ Determine the best auth method (digest/0k/plain) and use it for auth.
+ Returns used method name on success. Used internally.
+ """
+ if not self.resource:
+ return self.authComponent(owner)
+ self.DEBUG("Querying server about possible auth methods", "start")
+ resp = owner.Dispatcher.SendAndWaitForResponse(Iq("get", NS_AUTH, payload=[Node("username", payload=[self.user])]))
+ if not isResultNode(resp):
+ self.DEBUG("No result node arrived! Aborting...", "error")
+ return None
+ iq = Iq(typ="set", node=resp)
+ query = iq.getTag("query")
+ query.setTagData("username", self.user)
+ query.setTagData("resource", self.resource)
+ if query.getTag("digest"):
+ self.DEBUG("Performing digest authentication", "ok")
+ query.setTagData("digest", sha.new(owner.Dispatcher.Stream._document_attrs["id"] + self.password).hexdigest())
+ if query.getTag("password"):
+ query.delChild("password")
+ method = "digest"
+ elif query.getTag("token"):
+ token = query.getTagData("token")
+ seq = query.getTagData("sequence")
+ self.DEBUG("Performing zero-k authentication", "ok")
+ hash = sha.new(sha.new(self.password).hexdigest() + token).hexdigest()
+ for foo in xrange(int(seq)):
+ hash = sha.new(hash).hexdigest()
+ query.setTagData("hash", hash)
+ method = "0k"
+ else:
+ self.DEBUG("Sequre methods unsupported, performing plain text authentication", "warn")
+ query.setTagData("password", self.password)
+ method = "plain"
+ resp = owner.Dispatcher.SendAndWaitForResponse(iq)
+ if isResultNode(resp):
+ self.DEBUG("Sucessfully authenticated with remove host.", "ok")
+ owner.User = self.user
+ owner.Resource = self.resource
+ owner._registered_name = owner.User + "@" + owner.Server + "/" + owner.Resource
+ return method
+ self.DEBUG("Authentication failed!", "error")
+
+ def authComponent(self, owner):
+ """
+ Authenticate component. Send handshake stanza and wait for result. Returns "ok" on success.
+ """
+ self.handshake = 0
+ owner.send(Node(NS_COMPONENT_ACCEPT + " handshake", payload=[sha.new(owner.Dispatcher.Stream._document_attrs["id"] + self.password).hexdigest()]))
+ owner.RegisterHandler("handshake", self.handshakeHandler, xmlns=NS_COMPONENT_ACCEPT)
+ while not self.handshake:
+ self.DEBUG("waiting on handshake", "notify")
+ owner.Process(1)
+ owner._registered_name = self.user
+ if self.handshake + 1:
+ return "ok"
+
+ def handshakeHandler(self, disp, stanza):
+ """
+ Handler for registering in dispatcher for accepting transport authentication.
+ """
+ if stanza.getName() == "handshake":
+ self.handshake = 1
+ else:
+ self.handshake = -1
+
+class SASL(PlugIn):
+ """
+ Implements SASL authentication.
+ """
+ def __init__(self, username, password):
+ PlugIn.__init__(self)
+ self.username = username
+ self.password = password
+
+ def plugin(self, owner):
+ if not self._owner.Dispatcher.Stream._document_attrs.has_key("version"):
+ self.startsasl = "not-supported"
+ elif self._owner.Dispatcher.Stream.features:
+ try:
+ self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features)
+ except NodeProcessed:
+ pass
+ else:
+ self.startsasl = None
+
+ def auth(self):
+ """
+ Start authentication. Result can be obtained via "SASL.startsasl" attribute
+ and will beeither "success" or "failure". Note that successfull
+ auth will take at least two Dispatcher.Process() calls.
+ """
+ if self.startsasl:
+ pass
+ elif self._owner.Dispatcher.Stream.features:
+ try:
+ self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features)
+ except NodeProcessed:
+ pass
+ else:
+ self._owner.RegisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS)
+
+ def plugout(self):
+ """
+ Remove SASL handlers from owner's dispatcher. Used internally.
+ """
+ if self._owner.__dict__.has_key("features"):
+ self._owner.UnregisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS)
+ if self._owner.__dict__.has_key("challenge"):
+ self._owner.UnregisterHandler("challenge", self.SASLHandler, xmlns=NS_SASL)
+ if self._owner.__dict__.has_key("failure"):
+ self._owner.UnregisterHandler("failure", self.SASLHandler, xmlns=NS_SASL)
+ if self._owner.__dict__.has_key("success"):
+ self._owner.UnregisterHandler("success", self.SASLHandler, xmlns=NS_SASL)
+
+ def FeaturesHandler(self, conn, feats):
+ """
+ Used to determine if server supports SASL auth. Used internally.
+ """
+ if not feats.getTag("mechanisms", namespace=NS_SASL):
+ self.startsasl = "not-supported"
+ self.DEBUG("SASL not supported by server", "error")
+ return None
+ mecs = []
+ for mec in feats.getTag("mechanisms", namespace=NS_SASL).getTags("mechanism"):
+ mecs.append(mec.getData())
+ self._owner.RegisterHandler("challenge", self.SASLHandler, xmlns=NS_SASL)
+ self._owner.RegisterHandler("failure", self.SASLHandler, xmlns=NS_SASL)
+ self._owner.RegisterHandler("success", self.SASLHandler, xmlns=NS_SASL)
+ if "ANONYMOUS" in mecs and self.username == None:
+ node = Node("auth", attrs={"xmlns": NS_SASL, "mechanism": "ANONYMOUS"})
+ elif "DIGEST-MD5" in mecs:
+ node = Node("auth", attrs={"xmlns": NS_SASL, "mechanism": "DIGEST-MD5"})
+ elif "PLAIN" in mecs:
+ sasl_data = "%s\x00%s\x00%s" % ("@".join((self.username, self._owner.Server)), self.username, self.password)
+ node = Node("auth", attrs={"xmlns": NS_SASL, "mechanism": "PLAIN"}, payload=[encodestring(sasl_data).replace("\r", "").replace("\n", "")])
+ else:
+ self.startsasl = "failure"
+ self.DEBUG("I can only use DIGEST-MD5 and PLAIN mecanisms.", "error")
+ return
+ self.startsasl = "in-process"
+ self._owner.send(node.__str__())
+ raise NodeProcessed()
+
+ def SASLHandler(self, conn, challenge):
+ """
+ Perform next SASL auth step. Used internally.
+ """
+ if challenge.getNamespace() != NS_SASL:
+ return None
+ if challenge.getName() == "failure":
+ self.startsasl = "failure"
+ try:
+ reason = challenge.getChildren()[0]
+ except:
+ reason = challenge
+ self.DEBUG("Failed SASL authentification: %s" % reason, "error")
+ raise NodeProcessed()
+ elif challenge.getName() == "success":
+ self.startsasl = "success"
+ self.DEBUG("Successfully authenticated with remote server.", "ok")
+ handlers = self._owner.Dispatcher.dumpHandlers()
+ self._owner.Dispatcher.PlugOut()
+ dispatcher.Dispatcher().PlugIn(self._owner)
+ self._owner.Dispatcher.restoreHandlers(handlers)
+ self._owner.User = self.username
+ raise NodeProcessed()
+ incoming_data = challenge.getData()
+ chal = {}
+ data = decodestring(incoming_data)
+ self.DEBUG("Got challenge:" + data, "ok")
+ for pair in re_findall('(\w+\s*=\s*(?:(?:"[^"]+")|(?:[^,]+)))', data):
+ key, value = [x.strip() for x in pair.split("=", 1)]
+ if value[:1] == '"' and value[-1:] == '"':
+ value = value[1:-1]
+ chal[key] = value
+ if chal.has_key("qop") and "auth" in [x.strip() for x in chal["qop"].split(",")]:
+ resp = {}
+ resp["username"] = self.username
+ resp["realm"] = self._owner.Server
+ resp["nonce"] = chal["nonce"]
+ cnonce = ""
+ for i in range(7):
+ cnonce += hex(int(_random() * 65536 * 4096))[2:]
+ resp["cnonce"] = cnonce
+ resp["nc"] = ("00000001")
+ resp["qop"] = "auth"
+ resp["digest-uri"] = "xmpp/" + self._owner.Server
+ A1 = C([H(C([resp["username"], resp["realm"], self.password])), resp["nonce"], resp["cnonce"]])
+ A2 = C(["AUTHENTICATE", resp["digest-uri"]])
+ response = HH(C([HH(A1), resp["nonce"], resp["nc"], resp["cnonce"], resp["qop"], HH(A2)]))
+ resp["response"] = response
+ resp["charset"] = "utf-8"
+ sasl_data = ""
+ for key in ["charset", "username", "realm", "nonce", "nc", "cnonce", "digest-uri", "response", "qop"]:
+ if key in ["nc", "qop", "response", "charset"]:
+ sasl_data += "%s=%s," % (key, resp[key])
+ else:
+ sasl_data += "%s=\"%s\"," % (key, resp[key])
+ node = Node("response", attrs={"xmlns": NS_SASL}, payload=[encodestring(sasl_data[:-1]).replace("\r", "").replace("\n", "")])
+ self._owner.send(node.__str__())
+ elif chal.has_key("rspauth"):
+ self._owner.send(Node("response", attrs={"xmlns": NS_SASL}).__str__())
+ else:
+ self.startsasl = "failure"
+ self.DEBUG("Failed SASL authentification: unknown challenge", "error")
+ raise NodeProcessed()
+
+class Bind(PlugIn):
+ """
+ Bind some JID to the current connection to allow router know of our location.
+ """
+ def __init__(self):
+ PlugIn.__init__(self)
+ self.DBG_LINE = "bind"
+ self.bound = None
+
+ def plugin(self, owner):
+ """
+ Start resource binding, if allowed at this time. Used internally.
+ """
+ if self._owner.Dispatcher.Stream.features:
+ try:
+ self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features)
+ except NodeProcessed:
+ pass
+ else:
+ self._owner.RegisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS)
+
+ def plugout(self):
+ """
+ Remove Bind handler from owner's dispatcher. Used internally.
+ """
+ self._owner.UnregisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS)
+
+ def FeaturesHandler(self, conn, feats):
+ """
+ Determine if server supports resource binding and set some internal attributes accordingly.
+ """
+ if not feats.getTag("bind", namespace=NS_BIND):
+ self.bound = "failure"
+ self.DEBUG("Server does not requested binding.", "error")
+ return Nonr
+ if feats.getTag("session", namespace=NS_SESSION):
+ self.session = 1
+ else:
+ self.session = -1
+ self.bound = []
+
+ def Bind(self, resource=None):
+ """
+ Perform binding. Use provided resource name or random (if not provided).
+ """
+ while self.bound is None and self._owner.Process(1):
+ pass
+ if resource:
+ resource = [Node("resource", payload=[resource])]
+ else:
+ resource = []
+ resp = self._owner.SendAndWaitForResponse(Protocol("iq", typ="set", payload=[Node("bind", attrs={"xmlns": NS_BIND}, payload=resource)]))
+ if isResultNode(resp):
+ self.bound.append(resp.getTag("bind").getTagData("jid"))
+ self.DEBUG("Successfully bound %s." % self.bound[-1], "ok")
+ jid = JID(resp.getTag("bind").getTagData("jid"))
+ self._owner.User = jid.getNode()
+ self._owner.Resource = jid.getResource()
+ resp = self._owner.SendAndWaitForResponse(Protocol("iq", typ="set", payload=[Node("session", attrs={"xmlns": NS_SESSION})]))
+ if isResultNode(resp):
+ self.DEBUG("Successfully opened session.", "ok")
+ self.session = 1
+ return "ok"
+ else:
+ self.DEBUG("Session open failed.", "error")
+ self.session = 0
+ elif resp:
+ self.DEBUG("Binding failed: %s." % resp.getTag("error"), "error")
+ else:
+ self.DEBUG("Binding failed: timeout expired.", "error")
+ return ""
+
+class ComponentBind(PlugIn):
+ """
+ ComponentBind some JID to the current connection to allow router know of our location.
+ """
+ def __init__(self, sasl):
+ PlugIn.__init__(self)
+ self.DBG_LINE = "bind"
+ self.bound = None
+ self.needsUnregister = None
+ self.sasl = sasl
+
+ def plugin(self, owner):
+ """
+ Start resource binding, if allowed at this time. Used internally.
+ """
+ if not self.sasl:
+ self.bound = []
+ return None
+ if self._owner.Dispatcher.Stream.features:
+ try:
+ self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features)
+ except NodeProcessed:
+ pass
+ else:
+ self._owner.RegisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS)
+ self.needsUnregister = 1
+
+ def plugout(self):
+ """
+ Remove ComponentBind handler from owner's dispatcher. Used internally.
+ """
+ if self.needsUnregister:
+ self._owner.UnregisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS)
+
+ def FeaturesHandler(self, conn, feats):
+ """
+ Determine if server supports resource binding and set some internal attributes accordingly.
+ """
+ if not feats.getTag("bind", namespace=NS_BIND):
+ self.bound = "failure"
+ self.DEBUG("Server does not requested binding.", "error")
+ return None
+ if feats.getTag("session", namespace=NS_SESSION):
+ self.session = 1
+ else:
+ self.session = -1
+ self.bound = []
+
+ def Bind(self, domain=None):
+ """
+ Perform binding. Use provided domain name (if not provided).
+ """
+ while self.bound is None and self._owner.Process(1):
+ pass
+ if self.sasl:
+ xmlns = NS_COMPONENT_1
+ else:
+ xmlns = None
+ self.bindresponse = None
+ ttl = dispatcher.DefaultTimeout
+ self._owner.RegisterHandler("bind", self.BindHandler, xmlns=xmlns)
+ self._owner.send(Protocol("bind", attrs={"name": domain}, xmlns=NS_COMPONENT_1))
+ while self.bindresponse is None and self._owner.Process(1) and ttl > 0:
+ ttl -= 1
+ self._owner.UnregisterHandler("bind", self.BindHandler, xmlns=xmlns)
+ resp = self.bindresponse
+ if resp and resp.getAttr("error"):
+ self.DEBUG("Binding failed: %s." % resp.getAttr("error"), "error")
+ elif resp:
+ self.DEBUG("Successfully bound.", "ok")
+ return "ok"
+ else:
+ self.DEBUG("Binding failed: timeout expired.", "error")
+ return ""
+
+ def BindHandler(self, conn, bind):
+ self.bindresponse = bind
+ pass
diff --git a/xmpp/browser.py b/xmpp/browser.py
new file mode 100644
index 0000000..d2ffdf0
--- /dev/null
+++ b/xmpp/browser.py
@@ -0,0 +1,261 @@
+## browser.py
+##
+## Copyright (C) 2004 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## 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 General Public License for more details.
+
+# $Id: browser.py, v1.13 2013/11/03 alkorgun Exp $
+
+"""
+Browser module provides DISCO server framework for your application.
+This functionality can be used for very different purposes - from publishing
+software version and supported features to building of "jabber site" that users
+can navigate with their disco browsers and interact with active content.
+
+Such functionality is achieved via registering "DISCO handlers" that are
+automatically called when user requests some node of your disco tree.
+"""
+
+from dispatcher import *
+from plugin import PlugIn
+
+class Browser(PlugIn):
+ """
+ WARNING! This class is for components only. It will not work in client mode!
+
+ Standart xmpppy class that is ancestor of PlugIn and can be attached
+ to your application.
+ All processing will be performed in the handlers registered in the browser
+ instance. You can register any number of handlers ensuring that for each
+ node/jid combination only one (or none) handler registered.
+ You can register static information or the fully-blown function that will
+ calculate the answer dynamically.
+ Example of static info (see JEP-0030, examples 13-14):
+ # cl - your xmpppy connection instance.
+ b = xmpp.browser.Browser()
+ b.PlugIn(cl)
+ items = []
+ item = {}
+ item["jid"] = "catalog.shakespeare.lit"
+ item["node"] = "books"
+ item["name"] = "Books by and about Shakespeare"
+ items.append(item)
+ item = {}
+ item["jid"] = "catalog.shakespeare.lit"
+ item["node"] = "clothing"
+ item["name"] = "Wear your literary taste with pride"
+ items.append(item)
+ item = {}
+ item["jid"] = "catalog.shakespeare.lit"
+ item["node"] = "music"
+ item["name"] = "Music from the time of Shakespeare"
+ items.append(item)
+ info = {"ids": [], "features": []}
+ b.setDiscoHandler({"items": items, "info": info})
+
+ items should be a list of item elements.
+ every item element can have any of these four keys: "jid", "node", "name", "action"
+ info should be a dicionary and must have keys "ids" and "features".
+ Both of them should be lists:
+ ids is a list of dictionaries and features is a list of text strings.
+ Example (see JEP-0030, examples 1-2)
+ # cl - your xmpppy connection instance.
+ b = xmpp.browser.Browser()
+ b.PlugIn(cl)
+ items = []
+ ids = []
+ ids.append({"category": "conference", "type": "text", "name": "Play-Specific Chatrooms"})
+ ids.append({"category": "directory", "type": "chatroom", "name": "Play-Specific Chatrooms"})
+ features = [
+ NS_DISCO_INFO,
+ NS_DISCO_ITEMS,
+ NS_MUC,
+ NS_REGISTER,
+ NS_SEARCH,
+ NS_TIME,
+ NS_VERSION
+ ]
+ info = {"ids": ids, "features": features}
+ # info["xdata"] = xmpp.protocol.DataForm() # JEP-0128
+ b.setDiscoHandler({"items": [], "info": info})
+ """
+ def __init__(self):
+ """
+ Initialises internal variables. Used internally.
+ """
+ PlugIn.__init__(self)
+ DBG_LINE = "browser"
+ self._exported_methods = []
+ self._handlers = {"": {}}
+
+ def plugin(self, owner):
+ """
+ Registers it's own iq handlers in your application dispatcher instance.
+ Used internally.
+ """
+ owner.RegisterHandler("iq", self._DiscoveryHandler, typ="get", ns=NS_DISCO_INFO)
+ owner.RegisterHandler("iq", self._DiscoveryHandler, typ="get", ns=NS_DISCO_ITEMS)
+
+ def plugout(self):
+ """
+ Unregisters browser's iq handlers from your application dispatcher instance.
+ Used internally.
+ """
+ self._owner.UnregisterHandler("iq", self._DiscoveryHandler, typ="get", ns=NS_DISCO_INFO)
+ self._owner.UnregisterHandler("iq", self._DiscoveryHandler, typ="get", ns=NS_DISCO_ITEMS)
+
+ def _traversePath(self, node, jid, set=0):
+ """
+ Returns dictionary and key or None,None
+ None - root node (w/o "node" attribute)
+ /a/b/c - node
+ /a/b/ - branch
+ Set returns "" or None as the key
+ get returns "" or None as the key or None as the dict.
+ Used internally.
+ """
+ if self._handlers.has_key(jid):
+ cur = self._handlers[jid]
+ elif set:
+ self._handlers[jid] = {}
+ cur = self._handlers[jid]
+ else:
+ cur = self._handlers[""]
+ if node is None:
+ node = [None]
+ else:
+ node = node.replace("/", " /").split("/")
+ for i in node:
+ if i != "" and cur.has_key(i):
+ cur = cur[i]
+ elif set and i != "":
+ cur[i] = {dict: cur, str: i}
+ cur = cur[i]
+ elif set or cur.has_key(""):
+ return cur, ""
+ else:
+ return None, None
+ if cur.has_key(1) or set:
+ return cur, 1
+ raise Exception("Corrupted data")
+
+ def setDiscoHandler(self, handler, node="", jid=""):
+ """
+ This is the main method that you will use in this class.
+ It is used to register supplied DISCO handler (or dictionary with static info)
+ as handler of some disco tree branch.
+ If you do not specify the node this handler will be used for all queried nodes.
+ If you do not specify the jid this handler will be used for all queried JIDs.
+
+ Usage:
+ cl.Browser.setDiscoHandler(someDict, node, jid)
+ or
+ cl.Browser.setDiscoHandler(someDISCOHandler, node, jid)
+ where
+
+ someDict = {
+ "items":[
+ {"jid": "jid2", "action": "action2", "node":"node2", "name": "name2"},
+ {"jid": "jid4", "node": "node4"}
+ ],
+ "info" :{
+ "ids":[
+ {"category":" category1", "type": "type1", "name": "name1"},
+ {"category":" category3", "type": "type3", "name": "name3"},
+ ],
+ "features": ["feature1", "feature2", "feature3", "feature4"],
+ "xdata": DataForm
+ }
+ }
+
+ and/or
+
+ def someDISCOHandler(session,request,TYR):
+ # if TYR == "items": # returns items list of the same format as shown above
+ # elif TYR == "info": # returns info dictionary of the same format as shown above
+ # else: # this case is impossible for now.
+ """
+ self.DEBUG("Registering handler %s for \"%s\" node->%s" % (handler, jid, node), "info")
+ node, key = self._traversePath(node, jid, 1)
+ node[key] = handler
+
+ def getDiscoHandler(self, node="", jid=""):
+ """
+ Returns the previously registered DISCO handler
+ that is resonsible for this node/jid combination.
+ Used internally.
+ """
+ node, key = self._traversePath(node, jid)
+ if node:
+ return node[key]
+
+ def delDiscoHandler(self, node="", jid=""):
+ """
+ Unregisters DISCO handler that is resonsible for this
+ node/jid combination. When handler is unregistered the branch
+ is handled in the same way that it's parent branch from this moment.
+ """
+ node, key = self._traversePath(node, jid)
+ if node:
+ handler = node[key]
+ del node[dict][node[str]]
+ return handler
+
+ def _DiscoveryHandler(self, conn, request):
+ """
+ Servers DISCO iq request from the remote client.
+ Automatically determines the best handler to use and calls it
+ (to handle the request. Used internally.
+ """
+ node = request.getQuerynode()
+ if node:
+ nodestr = node
+ else:
+ nodestr = "None"
+ handler = self.getDiscoHandler(node, request.getTo())
+ if not handler:
+ self.DEBUG("No Handler for request with jid->%s node->%s ns->%s" % (request.getTo().__str__().encode("utf8"), nodestr.encode("utf8"), request.getQueryNS().encode("utf8")), "error")
+ conn.send(Error(request, ERR_ITEM_NOT_FOUND))
+ raise NodeProcessed()
+ self.DEBUG("Handling request with jid->%s node->%s ns->%s" % (request.getTo().__str__().encode("utf8"), nodestr.encode("utf8"), request.getQueryNS().encode("utf8")), "ok")
+ rep = request.buildReply("result")
+ if node:
+ rep.setQuerynode(node)
+ q = rep.getTag("query")
+ if request.getQueryNS() == NS_DISCO_ITEMS:
+ # handler must return list: [{jid, action, node, name}]
+ if isinstance(handler, dict):
+ lst = handler["items"]
+ else:
+ lst = handler(conn, request, "items")
+ if lst == None:
+ conn.send(Error(request, ERR_ITEM_NOT_FOUND))
+ raise NodeProcessed()
+ for item in lst:
+ q.addChild("item", item)
+ elif request.getQueryNS() == NS_DISCO_INFO:
+ if isinstance(handler, dict):
+ dt = handler["info"]
+ else:
+ dt = handler(conn, request, "info")
+ if dt == None:
+ conn.send(Error(request, ERR_ITEM_NOT_FOUND))
+ raise NodeProcessed()
+ # handler must return dictionary:
+ # {"ids": [{}, {}, {}, {}], "features": [fe, at, ur, es], "xdata": DataForm}
+ for id in dt["ids"]:
+ q.addChild("identity", id)
+ for feature in dt["features"]:
+ q.addChild("feature", {"var": feature})
+ if dt.has_key("xdata"):
+ q.addChild(node=dt["xdata"])
+ conn.send(rep)
+ raise NodeProcessed()
diff --git a/xmpp/client.py b/xmpp/client.py
new file mode 100644
index 0000000..b1c090a
--- /dev/null
+++ b/xmpp/client.py
@@ -0,0 +1,374 @@
+## client.py
+##
+## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## 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 General Public License for more details.
+
+# $Id: client.py, v1.62 2013/10/21 alkorgun Exp $
+
+"""
+Provides PlugIn class functionality to develop extentions for xmpppy.
+Also provides Client and Component classes implementations as the
+examples of xmpppy structures usage.
+These classes can be used for simple applications "AS IS" though.
+"""
+
+import debug
+import transports
+import dispatcher
+import auth
+import roster
+
+from plugin import PlugIn
+
+Debug = debug
+Debug.DEBUGGING_IS_ON = 1
+
+Debug.Debug.colors["socket"] = debug.color_dark_gray
+Debug.Debug.colors["CONNECTproxy"] = debug.color_dark_gray
+Debug.Debug.colors["nodebuilder"] = debug.color_brown
+Debug.Debug.colors["client"] = debug.color_cyan
+Debug.Debug.colors["component"] = debug.color_cyan
+Debug.Debug.colors["dispatcher"] = debug.color_green
+Debug.Debug.colors["browser"] = debug.color_blue
+Debug.Debug.colors["auth"] = debug.color_yellow
+Debug.Debug.colors["roster"] = debug.color_magenta
+Debug.Debug.colors["ibb"] = debug.color_yellow
+Debug.Debug.colors["down"] = debug.color_brown
+Debug.Debug.colors["up"] = debug.color_brown
+Debug.Debug.colors["data"] = debug.color_brown
+Debug.Debug.colors["ok"] = debug.color_green
+Debug.Debug.colors["warn"] = debug.color_yellow
+Debug.Debug.colors["error"] = debug.color_red
+Debug.Debug.colors["start"] = debug.color_dark_gray
+Debug.Debug.colors["stop"] = debug.color_dark_gray
+Debug.Debug.colors["sent"] = debug.color_yellow
+Debug.Debug.colors["got"] = debug.color_bright_cyan
+
+DBG_CLIENT = "client"
+DBG_COMPONENT = "component"
+
+
+class CommonClient:
+ """
+ Base for Client and Component classes.
+ """
+ def __init__(self, server, port=5222, debug=["always", "nodebuilder"]):
+ """
+ Caches server name and (optionally) port to connect to. "debug" parameter specifies
+ the debug IDs that will go into debug output. You can either specifiy an "include"
+ or "exclude" list. The latter is done via adding "always" pseudo-ID to the list.
+ Full list: ["nodebuilder", "dispatcher", "gen_auth", "SASL_auth", "bind", "socket",
+ "CONNECTproxy", "TLS", "roster", "browser", "ibb"].
+ """
+ if isinstance(self, Client):
+ self.Namespace, self.DBG = "jabber:client", DBG_CLIENT
+ elif isinstance(self, Component):
+ self.Namespace, self.DBG = dispatcher.NS_COMPONENT_ACCEPT, DBG_COMPONENT
+ self.defaultNamespace = self.Namespace
+ self.disconnect_handlers = []
+ self.Server = server
+ self.Port = port
+ if debug and not isinstance(debug, list):
+ debug = ["always", "nodebuilder"]
+ self._DEBUG = Debug.Debug(debug)
+ self.DEBUG = self._DEBUG.Show
+ self.debug_flags = self._DEBUG.debug_flags
+ self.debug_flags.append(self.DBG)
+ self._owner = self
+ self._registered_name = None
+ self.RegisterDisconnectHandler(self.DisconnectHandler)
+ self.connected = ""
+ self._route = 0
+
+ def RegisterDisconnectHandler(self, handler):
+ """
+ Register handler that will be called on disconnect.
+ """
+ self.disconnect_handlers.append(handler)
+
+ def UnregisterDisconnectHandler(self, handler):
+ """
+ Unregister handler that is called on disconnect.
+ """
+ self.disconnect_handlers.remove(handler)
+
+ def disconnected(self):
+ """
+ Called on disconnection. Calls disconnect handlers and cleans things up.
+ """
+ self.connected = ""
+ self.DEBUG(self.DBG, "Disconnect detected", "stop")
+ self.disconnect_handlers.reverse()
+ for dhnd in self.disconnect_handlers:
+ dhnd()
+ self.disconnect_handlers.reverse()
+ if self.__dict__.has_key("TLS"):
+ self.TLS.PlugOut()
+
+ def DisconnectHandler(self):
+ """
+ Default disconnect handler. Just raises an IOError.
+ If you choosed to use this class in your production client,
+ override this method or at least unregister it.
+ """
+ raise IOError("Disconnected!")
+
+ def event(self, eventName, args={}):
+ """
+ Default event handler. To be overriden.
+ """
+ print "Event: ", (eventName, args)
+
+ def isConnected(self):
+ """
+ Returns connection state. F.e.: None / "tls" / "tcp+non_sasl" .
+ """
+ return self.connected
+
+ def reconnectAndReauth(self, handlerssave=None):
+ """
+ Example of reconnection method. In fact, it can be used to batch connection and auth as well.
+ """
+ Dispatcher_ = False
+ if not handlerssave:
+ Dispatcher_, handlerssave = True, self.Dispatcher.dumpHandlers()
+ if self.__dict__.has_key("ComponentBind"):
+ self.ComponentBind.PlugOut()
+ if self.__dict__.has_key("Bind"):
+ self.Bind.PlugOut()
+ self._route = 0
+ if self.__dict__.has_key("NonSASL"):
+ self.NonSASL.PlugOut()
+ if self.__dict__.has_key("SASL"):
+ self.SASL.PlugOut()
+ if self.__dict__.has_key("TLS"):
+ self.TLS.PlugOut()
+ if Dispatcher_:
+ self.Dispatcher.PlugOut()
+ if self.__dict__.has_key("HTTPPROXYsocket"):
+ self.HTTPPROXYsocket.PlugOut()
+ if self.__dict__.has_key("TCPsocket"):
+ self.TCPsocket.PlugOut()
+ if not self.connect(server=self._Server, proxy=self._Proxy):
+ return None
+ if not self.auth(self._User, self._Password, self._Resource):
+ return None
+ self.Dispatcher.restoreHandlers(handlerssave)
+ return self.connected
+
+ def connect(self, server=None, proxy=None, ssl=None, use_srv=False):
+ """
+ Make a tcp/ip connection, protect it with tls/ssl if possible and start XMPP stream.
+ Returns None or "tcp" or "tls", depending on the result.
+ """
+ if not server:
+ server = (self.Server, self.Port)
+ if proxy:
+ sock = transports.HTTPPROXYsocket(proxy, server, use_srv)
+ else:
+ sock = transports.TCPsocket(server, use_srv)
+ connected = sock.PlugIn(self)
+ if not connected:
+ sock.PlugOut()
+ return None
+ self._Server, self._Proxy = server, proxy
+ self.connected = "tcp"
+ if (ssl is None and self.Connection.getPort() in (5223, 443)) or ssl:
+ try: # FIXME. This should be done in transports.py
+ transports.TLS().PlugIn(self, now=1)
+ self.connected = "ssl"
+ except transports.socket.sslerror:
+ return None
+ dispatcher.Dispatcher().PlugIn(self)
+ while self.Dispatcher.Stream._document_attrs is None:
+ if not self.Process(1):
+ return None
+ if self.Dispatcher.Stream._document_attrs.has_key("version") and self.Dispatcher.Stream._document_attrs["version"] == "1.0":
+ while not self.Dispatcher.Stream.features and self.Process(1):
+ pass # If we get version 1.0 stream the features tag MUST BE presented
+ return self.connected
+
+class Client(CommonClient):
+ """
+ Example client class, based on CommonClient.
+ """
+ def connect(self, server=None, proxy=None, secure=None, use_srv=True):
+ """
+ Connect to jabber server. If you want to specify different ip/port to connect to you can
+ pass it as tuple as first parameter. If there is HTTP proxy between you and server
+ specify it's address and credentials (if needed) in the second argument.
+ If you want ssl/tls support to be discovered and enable automatically - leave third argument as None. (ssl will be autodetected only if port is 5223 or 443)
+ If you want to force SSL start (i.e. if port 5223 or 443 is remapped to some non-standard port) then set it to 1.
+ If you want to disable tls/ssl support completely, set it to 0.
+ Example: connect(("192.168.5.5", 5222), {"host": "proxy.my.net", "port": 8080, "user": "me", "password": "secret"})
+ Returns "" or "tcp" or "tls", depending on the result.
+ """
+ if not CommonClient.connect(self, server, proxy, secure, use_srv) or secure != None and not secure:
+ return self.connected
+ transports.TLS().PlugIn(self)
+ if not hasattr(self, "Dispatcher"):
+ return None
+ if not self.Dispatcher.Stream._document_attrs.has_key("version") or not self.Dispatcher.Stream._document_attrs["version"] == "1.0":
+ return self.connected
+ while not self.Dispatcher.Stream.features and self.Process(1):
+ pass # If we get version 1.0 stream the features tag MUST BE presented
+ if not self.Dispatcher.Stream.features.getTag("starttls"):
+ return self.connected # TLS not supported by server
+ while not self.TLS.starttls and self.Process(1):
+ pass
+ if not hasattr(self, "TLS") or self.TLS.starttls != "success":
+ self.event("tls_failed"); return self.connected
+ self.connected = "tls"
+ return self.connected
+
+ def auth(self, user, password, resource="", sasl=1):
+ """
+ Authenticate connnection and bind resource. If resource is not provided
+ random one or library name used.
+ """
+ self._User, self._Password, self._Resource = user, password, resource
+ while not self.Dispatcher.Stream._document_attrs and self.Process(1):
+ pass
+ if self.Dispatcher.Stream._document_attrs.has_key("version") and self.Dispatcher.Stream._document_attrs["version"] == "1.0":
+ while not self.Dispatcher.Stream.features and self.Process(1):
+ pass # If we get version 1.0 stream the features tag MUST BE presented
+ if sasl:
+ auth.SASL(user, password).PlugIn(self)
+ if not sasl or self.SASL.startsasl == "not-supported":
+ if not resource:
+ resource = "xmpppy"
+ if auth.NonSASL(user, password, resource).PlugIn(self):
+ self.connected += "+old_auth"
+ return "old_auth"
+ return None
+ self.SASL.auth()
+ while self.SASL.startsasl == "in-process" and self.Process(1):
+ pass
+ if self.SASL.startsasl == "success":
+ auth.Bind().PlugIn(self)
+ while self.Bind.bound is None and self.Process(1):
+ pass
+ if self.Bind.Bind(resource):
+ self.connected += "+sasl"
+ return "sasl"
+ elif self.__dict__.has_key("SASL"):
+ self.SASL.PlugOut()
+
+ def getRoster(self):
+ """
+ Return the Roster instance, previously plugging it in and
+ requesting roster from server if needed.
+ """
+ if not self.__dict__.has_key("Roster"):
+ roster.Roster().PlugIn(self)
+ return self.Roster.getRoster()
+
+ def sendInitPresence(self, requestRoster=1):
+ """
+ Send roster request and initial <presence/>.
+ You can disable the first by setting requestRoster argument to 0.
+ """
+ self.sendPresence(requestRoster=requestRoster)
+
+ def sendPresence(self, jid=None, typ=None, requestRoster=0):
+ """
+ Send some specific presence state.
+ Can also request roster from server if according agrument is set.
+ """
+ if requestRoster:
+ roster.Roster().PlugIn(self)
+ self.send(dispatcher.Presence(to=jid, typ=typ))
+
+class Component(CommonClient):
+ """
+ Component class. The only difference from CommonClient is ability to perform component authentication.
+ """
+ def __init__(self, transport, port=5347, typ=None, debug=["always", "nodebuilder"], domains=None, sasl=0, bind=0, route=0, xcp=0):
+ """
+ Init function for Components.
+ As components use a different auth mechanism which includes the namespace of the component.
+ Jabberd1.4 and Ejabberd use the default namespace then for all client messages.
+ Jabberd2 uses jabber:client.
+ "transport" argument is a transport name that you are going to serve (f.e. "irc.localhost").
+ "port" can be specified if "transport" resolves to correct IP. If it is not then you'll have to specify IP
+ and port while calling "connect()".
+ If you are going to serve several different domains with single Component instance - you must list them ALL
+ in the "domains" argument.
+ For jabberd2 servers you should set typ="jabberd2" argument.
+ """
+ CommonClient.__init__(self, transport, port=port, debug=debug)
+ self.typ = typ
+ self.sasl = sasl
+ self.bind = bind
+ self.route = route
+ self.xcp = xcp
+ if domains:
+ self.domains = domains
+ else:
+ self.domains = [transport]
+
+ def connect(self, server=None, proxy=None):
+ """
+ This will connect to the server, and if the features tag is found then set
+ the namespace to be jabber:client as that is required for jabberd2.
+ "server" and "proxy" arguments have the same meaning as in xmpp.Client.connect().
+ """
+ if self.sasl:
+ self.Namespace = auth.NS_COMPONENT_1
+ self.Server = server[0]
+ CommonClient.connect(self, server=server, proxy=proxy)
+ if self.connected and (self.typ == "jabberd2" or not self.typ and self.Dispatcher.Stream.features != None) and (not self.xcp):
+ self.defaultNamespace = auth.NS_CLIENT
+ self.Dispatcher.RegisterNamespace(self.defaultNamespace)
+ self.Dispatcher.RegisterProtocol("iq", dispatcher.Iq)
+ self.Dispatcher.RegisterProtocol("message", dispatcher.Message)
+ self.Dispatcher.RegisterProtocol("presence", dispatcher.Presence)
+ return self.connected
+
+ def dobind(self, sasl):
+ # This has to be done before binding, because we can receive a route stanza before binding finishes
+ self._route = self.route
+ if self.bind:
+ for domain in self.domains:
+ auth.ComponentBind(sasl).PlugIn(self)
+ while self.ComponentBind.bound is None:
+ self.Process(1)
+ if (not self.ComponentBind.Bind(domain)):
+ self.ComponentBind.PlugOut()
+ return None
+ self.ComponentBind.PlugOut()
+
+ def auth(self, name, password, dup=None):
+ """
+ Authenticate component "name" with password "password".
+ """
+ self._User, self._Password, self._Resource = name, password, ""
+ try:
+ if self.sasl:
+ auth.SASL(name, password).PlugIn(self)
+ if not self.sasl or self.SASL.startsasl == "not-supported":
+ if auth.NonSASL(name, password, "").PlugIn(self):
+ self.dobind(sasl=False)
+ self.connected += "+old_auth"
+ return "old_auth"
+ return None
+ self.SASL.auth()
+ while self.SASL.startsasl == "in-process" and self.Process(1):
+ pass
+ if self.SASL.startsasl == "success":
+ self.dobind(sasl=True)
+ self.connected += "+sasl"
+ return "sasl"
+ else:
+ raise auth.NotAuthorized(self.SASL.startsasl)
+ except:
+ self.DEBUG(self.DBG, "Failed to authenticate %s" % name, "error")
diff --git a/xmpp/commands.py b/xmpp/commands.py
new file mode 100644
index 0000000..f38fc2f
--- /dev/null
+++ b/xmpp/commands.py
@@ -0,0 +1,448 @@
+## Ad-Hoc Command manager
+
+## Mike Albon (c) 5th January 2005
+
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## 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 General Public License for more details.
+
+# $Id: commands.py, v1.18 2013/11/05 alkorgun Exp $
+
+"""
+This module is a ad-hoc command processor for xmpppy. It uses the plug-in mechanism like most of the core library.
+It depends on a DISCO browser manager.
+
+There are 3 classes here, a command processor Commands like the Browser,
+and a command template plugin Command, and an example command.
+
+To use this module:
+
+ Instansiate the module with the parent transport and disco browser manager as parameters.
+ "Plug in" commands using the command template.
+ The command feature must be added to existing disco replies where neccessary.
+
+What it supplies:
+
+ Automatic command registration with the disco browser manager.
+ Automatic listing of commands in the public command list.
+ A means of handling requests, by redirection though the command manager.
+"""
+
+from plugin import PlugIn
+from protocol import *
+
+class Commands(PlugIn):
+ """
+ Commands is an ancestor of PlugIn and can be attached to any session.
+
+ The commands class provides a lookup and browse mechnism.
+ It follows the same priciple of the Browser class, for Service Discovery to provide the list of commands,
+ it adds the "list" disco type to your existing disco handler function.
+
+ How it works:
+ The commands are added into the existing Browser on the correct nodes.
+ When the command list is built the supplied discovery handler function needs to have a "list" option in type.
+ This then gets enumerated, all results returned as None are ignored.
+ The command executed is then called using it's Execute method.
+ All session management is handled by the command itself.
+ """
+ def __init__(self, browser):
+ """
+ Initialises class and sets up local variables.
+ """
+ PlugIn.__init__(self)
+ DBG_LINE = "commands"
+ self._exported_methods = []
+ self._handlers = {"": {}}
+ self._browser = browser
+
+ def plugin(self, owner):
+ """
+ Makes handlers within the session.
+ """
+ # Plug into the session and the disco manager
+ # We only need get and set, results are not needed by a service provider, only a service user.
+ owner.RegisterHandler("iq", self._CommandHandler, typ="set", ns=NS_COMMANDS)
+ owner.RegisterHandler("iq", self._CommandHandler, typ="get", ns=NS_COMMANDS)
+ self._browser.setDiscoHandler(self._DiscoHandler, node=NS_COMMANDS, jid="")
+
+ def plugout(self):
+ """
+ Removes handlers from the session.
+ """
+ # unPlug from the session and the disco manager
+ self._owner.UnregisterHandler("iq", self._CommandHandler, ns=NS_COMMANDS)
+ for jid in self._handlers:
+ self._browser.delDiscoHandler(self._DiscoHandler, node=NS_COMMANDS)
+
+ def _CommandHandler(self, conn, request):
+ """
+ The internal method to process the routing of command execution requests.
+ """
+ # This is the command handler itself.
+ # We must:
+ # Pass on command execution to command handler
+ # (Do we need to keep session details here, or can that be done in the command?)
+ jid = str(request.getTo())
+ try:
+ node = request.getTagAttr("command", "node")
+ except:
+ conn.send(Error(request, ERR_BAD_REQUEST))
+ raise NodeProcessed()
+ if self._handlers.has_key(jid):
+ if self._handlers[jid].has_key(node):
+ self._handlers[jid][node]["execute"](conn, request)
+ else:
+ conn.send(Error(request, ERR_ITEM_NOT_FOUND))
+ raise NodeProcessed()
+ elif self._handlers[""].has_key(node):
+ self._handlers[""][node]["execute"](conn, request)
+ else:
+ conn.send(Error(request, ERR_ITEM_NOT_FOUND))
+ raise NodeProcessed()
+
+ def _DiscoHandler(self, conn, request, typ):
+ """
+ The internal method to process service discovery requests.
+ """
+ # This is the disco manager handler.
+ if typ == "items":
+ # We must:
+ # Generate a list of commands and return the list
+ # * This handler does not handle individual commands disco requests.
+ # Pseudo:
+ # Enumerate the "item" disco of each command for the specified jid
+ # Build responce and send
+ # To make this code easy to write we add an "list" disco type, it returns a tuple or "none" if not advertised
+ list = []
+ items = []
+ jid = str(request.getTo())
+ # Get specific jid based results
+ if self._handlers.has_key(jid):
+ for each in self._handlers[jid].keys():
+ items.append((jid, each))
+ else:
+ # Get generic results
+ for each in self._handlers[""].keys():
+ items.append(("", each))
+ if items:
+ for each in items:
+ i = self._handlers[each[0]][each[1]]["disco"](conn, request, "list")
+ if i != None:
+ list.append(Node(tag="item", attrs={"jid": i[0], "node": i[1], "name": i[2]}))
+ iq = request.buildReply("result")
+ if request.getQuerynode():
+ iq.setQuerynode(request.getQuerynode())
+ iq.setQueryPayload(list)
+ conn.send(iq)
+ else:
+ conn.send(Error(request, ERR_ITEM_NOT_FOUND))
+ raise NodeProcessed()
+ if typ == "info":
+ return {
+ "ids": [{"category": "automation", "type": "command-list"}],
+ "features": []
+ }
+
+ def addCommand(self, name, cmddisco, cmdexecute, jid=""):
+ """
+ The method to call if adding a new command to the session,
+ the requred parameters of cmddisco and cmdexecute
+ are the methods to enable that command to be executed.
+ """
+ # This command takes a command object and the name of the command for registration
+ # We must:
+ # Add item into disco
+ # Add item into command list
+ if not self._handlers.has_key(jid):
+ self._handlers[jid] = {}
+ self._browser.setDiscoHandler(self._DiscoHandler, node=NS_COMMANDS, jid=jid)
+ if self._handlers[jid].has_key(name):
+ raise NameError("Command Exists")
+ self._handlers[jid][name] = {"disco": cmddisco, "execute": cmdexecute}
+ # Need to add disco stuff here
+ self._browser.setDiscoHandler(cmddisco, node=name, jid=jid)
+
+ def delCommand(self, name, jid=""):
+ """
+ Removed command from the session.
+ """
+ # This command takes a command object and the name used for registration
+ # We must:
+ # Remove item from disco
+ # Remove item from command list
+ if not self._handlers.has_key(jid):
+ raise NameError("Jid not found")
+ if not self._handlers[jid].has_key(name):
+ raise NameError("Command not found")
+ # Do disco removal here
+ command = self.getCommand(name, jid)["disco"]
+ del self._handlers[jid][name]
+ self._browser.delDiscoHandler(command, node=name, jid=jid)
+
+ def getCommand(self, name, jid=""):
+ """
+ Returns the command tuple.
+ """
+ # This gets the command object with name
+ # We must:
+ # Return item that matches this name
+ if not self._handlers.has_key(jid):
+ raise NameError("Jid not found")
+ if not self._handlers[jid].has_key(name):
+ raise NameError("Command not found")
+ return self._handlers[jid][name]
+
+class Command_Handler_Prototype(PlugIn):
+ """
+ This is a prototype command handler, as each command uses a disco method
+ and execute method you can implement it any way you like, however this is
+ my first attempt at making a generic handler that you can hang process
+ stages on too. There is an example command below.
+
+ The parameters are as follows:
+ name: the name of the command within the jabber environment
+ description: the natural language description
+ discofeatures: the features supported by the command
+ initial: the initial command in the from of {"execute": commandname}
+
+ All stages set the "actions" dictionary for each session to represent the possible options available.
+ """
+ name = "examplecommand"
+ count = 0
+ description = "an example command"
+ discofeatures = [NS_COMMANDS, NS_DATA]
+
+ # This is the command template
+ def __init__(self, jid=""):
+ """
+ Set up the class.
+ """
+ PlugIn.__init__(self)
+ DBG_LINE = "command"
+ self.sessioncount = 0
+ self.sessions = {}
+ # Disco information for command list pre-formatted as a tuple
+ self.discoinfo = {
+ "ids": [{
+ "category": "automation",
+ "type": "command-node",
+ "name": self.description
+ }],
+ "features": self.discofeatures
+ }
+ self._jid = jid
+
+ def plugin(self, owner):
+ """
+ Plug command into the commands class.
+ """
+ # The owner in this instance is the Command Processor
+ self._commands = owner
+ self._owner = owner._owner
+ self._commands.addCommand(self.name, self._DiscoHandler, self.Execute, jid=self._jid)
+
+ def plugout(self):
+ """
+ Remove command from the commands class.
+ """
+ self._commands.delCommand(self.name, self._jid)
+
+ def getSessionID(self):
+ """
+ Returns an id for the command session.
+ """
+ self.count = self.count + 1
+ return "cmd-%s-%d" % (self.name, self.count)
+
+ def Execute(self, conn, request):
+ """
+ The method that handles all the commands, and routes them to the correct method for that stage.
+ """
+ # New request or old?
+ try:
+ session = request.getTagAttr("command", "sessionid")
+ except:
+ session = None
+ try:
+ action = request.getTagAttr("command", "action")
+ except:
+ action = None
+ if action == None:
+ action = "execute"
+ # Check session is in session list
+ if self.sessions.has_key(session):
+ if self.sessions[session]["jid"] == request.getFrom():
+ # Check action is vaild
+ if self.sessions[session]["actions"].has_key(action):
+ # Execute next action
+ self.sessions[session]["actions"][action](conn, request)
+ else:
+ # Stage not presented as an option
+ self._owner.send(Error(request, ERR_BAD_REQUEST))
+ raise NodeProcessed()
+ else:
+ # Jid and session don't match. Go away imposter
+ self._owner.send(Error(request, ERR_BAD_REQUEST))
+ raise NodeProcessed()
+ elif session != None:
+ # Not on this sessionid you won't.
+ self._owner.send(Error(request, ERR_BAD_REQUEST))
+ raise NodeProcessed()
+ else:
+ # New session
+ self.initial[action](conn, request)
+
+ def _DiscoHandler(self, conn, request, type):
+ """
+ The handler for discovery events.
+ """
+ if type == "list":
+ result = (request.getTo(), self.name, self.description)
+ elif type == "items":
+ result = []
+ elif type == "info":
+ result = self.discoinfo
+ return result
+
+class TestCommand(Command_Handler_Prototype):
+ """
+ Example class. You should read source if you wish to understate how it works.
+ Generally, it presents a "master" that giudes user through to calculate something.
+ """
+ name = "testcommand"
+ description = "a noddy example command"
+
+ def __init__(self, jid=""):
+ """ Init internal constants. """
+ Command_Handler_Prototype.__init__(self, jid)
+ self.initial = {"execute": self.cmdFirstStage}
+
+ def cmdFirstStage(self, conn, request):
+ """
+ Determine.
+ """
+ # This is the only place this should be repeated as all other stages should have SessionIDs
+ try:
+ session = request.getTagAttr("command", "sessionid")
+ except:
+ session = None
+ if session == None:
+ session = self.getSessionID()
+ self.sessions[session] = {
+ "jid": request.getFrom(),
+ "actions": {
+ "cancel": self.cmdCancel,
+ "next": self.cmdSecondStage,
+ "execute": self.cmdSecondStage
+ },
+ "data": {"type": None}
+ }
+ # As this is the first stage we only send a form
+ reply = request.buildReply("result")
+ form = DataForm(title="Select type of operation",
+ data=[
+ "Use the combobox to select the type of calculation you would like to do, then click Next.",
+ DataField(name="calctype", desc="Calculation Type",
+ value=self.sessions[session]["data"]["type"],
+ options=[
+ ["circlediameter", "Calculate the Diameter of a circle"],
+ ["circlearea", "Calculate the area of a circle"]
+ ],
+ typ="list-single",
+ required=1
+ )])
+ replypayload = [Node("actions", attrs={"execute": "next"}, payload=[Node("next")]), form]
+ reply.addChild(name="command",
+ namespace=NS_COMMANDS,
+ attrs={
+ "node": request.getTagAttr("command", "node"),
+ "sessionid": session,
+ "status": "executing"
+ },
+ payload=replypayload
+ )
+ self._owner.send(reply)
+ raise NodeProcessed()
+
+ def cmdSecondStage(self, conn, request):
+ form = DataForm(node=request.getTag(name="command").getTag(name="x", namespace=NS_DATA))
+ self.sessions[request.getTagAttr("command", "sessionid")]["data"]["type"] = form.getField("calctype").getValue()
+ self.sessions[request.getTagAttr("command", "sessionid")]["actions"] = {
+ "cancel": self.cmdCancel,
+ None: self.cmdThirdStage,
+ "previous": self.cmdFirstStage,
+ "execute": self.cmdThirdStage,
+ "next": self.cmdThirdStage
+ }
+ # The form generation is split out to another method as it may be called by cmdThirdStage
+ self.cmdSecondStageReply(conn, request)
+
+ def cmdSecondStageReply(self, conn, request):
+ reply = request.buildReply("result")
+ form = DataForm(title="Enter the radius",
+ data=[
+ "Enter the radius of the circle (numbers only)",
+ DataField(desc="Radius", name="radius", typ="text-single")
+ ])
+ replypayload = [
+ Node("actions",
+ attrs={"execute": "complete"},
+ payload=[Node("complete"),
+ Node("prev")]),
+ form
+ ]
+ reply.addChild(name="command",
+ namespace=NS_COMMANDS,
+ attrs={
+ "node": request.getTagAttr("command", "node"),
+ "sessionid": request.getTagAttr("command", "sessionid"),
+ "status": "executing"
+ },
+ payload=replypayload
+ )
+ self._owner.send(reply)
+ raise NodeProcessed()
+
+ def cmdThirdStage(self, conn, request):
+ form = DataForm(node=request.getTag(name="command").getTag(name="x", namespace=NS_DATA))
+ try:
+ num = float(form.getField("radius").getValue())
+ except:
+ self.cmdSecondStageReply(conn, request)
+ from math import pi
+ if self.sessions[request.getTagAttr("command", "sessionid")]["data"]["type"] == "circlearea":
+ result = (num ** 2) * pi
+ else:
+ result = num * 2 * pi
+ reply = request.buildReply("result")
+ form = DataForm(typ="result", data=[DataField(desc="result", name="result", value=result)])
+ reply.addChild(name="command",
+ namespace=NS_COMMANDS,
+ attrs={
+ "node": request.getTagAttr("command", "node"),
+ "sessionid": request.getTagAttr("command", "sessionid"),
+ "status": "completed"
+ },
+ payload=[form]
+ )
+ self._owner.send(reply)
+ raise NodeProcessed()
+
+ def cmdCancel(self, conn, request):
+ reply = request.buildReply("result")
+ reply.addChild(name="command",
+ namespace=NS_COMMANDS,
+ attrs={
+ "node": request.getTagAttr("command", "node"),
+ "sessionid": request.getTagAttr("command", "sessionid"),
+ "status": "cancelled"
+ })
+ self._owner.send(reply)
+ del self.sessions[request.getTagAttr("command", "sessionid")]
diff --git a/xmpp/debug.py b/xmpp/debug.py
new file mode 100644
index 0000000..be208c1
--- /dev/null
+++ b/xmpp/debug.py
@@ -0,0 +1,314 @@
+## debug.py
+##
+## Copyright (C) 2003 Jacob Lundqvist
+##
+## This program 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, or (at your option)
+## any later version.
+##
+## 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.
+
+# $Id: debug.py, v1.41 2013/10/21 alkorgun Exp $
+
+_version_ = "1.4.1"
+
+import os
+import sys
+import time
+
+from traceback import format_exception as traceback_format_exception
+
+colors_enabled = os.environ.has_key("TERM")
+
+color_none = chr(27) + "[0m"
+color_black = chr(27) + "[30m"
+color_red = chr(27) + "[31m"
+color_green = chr(27) + "[32m"
+color_brown = chr(27) + "[33m"
+color_blue = chr(27) + "[34m"
+color_magenta = chr(27) + "[35m"
+color_cyan = chr(27) + "[36m"
+color_light_gray = chr(27) + "[37m"
+color_dark_gray = chr(27) + "[30;1m"
+color_bright_red = chr(27) + "[31;1m"
+color_bright_green = chr(27) + "[32;1m"
+color_yellow = chr(27) + "[33;1m"
+color_bright_blue = chr(27) + "[34;1m"
+color_purple = chr(27) + "[35;1m"
+color_bright_cyan = chr(27) + "[36;1m"
+color_white = chr(27) + "[37;1m"
+
+class NoDebug:
+
+ def __init__(self, *args, **kwargs):
+ self.debug_flags = []
+
+ def show(self, *args, **kwargs):
+ pass
+
+ def Show(self, *args, **kwargs):
+ pass
+
+ def is_active(self, flag):
+ pass
+
+ colors = {}
+
+ def active_set(self, active_flags=None):
+ return 0
+
+LINE_FEED = "\n"
+
+class Debug:
+
+ def __init__(self, active_flags=None, log_file=sys.stderr, prefix="DEBUG: ", sufix="\n", time_stamp=0, flag_show=None, validate_flags=1, welcome= -1):
+ self.debug_flags = []
+ if welcome == -1:
+ if active_flags and len(active_flags):
+ welcome = 1
+ else:
+ welcome = 0
+ self._remove_dupe_flags()
+ if log_file:
+ if isinstance(log_file, str):
+ try:
+ self._fh = open(log_file, "w")
+ except:
+ print "ERROR: can open %s for writing."
+ sys.exit(0)
+ else: # assume its a stream type object
+ self._fh = log_file
+ else:
+ self._fh = sys.stdout
+ if time_stamp not in (0, 1, 2):
+ raise Exception("Invalid time_stamp param", str(time_stamp))
+ self.prefix = prefix
+ self.sufix = sufix
+ self.time_stamp = time_stamp
+ self.flag_show = None # must be initialised after possible welcome
+ self.validate_flags = validate_flags
+ self.active_set(active_flags)
+ if welcome:
+ self.show("")
+ caller = sys._getframe(1) # used to get name of caller
+ try:
+ mod_name = ":%s" % caller.f_locals["__name__"]
+ except:
+ mod_name = ""
+ self.show("Debug created for %s%s" % (caller.f_code.co_filename, mod_name))
+ self.show(" flags defined: %s" % ",".join(self.active))
+ if isinstance(flag_show, (str, type(None))):
+ self.flag_show = flag_show
+ else:
+ raise Exception("Invalid type for flag_show!", str(flag_show))
+
+ def show(self, msg, flag=None, prefix=None, sufix=None, lf=0):
+ """
+ flag can be of folowing types:
+ None - this msg will always be shown if any debugging is on
+ flag - will be shown if flag is active
+ (flag1,flag2,,,) - will be shown if any of the given flags are active
+
+ if prefix / sufix are not given, default ones from init will be used
+
+ lf = -1 means strip linefeed if pressent
+ lf = 1 means add linefeed if not pressent
+ """
+ if self.validate_flags:
+ self._validate_flag(flag)
+ if not self.is_active(flag):
+ return None
+ if prefix:
+ pre = prefix
+ else:
+ pre = self.prefix
+ if sufix:
+ suf = sufix
+ else:
+ suf = self.sufix
+ if self.time_stamp == 2:
+ output = "%s%s " % (
+ pre,
+ trftime("%b %d %H:%M:%S",
+ caltime(time.time()))
+ )
+ elif self.time_stamp == 1:
+ output = "%s %s" % (
+ time.strftime("%b %d %H:%M:%S",
+ time.localtime(time.time())),
+ pre
+ )
+ else:
+ output = pre
+ if self.flag_show:
+ if flag:
+ output = "%s%s%s" % (output, flag, self.flag_show)
+ else:
+ # this call uses the global default, dont print "None", just show the separator
+ output = "%s %s" % (output, self.flag_show)
+ output = "%s%s%s" % (output, msg, suf)
+ if lf:
+ # strip/add lf if needed
+ last_char = output[-1]
+ if lf == 1 and last_char != LINE_FEED:
+ output = output + LINE_FEED
+ elif lf == -1 and last_char == LINE_FEED:
+ output = output[:-1]
+ try:
+ self._fh.write(output)
+ except:
+ # unicode strikes again ;)
+ s = u""
+ for i in xrange(len(output)):
+ if ord(output[i]) < 128:
+ c = output[i]
+ else:
+ c = "?"
+ s = s + c
+ self._fh.write("%s%s%s" % (pre, s, suf))
+ self._fh.flush()
+
+ def is_active(self, flag):
+ """
+ If given flag(s) should generate output.
+ """
+ # try to abort early to quicken code
+ if not self.active:
+ return 0
+ if not flag or flag in self.active:
+ return 1
+ else:
+ # check for multi flag type:
+ if isinstance(flag, (list, tuple)):
+ for s in flag:
+ if s in self.active:
+ return 1
+ return 0
+
+ def active_set(self, active_flags=None):
+ """
+ Returns 1 if any flags where actually set, otherwise 0.
+ """
+ r = 0
+ ok_flags = []
+ if not active_flags:
+ # no debuging at all
+ self.active = []
+ elif isinstance(active_flags, (tuple, list)):
+ flags = self._as_one_list(active_flags)
+ for t in flags:
+ if t not in self.debug_flags:
+ sys.stderr.write("Invalid debugflag given: %s\n" % t)
+ ok_flags.append(t)
+
+ self.active = ok_flags
+ r = 1
+ else:
+ # assume comma string
+ try:
+ flags = active_flags.split(",")
+ except:
+ self.show("***")
+ self.show("*** Invalid debug param given: %s" % active_flags)
+ self.show("*** please correct your param!")
+ self.show("*** due to this, full debuging is enabled")
+ self.active = self.debug_flags
+ for f in flags:
+ s = f.strip()
+ ok_flags.append(s)
+ self.active = ok_flags
+ self._remove_dupe_flags()
+ return r
+
+ def active_get(self):
+ """
+ Returns currently active flags.
+ """
+ return self.active
+
+ def _as_one_list(self, items):
+ """
+ Init param might contain nested lists, typically from group flags.
+ This code organises lst and remves dupes.
+ """
+ if not isinstance(items, (list, tuple)):
+ return [items]
+ r = []
+ for l in items:
+ if isinstance(l, list):
+ lst2 = self._as_one_list(l)
+ for l2 in lst2:
+ self._append_unique_str(r, l2)
+ elif l == None:
+ continue
+ else:
+ self._append_unique_str(r, l)
+ return r
+
+ def _append_unique_str(self, lst, item):
+ """
+ Filter out any dupes.
+ """
+ if not isinstance(item, str):
+ raise Exception("Invalid item type (should be string)", str(item))
+ if item not in lst:
+ lst.append(item)
+ return lst
+
+ def _validate_flag(self, flags):
+ """
+ Verify that flag is defined.
+ """
+ if flags:
+ for flag in self._as_one_list(flags):
+ if not flag in self.debug_flags:
+ raise Exception("Invalid debugflag given", str(flag))
+
+ def _remove_dupe_flags(self):
+ """
+ If multiple instances of Debug is used in same app,
+ some flags might be created multiple time, filter out dupes.
+ """
+ unique_flags = []
+ for f in self.debug_flags:
+ if f not in unique_flags:
+ unique_flags.append(f)
+ self.debug_flags = unique_flags
+
+ colors = {}
+
+ def Show(self, flag, msg, prefix=""):
+ msg = msg.replace("\r", "\\r").replace("\n", "\\n").replace("><", ">\n <")
+ if not colors_enabled:
+ pass
+ elif self.colors.has_key(prefix):
+ msg = self.colors[prefix] + msg + color_none
+ else:
+ msg = color_none + msg
+ if not colors_enabled:
+ prefixcolor = ""
+ elif self.colors.has_key(flag):
+ prefixcolor = self.colors[flag]
+ else:
+ prefixcolor = color_none
+ if prefix == "error":
+ e = sys.exc_info()
+ if e[0]:
+ msg = msg + "\n" + "".join(traceback_format_exception(e[0], e[1], e[2])).rstrip()
+ prefix = self.prefix + prefixcolor + (flag + " " * 12)[:12] + " " + (prefix + " " * 6)[:6]
+ self.show(msg, flag, prefix)
+
+ def is_active(self, flag):
+ if not self.active:
+ return 0
+ if not flag or flag in self.active and DBG_ALWAYS not in self.active or flag not in self.active and DBG_ALWAYS in self.active:
+ return 1
+ return 0
+
+DBG_ALWAYS = "always"
+
+# Debug=NoDebug # Uncomment this to effectively disable all debugging and all debugging overhead.
diff --git a/xmpp/dispatcher.py b/xmpp/dispatcher.py
new file mode 100644
index 0000000..c364ba9
--- /dev/null
+++ b/xmpp/dispatcher.py
@@ -0,0 +1,477 @@
+## transports.py
+##
+## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## 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 General Public License for more details.
+
+# $Id: dispatcher.py, v1.43 2013/10/21 alkorgun Exp $
+
+"""
+Main xmpppy mechanism. Provides library with methods to assign different handlers
+to different XMPP stanzas.
+Contains one tunable attribute: DefaultTimeout (25 seconds by default). It defines time that
+Dispatcher.SendAndWaitForResponce method will wait for reply stanza before giving up.
+"""
+
+import simplexml
+import sys
+import time
+
+from plugin import PlugIn
+from protocol import *
+from xml.parsers.expat import ExpatError
+
+DefaultTimeout = 25
+ID = 0
+
+DBG_LINE = "dispatcher"
+
+class Dispatcher(PlugIn):
+ """
+ Ancestor of PlugIn class. Handles XMPP stream, i.e. aware of stream headers.
+ Can be plugged out/in to restart these headers (used for SASL f.e.).
+ """
+ def __init__(self):
+ PlugIn.__init__(self)
+ self.handlers = {}
+ self._expected = {}
+ self._defaultHandler = None
+ self._pendingExceptions = []
+ self._eventHandler = None
+ self._cycleHandlers = []
+ self._exported_methods = [
+ self.Process,
+ self.RegisterHandler,
+# self.RegisterDefaultHandler,
+ self.RegisterEventHandler,
+ self.UnregisterCycleHandler,
+ self.RegisterCycleHandler,
+ self.RegisterHandlerOnce,
+ self.UnregisterHandler,
+ self.RegisterProtocol,
+ self.WaitForResponse,
+ self.SendAndWaitForResponse,
+ self.send,
+ self.SendAndCallForResponse,
+ self.disconnect,
+ self.iter
+ ]
+
+ def dumpHandlers(self):
+ """
+ Return set of user-registered callbacks in it's internal format.
+ Used within the library to carry user handlers set over Dispatcher replugins.
+ """
+ return self.handlers
+
+ def restoreHandlers(self, handlers):
+ """
+ Restores user-registered callbacks structure from dump previously obtained via dumpHandlers.
+ Used within the library to carry user handlers set over Dispatcher replugins.
+ """
+ self.handlers = handlers
+
+ def _init(self):
+ """
+ Registers default namespaces/protocols/handlers. Used internally.
+ """
+ self.RegisterNamespace("unknown")
+ self.RegisterNamespace(NS_STREAMS)
+ self.RegisterNamespace(self._owner.defaultNamespace)
+ self.RegisterProtocol("iq", Iq)
+ self.RegisterProtocol("presence", Presence)
+ self.RegisterProtocol("message", Message)
+# self.RegisterDefaultHandler(self.returnStanzaHandler)
+ self.RegisterHandler("error", self.streamErrorHandler, xmlns=NS_STREAMS)
+
+ def plugin(self, owner):
+ """
+ Plug the Dispatcher instance into Client class instance and send initial stream header. Used internally.
+ """
+ self._init()
+ for method in self._old_owners_methods:
+ if method.__name__ == "send":
+ self._owner_send = method; break
+ self._owner.lastErrNode = None
+ self._owner.lastErr = None
+ self._owner.lastErrCode = None
+ self.StreamInit()
+
+ def plugout(self):
+ """
+ Prepares instance to be destructed.
+ """
+ self.Stream.dispatch = None
+ self.Stream.DEBUG = None
+ self.Stream.features = None
+ self.Stream.destroy()
+
+ def StreamInit(self):
+ """
+ Send an initial stream header.
+ """
+ self.Stream = simplexml.NodeBuilder()
+ self.Stream._dispatch_depth = 2
+ self.Stream.dispatch = self.dispatch
+ self.Stream.stream_header_received = self._check_stream_start
+ self._owner.debug_flags.append(simplexml.DBG_NODEBUILDER)
+ self.Stream.DEBUG = self._owner.DEBUG
+ self.Stream.features = None
+ self._metastream = Node("stream:stream")
+ self._metastream.setNamespace(self._owner.Namespace)
+ self._metastream.setAttr("version", "1.0")
+ self._metastream.setAttr("xmlns:stream", NS_STREAMS)
+ self._metastream.setAttr("to", self._owner.Server)
+ self._owner.send("<?xml version=\"1.0\"?>%s>" % str(self._metastream)[:-2])
+
+ def _check_stream_start(self, ns, tag, attrs):
+ if ns != NS_STREAMS or tag != "stream":
+ raise ValueError("Incorrect stream start: (%s,%s). Terminating." % (tag, ns))
+
+ def Process(self, timeout=8):
+ """
+ Check incoming stream for data waiting. If "timeout" is positive - block for as max. this time.
+ Returns:
+ 1) length of processed data if some data were processed;
+ 2) "0" string if no data were processed but link is alive;
+ 3) 0 (zero) if underlying connection is closed.
+ Take note that in case of disconnection detect during Process() call
+ disconnect handlers are called automatically.
+ """
+ for handler in self._cycleHandlers:
+ handler(self)
+ if self._pendingExceptions:
+ e = self._pendingExceptions.pop()
+ raise e[0], e[1], e[2]
+ if self._owner.Connection.pending_data(timeout):
+ try:
+ data = self._owner.Connection.receive()
+ except IOError:
+ return None
+ try:
+ self.Stream.Parse(data)
+ except ExpatError:
+ pass
+ if self._pendingExceptions:
+ e = self._pendingExceptions.pop()
+ raise e[0], e[1], e[2]
+ if data:
+ return len(data)
+ return "0"
+
+ def RegisterNamespace(self, xmlns, order="info"):
+ """
+ Creates internal structures for newly registered namespace.
+ You can register handlers for this namespace afterwards. By default one namespace
+ already registered (jabber:client or jabber:component:accept depending on context.
+ """
+ self.DEBUG("Registering namespace \"%s\"" % xmlns, order)
+ self.handlers[xmlns] = {}
+ self.RegisterProtocol("unknown", Protocol, xmlns=xmlns)
+ self.RegisterProtocol("default", Protocol, xmlns=xmlns)
+
+ def RegisterProtocol(self, tag_name, Proto, xmlns=None, order="info"):
+ """
+ Used to declare some top-level stanza name to dispatcher.
+ Needed to start registering handlers for such stanzas.
+ Iq, message and presence protocols are registered by default.
+ """
+ if not xmlns:
+ xmlns = self._owner.defaultNamespace
+ self.DEBUG("Registering protocol \"%s\" as %s(%s)" % (tag_name, Proto, xmlns), order)
+ self.handlers[xmlns][tag_name] = {"type": Proto, "default": []}
+
+ def RegisterNamespaceHandler(self, xmlns, handler, typ="", ns="", makefirst=0, system=0):
+ """
+ Register handler for processing all stanzas for specified namespace.
+ """
+ self.RegisterHandler("default", handler, typ, ns, xmlns, makefirst, system)
+
+ def RegisterHandler(self, name, handler, typ="", ns="", xmlns=None, makefirst=0, system=0):
+ """Register user callback as stanzas handler of declared type. Callback must take
+ (if chained, see later) arguments: dispatcher instance (for replying), incomed
+ return of previous handlers.
+ The callback must raise xmpp.NodeProcessed just before return if it want preven
+ callbacks to be called with the same stanza as argument _and_, more importantly
+ library from returning stanza to sender with error set (to be enabled in 0.2 ve
+ Arguments:
+ "name" - name of stanza. F.e. "iq".
+ "handler" - user callback.
+ "typ" - value of stanza's "type" attribute. If not specified any value match
+ "ns" - namespace of child that stanza must contain.
+ "chained" - chain together output of several handlers.
+ "makefirst" - insert handler in the beginning of handlers list instead of
+ adding it to the end. Note that more common handlers (i.e. w/o "typ" and
+ will be called first nevertheless).
+ "system" - call handler even if NodeProcessed Exception were raised already.
+ """
+ if not xmlns:
+ xmlns = self._owner.defaultNamespace
+ self.DEBUG("Registering handler %s for \"%s\" type->%s ns->%s(%s)" % (handler, name, typ, ns, xmlns), "info")
+ if not typ and not ns:
+ typ = "default"
+ if not self.handlers.has_key(xmlns):
+ self.RegisterNamespace(xmlns, "warn")
+ if not self.handlers[xmlns].has_key(name):
+ self.RegisterProtocol(name, Protocol, xmlns, "warn")
+ if not self.handlers[xmlns][name].has_key(typ + ns):
+ self.handlers[xmlns][name][typ + ns] = []
+ if makefirst:
+ self.handlers[xmlns][name][typ + ns].insert(0, {"func": handler, "system": system})
+ else:
+ self.handlers[xmlns][name][typ + ns].append({"func": handler, "system": system})
+
+ def RegisterHandlerOnce(self, name, handler, typ="", ns="", xmlns=None, makefirst=0, system=0):
+ """
+ Unregister handler after first call (not implemented yet).
+ """
+ if not xmlns:
+ xmlns = self._owner.defaultNamespace
+ self.RegisterHandler(name, handler, typ, ns, xmlns, makefirst, system)
+
+ def UnregisterHandler(self, name, handler, typ="", ns="", xmlns=None):
+ """
+ Unregister handler. "typ" and "ns" must be specified exactly the same as with registering.
+ """
+ if not xmlns:
+ xmlns = self._owner.defaultNamespace
+ if not self.handlers.has_key(xmlns):
+ return None
+ if not typ and not ns:
+ typ = "default"
+ for pack in self.handlers[xmlns][name][typ + ns]:
+ if handler == pack["func"]:
+ break
+ else:
+ pack = None
+ try:
+ self.handlers[xmlns][name][typ + ns].remove(pack)
+ except ValueError:
+ pass
+
+ def RegisterDefaultHandler(self, handler):
+ """
+ Specify the handler that will be used if no NodeProcessed exception were raised.
+ This is returnStanzaHandler by default.
+ """
+ self._defaultHandler = handler
+
+ def RegisterEventHandler(self, handler):
+ """
+ Register handler that will process events. F.e. "FILERECEIVED" event.
+ """
+ self._eventHandler = handler
+
+ def returnStanzaHandler(self, conn, stanza):
+ """
+ Return stanza back to the sender with <feature-not-implemennted/> error set.
+ """
+ if stanza.getType() in ("get", "set"):
+ conn.send(Error(stanza, ERR_FEATURE_NOT_IMPLEMENTED))
+
+ def streamErrorHandler(self, conn, error):
+ name, text = "error", error.getData()
+ for tag in error.getChildren():
+ if tag.getNamespace() == NS_XMPP_STREAMS:
+ if tag.getName() == "text":
+ text = tag.getData()
+ else:
+ name = tag.getName()
+ if name in stream_exceptions.keys():
+ exc = stream_exceptions[name]
+ else:
+ exc = StreamError
+ raise exc((name, text))
+
+ def RegisterCycleHandler(self, handler):
+ """
+ Register handler that will be called on every Dispatcher.Process() call.
+ """
+ if handler not in self._cycleHandlers:
+ self._cycleHandlers.append(handler)
+
+ def UnregisterCycleHandler(self, handler):
+ """
+ Unregister handler that will is called on every Dispatcher.Process() call.
+ """
+ if handler in self._cycleHandlers:
+ self._cycleHandlers.remove(handler)
+
+ def Event(self, realm, event, data):
+ """
+ Raise some event. Takes three arguments:
+ 1) "realm" - scope of event. Usually a namespace.
+ 2) "event" - the event itself. F.e. "SUCESSFULL SEND".
+ 3) data that comes along with event. Depends on event.
+ """
+ if self._eventHandler:
+ self._eventHandler(realm, event, data)
+
+ def dispatch(self, stanza, session=None, direct=0):
+ """
+ Main procedure that performs XMPP stanza recognition and calling apppropriate handlers for it.
+ Called internally.
+ """
+ if not session:
+ session = self
+ session.Stream._mini_dom = None
+ name = stanza.getName()
+ if not direct and self._owner._route:
+ if name == "route":
+ if stanza.getAttr("error") == None:
+ if len(stanza.getChildren()) == 1:
+ stanza = stanza.getChildren()[0]
+ name = stanza.getName()
+ else:
+ for each in stanza.getChildren():
+ self.dispatch(each, session, direct=1)
+ return None
+ elif name == "presence":
+ return None
+ elif name in ("features", "bind"):
+ pass
+ else:
+ raise UnsupportedStanzaType(name)
+ if name == "features":
+ session.Stream.features = stanza
+ xmlns = stanza.getNamespace()
+ if xmlns not in self.handlers:
+ self.DEBUG("Unknown namespace: " + xmlns, "warn")
+ xmlns = "unknown"
+ if name not in self.handlers[xmlns]:
+ self.DEBUG("Unknown stanza: " + name, "warn")
+ name = "unknown"
+ else:
+ self.DEBUG("Got %s/%s stanza" % (xmlns, name), "ok")
+ if isinstance(stanza, Node):
+ stanza = self.handlers[xmlns][name]["type"](node=stanza)
+ typ = stanza.getType()
+ if not typ:
+ typ = ""
+ stanza.props = stanza.getProperties()
+ ID = stanza.getID()
+ session.DEBUG("Dispatching %s stanza with type->%s props->%s id->%s" % (name, typ, stanza.props, ID), "ok")
+ ls = ["default"] # we will use all handlers:
+ if typ in self.handlers[xmlns][name]:
+ ls.append(typ) # from very common...
+ for prop in stanza.props:
+ if prop in self.handlers[xmlns][name]:
+ ls.append(prop)
+ if typ and (typ + prop) in self.handlers[xmlns][name]:
+ ls.append(typ + prop) # ...to very particular
+ chain = self.handlers[xmlns]["default"]["default"]
+ for key in ls:
+ if key:
+ chain = chain + self.handlers[xmlns][name][key]
+ output = ""
+ if ID in session._expected:
+ user = 0
+ if isinstance(session._expected[ID], tuple):
+ cb, args = session._expected.pop(ID)
+ session.DEBUG("Expected stanza arrived. Callback %s(%s) found!" % (cb, args), "ok")
+ try:
+ cb(session, stanza, **args)
+ except NodeProcessed:
+ pass
+ else:
+ session.DEBUG("Expected stanza arrived!", "ok")
+ session._expected[ID] = stanza
+ else:
+ user = 1
+ for handler in chain:
+ if user or handler["system"]:
+ try:
+ handler["func"](session, stanza)
+ except NodeProcessed:
+ user = 0
+ except:
+ self._pendingExceptions.insert(0, sys.exc_info())
+ if user and self._defaultHandler:
+ self._defaultHandler(session, stanza)
+
+ def WaitForResponse(self, ID, timeout=DefaultTimeout):
+ """
+ Block and wait until stanza with specific "id" attribute will come.
+ If no such stanza is arrived within timeout, return None.
+ If operation failed for some reason then owner's attributes
+ lastErrNode, lastErr and lastErrCode are set accordingly.
+ """
+ self._expected[ID] = None
+ abort_time = time.time() + timeout
+ self.DEBUG("Waiting for ID:%s with timeout %s..." % (ID, timeout), "wait")
+ while not self._expected[ID]:
+ if not self.Process(0.04):
+ self._owner.lastErr = "Disconnect"
+ return None
+ if time.time() > abort_time:
+ self._owner.lastErr = "Timeout"
+ return None
+ resp = self._expected.pop(ID)
+ if resp.getErrorCode():
+ self._owner.lastErrNode = resp
+ self._owner.lastErr = resp.getError()
+ self._owner.lastErrCode = resp.getErrorCode()
+ return resp
+
+ def SendAndWaitForResponse(self, stanza, timeout=DefaultTimeout):
+ """
+ Put stanza on the wire and wait for recipient's response to it.
+ """
+ return self.WaitForResponse(self.send(stanza), timeout)
+
+ def SendAndCallForResponse(self, stanza, func, args={}):
+ """
+ Put stanza on the wire and call back when recipient replies.
+ Additional callback arguments can be specified in args.
+ """
+ self._expected[self.send(stanza)] = (func, args)
+
+ def send(self, stanza):
+ """
+ Serialize stanza and put it on the wire. Assign an unique ID to it before send.
+ Returns assigned ID.
+ """
+ if isinstance(stanza, basestring):
+ return self._owner_send(stanza)
+ if not isinstance(stanza, Protocol):
+ id = None
+ elif not stanza.getID():
+ global ID
+ ID += 1
+ id = repr(ID)
+ stanza.setID(id)
+ else:
+ id = stanza.getID()
+ if self._owner._registered_name and not stanza.getAttr("from"):
+ stanza.setAttr("from", self._owner._registered_name)
+ if self._owner._route and stanza.getName() != "bind":
+ to = self._owner.Server
+ if stanza.getTo() and stanza.getTo().getDomain():
+ to = stanza.getTo().getDomain()
+ frm = stanza.getFrom()
+ if frm.getDomain():
+ frm = frm.getDomain()
+ route = Protocol("route", to=to, frm=frm, payload=[stanza])
+ stanza = route
+ stanza.setNamespace(self._owner.Namespace)
+ stanza.setParent(self._metastream)
+ self._owner_send(stanza)
+ return id
+
+ def disconnect(self):
+ """
+ Send a stream terminator and and handle all incoming stanzas before stream closure.
+ """
+ self._owner_send("</stream:stream>")
+ while self.Process(1):
+ pass
+
+ iter = type(send)(Process.func_code, Process.func_globals, name = "iter", argdefs = Process.func_defaults, closure = Process.func_closure)
diff --git a/xmpp/features.py b/xmpp/features.py
new file mode 100644
index 0000000..99088f7
--- /dev/null
+++ b/xmpp/features.py
@@ -0,0 +1,230 @@
+## features.py
+##
+## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## 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 General Public License for more details.
+
+# $Id: features.py, v1.26 2013/10/21 alkorgun Exp $
+
+"""
+This module contains variable stuff that is not worth splitting into separate modules.
+Here is:
+ DISCO client and agents-to-DISCO and browse-to-DISCO emulators.
+ IBR and password manager.
+ jabber:iq:privacy methods
+All these methods takes "disp" first argument that should be already connected
+(and in most cases already authorised) dispatcher instance.
+"""
+
+from protocol import *
+
+REGISTER_DATA_RECEIVED = "REGISTER DATA RECEIVED"
+
+def _discover(disp, ns, jid, node=None, fb2b=0, fb2a=1):
+ """
+ Try to obtain info from the remote object.
+ If remote object doesn't support disco fall back to browse (if fb2b is true)
+ and if it doesnt support browse (or fb2b is not true) fall back to agents protocol
+ (if gb2a is true). Returns obtained info. Used internally.
+ """
+ iq = Iq(to=jid, typ="get", queryNS=ns)
+ if node:
+ iq.setQuerynode(node)
+ rep = disp.SendAndWaitForResponse(iq)
+ if fb2b and not isResultNode(rep):
+ rep = disp.SendAndWaitForResponse(Iq(to=jid, typ="get", queryNS=NS_BROWSE)) # Fallback to browse
+ if fb2a and not isResultNode(rep):
+ rep = disp.SendAndWaitForResponse(Iq(to=jid, typ="get", queryNS=NS_AGENTS)) # Fallback to agents
+ if isResultNode(rep):
+ return [n for n in rep.getQueryPayload() if isinstance(n, Node)]
+ return []
+
+def discoverItems(disp, jid, node=None):
+ """
+ Query remote object about any items that it contains. Return items list.
+ """
+ ret = []
+ for i in _discover(disp, NS_DISCO_ITEMS, jid, node):
+ if i.getName() == "agent" and i.getTag("name"):
+ i.setAttr("name", i.getTagData("name"))
+ ret.append(i.attrs)
+ return ret
+
+def discoverInfo(disp, jid, node=None):
+ """
+ Query remote object about info that it publishes. Returns identities and features lists.
+ """
+ identities, features = [], []
+ for i in _discover(disp, NS_DISCO_INFO, jid, node):
+ if i.getName() == "identity":
+ identities.append(i.attrs)
+ elif i.getName() == "feature":
+ features.append(i.getAttr("var"))
+ elif i.getName() == "agent":
+ if i.getTag("name"):
+ i.setAttr("name", i.getTagData("name"))
+ if i.getTag("description"):
+ i.setAttr("name", i.getTagData("description"))
+ identities.append(i.attrs)
+ if i.getTag("groupchat"):
+ features.append(NS_GROUPCHAT)
+ if i.getTag("register"):
+ features.append(NS_REGISTER)
+ if i.getTag("search"):
+ features.append(NS_SEARCH)
+ return identities, features
+
+def getRegInfo(disp, host, info={}, sync=True):
+ """
+ Gets registration form from remote host.
+ You can pre-fill the info dictionary.
+ F.e. if you are requesting info on registering user joey than specify
+ info as {"username": "joey"}. See JEP-0077 for details.
+ "disp" must be connected dispatcher instance.
+ """
+ iq = Iq("get", NS_REGISTER, to=host)
+ for i in info.keys():
+ iq.setTagData(i, info[i])
+ if sync:
+ resp = disp.SendAndWaitForResponse(iq)
+ _ReceivedRegInfo(disp.Dispatcher, resp, host)
+ return resp
+ else:
+ disp.SendAndCallForResponse(iq, _ReceivedRegInfo, {"agent": host})
+
+def _ReceivedRegInfo(con, resp, agent):
+ iq = Iq("get", NS_REGISTER, to=agent)
+ if not isResultNode(resp):
+ return None
+ df = resp.getTag("query", namespace=NS_REGISTER).getTag("x", namespace=NS_DATA)
+ if df:
+ con.Event(NS_REGISTER, REGISTER_DATA_RECEIVED, (agent, DataForm(node=df)))
+ return None
+ df = DataForm(typ="form")
+ for i in resp.getQueryPayload():
+ if not isinstance(i, Iq):
+ pass
+ elif i.getName() == "instructions":
+ df.addInstructions(i.getData())
+ else:
+ df.setField(i.getName()).setValue(i.getData())
+ con.Event(NS_REGISTER, REGISTER_DATA_RECEIVED, (agent, df))
+
+def register(disp, host, info):
+ """
+ Perform registration on remote server with provided info.
+ disp must be connected dispatcher instance.
+ Returns true or false depending on registration result.
+ If registration fails you can get additional info from the dispatcher's owner
+ attributes lastErrNode, lastErr and lastErrCode.
+ """
+ iq = Iq("set", NS_REGISTER, to=host)
+ if not isinstance(info, dict):
+ info = info.asDict()
+ for i in info.keys():
+ iq.setTag("query").setTagData(i, info[i])
+ resp = disp.SendAndWaitForResponse(iq)
+ if isResultNode(resp):
+ return 1
+
+def unregister(disp, host):
+ """
+ Unregisters with host (permanently removes account).
+ disp must be connected and authorized dispatcher instance.
+ Returns true on success.
+ """
+ resp = disp.SendAndWaitForResponse(Iq("set", NS_REGISTER, to=host, payload=[Node("remove")]))
+ if isResultNode(resp):
+ return 1
+
+def changePasswordTo(disp, newpassword, host=None):
+ """
+ Changes password on specified or current (if not specified) server.
+ disp must be connected and authorized dispatcher instance.
+ Returns true on success."""
+ if not host:
+ host = disp._owner.Server
+ resp = disp.SendAndWaitForResponse(Iq("set", NS_REGISTER, to=host,
+ payload=[
+ Node("username", payload=[disp._owner.User]),
+ Node("password", payload=[newpassword])
+ ]))
+ if isResultNode(resp):
+ return 1
+
+def getPrivacyLists(disp):
+ """
+ Requests privacy lists from connected server.
+ Returns dictionary of existing lists on success.
+ """
+ dict = {"lists": []}
+ try:
+ resp = disp.SendAndWaitForResponse(Iq("get", NS_PRIVACY))
+ if not isResultNode(resp):
+ return None
+ for list in resp.getQueryPayload():
+ if list.getName() == "list":
+ dict["lists"].append(list.getAttr("name"))
+ else:
+ dict[list.getName()] = list.getAttr("name")
+ except:
+ pass
+ else:
+ return dict
+
+def getPrivacyList(disp, listname):
+ """
+ Requests specific privacy list listname. Returns list of XML nodes (rules)
+ taken from the server responce.
+ """
+ try:
+ resp = disp.SendAndWaitForResponse(Iq("get", NS_PRIVACY, payload=[Node("list", {"name": listname})]))
+ if isResultNode(resp):
+ return resp.getQueryPayload()[0]
+ except:
+ pass
+
+def setActivePrivacyList(disp, listname=None, typ="active"):
+ """
+ Switches privacy list "listname" to specified type.
+ By default the type is "active". Returns true on success.
+ """
+ if listname:
+ attrs = {"name": listname}
+ else:
+ attrs = {}
+ resp = disp.SendAndWaitForResponse(Iq("set", NS_PRIVACY, payload=[Node(typ, attrs)]))
+ if isResultNode(resp):
+ return 1
+
+def setDefaultPrivacyList(disp, listname=None):
+ """
+ Sets the default privacy list as "listname". Returns true on success.
+ """
+ return setActivePrivacyList(disp, listname, "default")
+
+def setPrivacyList(disp, list):
+ """
+ Set the ruleset. "list" should be the simpleXML node formatted
+ according to RFC 3921 (XMPP-IM) (I.e. Node("list", {"name": listname}, payload=[...]) )
+ Returns true on success.
+ """
+ resp = disp.SendAndWaitForResponse(Iq("set", NS_PRIVACY, payload=[list]))
+ if isResultNode(resp):
+ return 1
+
+def delPrivacyList(disp, listname):
+ """
+ Deletes privacy list "listname". Returns true on success.
+ """
+ resp = disp.SendAndWaitForResponse(Iq("set", NS_PRIVACY, payload=[Node("list", {"name": listname})]))
+ if isResultNode(resp):
+ return 1
diff --git a/xmpp/filetransfer.py b/xmpp/filetransfer.py
new file mode 100644
index 0000000..fc938ab
--- /dev/null
+++ b/xmpp/filetransfer.py
@@ -0,0 +1,226 @@
+## filetransfer.py
+##
+## Copyright (C) 2004 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## 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 General Public License for more details.
+
+# $Id: filetransfer.py, v1.7 2013/10/21 alkorgun Exp $
+
+"""
+This module contains IBB class that is the simple implementation of JEP-0047.
+Note that this is just a transport for data. You have to negotiate data transfer before
+(via StreamInitiation most probably). Unfortunately SI is not implemented yet.
+"""
+
+from base64 import encodestring, decodestring
+from dispatcher import PlugIn
+from protocol import *
+
+class IBB(PlugIn):
+ """
+ IBB used to transfer small-sized data chunk over estabilished xmpp connection.
+ Data is split into small blocks (by default 3000 bytes each), encoded as base 64
+ and sent to another entity that compiles these blocks back into the data chunk.
+ This is very inefficiend but should work under any circumstances. Note that
+ using IBB normally should be the last resort.
+ """
+ def __init__(self):
+ """
+ Initialise internal variables.
+ """
+ PlugIn.__init__(self)
+ self.DBG_LINE = "ibb"
+ self._exported_methods = [self.OpenStream]
+ self._streams = {}
+ self._ampnode = Node(NS_AMP + " amp",
+ payload=[
+ Node("rule", {"condition": "deliver-at", "value": "stored", "action": "error"}),
+ Node("rule", {"condition": "match-resource", "value": "exact", "action": "error"})
+ ])
+
+ def plugin(self, owner):
+ """
+ Register handlers for receiving incoming datastreams. Used internally.
+ """
+ self._owner.RegisterHandlerOnce("iq", self.StreamOpenReplyHandler)
+ self._owner.RegisterHandler("iq", self.IqHandler, ns=NS_IBB)
+ self._owner.RegisterHandler("message", self.ReceiveHandler, ns=NS_IBB)
+
+ def IqHandler(self, conn, stanza):
+ """
+ Handles streams state change. Used internally.
+ """
+ typ = stanza.getType()
+ self.DEBUG("IqHandler called typ->%s" % typ, "info")
+ if typ == "set" and stanza.getTag("open", namespace=NS_IBB):
+ self.StreamOpenHandler(conn, stanza)
+ elif typ == "set" and stanza.getTag("close", namespace=NS_IBB):
+ self.StreamCloseHandler(conn, stanza)
+ elif typ == "result":
+ self.StreamCommitHandler(conn, stanza)
+ elif typ == "error":
+ self.StreamOpenReplyHandler(conn, stanza)
+ else:
+ conn.send(Error(stanza, ERR_BAD_REQUEST))
+ raise NodeProcessed()
+
+ def StreamOpenHandler(self, conn, stanza):
+ """
+ Handles opening of new incoming stream. Used internally.
+ """
+ err = None
+ sid = stanza.getTagAttr("open", "sid")
+ blocksize = stanza.getTagAttr("open", "block-size")
+ self.DEBUG("StreamOpenHandler called sid->%s blocksize->%s" % (sid, blocksize), "info")
+ try:
+ blocksize = int(blocksize)
+ except:
+ err = ERR_BAD_REQUEST
+ if not sid or not blocksize:
+ err = ERR_BAD_REQUEST
+ elif sid in self._streams.keys():
+ err = ERR_UNEXPECTED_REQUEST
+ if err:
+ rep = Error(stanza, err)
+ else:
+ self.DEBUG("Opening stream: id %s, block-size %s" % (sid, blocksize), "info")
+ rep = Protocol("iq", stanza.getFrom(), "result", stanza.getTo(), {"id": stanza.getID()})
+ self._streams[sid] = {
+ "direction": "<" + str(stanza.getFrom()),
+ "block-size": blocksize,
+ "fp": open("/tmp/xmpp_file_" + sid, "w"),
+ "seq": 0,
+ "syn_id": stanza.getID()
+ }
+ conn.send(rep)
+
+ def OpenStream(self, sid, to, fp, blocksize=3000):
+ """
+ Start new stream. You should provide stream id "sid", the endpoind jid "to",
+ the file object containing info for send "fp". Also the desired blocksize can be specified.
+ Take into account that recommended stanza size is 4k and IBB uses base64 encoding
+ that increases size of data by 1/3.
+ """
+ if sid in self._streams.keys():
+ return None
+ if not JID(to).getResource():
+ return None
+ self._streams[sid] = {"direction": "|>" + to, "block-size": blocksize, "fp": fp, "seq": 0}
+ self._owner.RegisterCycleHandler(self.SendHandler)
+ syn = Protocol("iq", to, "set", payload=[Node(NS_IBB + " open", {"sid": sid, "block-size": blocksize})])
+ self._owner.send(syn)
+ self._streams[sid]["syn_id"] = syn.getID()
+ return self._streams[sid]
+
+ def SendHandler(self, conn):
+ """
+ Send next portion of data if it is time to do it. Used internally.
+ """
+ self.DEBUG("SendHandler called", "info")
+ for sid in self._streams.keys():
+ stream = self._streams[sid]
+ if stream["direction"][:2] == "|>":
+ cont = 1
+ elif stream["direction"][0] == ">":
+ chunk = stream["fp"].read(stream["block-size"])
+ if chunk:
+ datanode = Node(NS_IBB + " data", {"sid": sid, "seq": stream["seq"]}, encodestring(chunk))
+ stream["seq"] += 1
+ if stream["seq"] == 65536:
+ stream["seq"] = 0
+ conn.send(Protocol("message", stream["direction"][1:], payload=[datanode, self._ampnode]))
+ else:
+ conn.send(Protocol("iq", stream["direction"][1:], "set", payload=[Node(NS_IBB + " close", {"sid": sid})]))
+ conn.Event(self.DBG_LINE, "SUCCESSFULL SEND", stream)
+ del self._streams[sid]
+ self._owner.UnregisterCycleHandler(self.SendHandler)
+
+ def ReceiveHandler(self, conn, stanza):
+ """
+ Receive next portion of incoming datastream and store it write
+ it to temporary file. Used internally.
+ """
+ sid, seq, data = stanza.getTagAttr("data", "sid"), stanza.getTagAttr("data", "seq"), stanza.getTagData("data")
+ self.DEBUG("ReceiveHandler called sid->%s seq->%s" % (sid, seq), "info")
+ try:
+ seq = int(seq)
+ data = decodestring(data)
+ except:
+ seq = data = ""
+ err = None
+ if not sid in self._streams.keys():
+ err = ERR_ITEM_NOT_FOUND
+ else:
+ stream = self._streams[sid]
+ if not data:
+ err = ERR_BAD_REQUEST
+ elif seq != stream["seq"]:
+ err = ERR_UNEXPECTED_REQUEST
+ else:
+ self.DEBUG("Successfull receive sid->%s %s+%s bytes" % (sid, stream["fp"].tell(), len(data)), "ok")
+ stream["seq"] += 1
+ stream["fp"].write(data)
+ if err:
+ self.DEBUG("Error on receive: %s" % err, "error")
+ conn.send(Error(Iq(to=stanza.getFrom(), frm=stanza.getTo(), payload=[Node(NS_IBB + " close")]), err, reply=0))
+
+ def StreamCloseHandler(self, conn, stanza):
+ """
+ Handle stream closure due to all data transmitted.
+ Raise xmpppy event specifying successfull data receive.
+ """
+ sid = stanza.getTagAttr("close", "sid")
+ self.DEBUG("StreamCloseHandler called sid->%s" % sid, "info")
+ if sid in self._streams.keys():
+ conn.send(stanza.buildReply("result"))
+ conn.Event(self.DBG_LINE, "SUCCESSFULL RECEIVE", self._streams[sid])
+ del self._streams[sid]
+ else:
+ conn.send(Error(stanza, ERR_ITEM_NOT_FOUND))
+
+ def StreamBrokenHandler(self, conn, stanza):
+ """
+ Handle stream closure due to all some error while receiving data.
+ Raise xmpppy event specifying unsuccessfull data receive.
+ """
+ syn_id = stanza.getID()
+ self.DEBUG("StreamBrokenHandler called syn_id->%s" % syn_id, "info")
+ for sid in self._streams.keys():
+ stream = self._streams[sid]
+ if stream["syn_id"] == syn_id:
+ if stream["direction"][0] == "<":
+ conn.Event(self.DBG_LINE, "ERROR ON RECEIVE", stream)
+ else:
+ conn.Event(self.DBG_LINE, "ERROR ON SEND", stream)
+ del self._streams[sid]
+
+ def StreamOpenReplyHandler(self, conn, stanza):
+ """
+ Handle remote side reply about is it agree or not to receive our datastream.
+ Used internally. Raises xmpppy event specfiying if the data transfer is agreed upon.
+ """
+ syn_id = stanza.getID()
+ self.DEBUG("StreamOpenReplyHandler called syn_id->%s" % syn_id, "info")
+ for sid in self._streams.keys():
+ stream = self._streams[sid]
+ if stream["syn_id"] == syn_id:
+ if stanza.getType() == "error":
+ if stream["direction"][0] == "<":
+ conn.Event(self.DBG_LINE, "ERROR ON RECEIVE", stream)
+ else:
+ conn.Event(self.DBG_LINE, "ERROR ON SEND", stream)
+ del self._streams[sid]
+ elif stanza.getType() == "result":
+ if stream["direction"][0] == "|":
+ stream["direction"] = stream["direction"][1:]
+ conn.Event(self.DBG_LINE, "STREAM COMMITTED", stream)
+ else:
+ conn.send(Error(stanza, ERR_UNEXPECTED_REQUEST))
diff --git a/xmpp/plugin.py b/xmpp/plugin.py
new file mode 100644
index 0000000..badb9e2
--- /dev/null
+++ b/xmpp/plugin.py
@@ -0,0 +1,69 @@
+## plugin.py
+##
+## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## 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 General Public License for more details.
+
+# $Id: plugin.py, v1.0 2013/10/21 alkorgun Exp $
+
+"""
+Provides library with all Non-SASL and SASL authentication mechanisms.
+Can be used both for client and transport authentication.
+"""
+
+class PlugIn:
+ """
+ Common xmpppy plugins infrastructure: plugging in/out, debugging.
+ """
+ def __init__(self):
+ self._exported_methods = []
+ self.DBG_LINE = self.__class__.__name__.lower()
+
+ def PlugIn(self, owner):
+ """
+ Attach to main instance and register ourself and all our staff in it.
+ """
+ self._owner = owner
+ if self.DBG_LINE not in owner.debug_flags:
+ owner.debug_flags.append(self.DBG_LINE)
+ self.DEBUG("Plugging %s into %s" % (self, self._owner), "start")
+ if owner.__dict__.has_key(self.__class__.__name__):
+ return self.DEBUG("Plugging ignored: another instance already plugged.", "error")
+ self._old_owners_methods = []
+ for method in self._exported_methods:
+ if owner.__dict__.has_key(method.__name__):
+ self._old_owners_methods.append(owner.__dict__[method.__name__])
+ owner.__dict__[method.__name__] = method
+ owner.__dict__[self.__class__.__name__] = self
+ if self.__class__.__dict__.has_key("plugin"):
+ return self.plugin(owner)
+
+ def PlugOut(self):
+ """
+ Unregister all our staff from main instance and detach from it.
+ """
+ self.DEBUG("Plugging %s out of %s." % (self, self._owner), "stop")
+ ret = None
+ if self.__class__.__dict__.has_key("plugout"):
+ ret = self.plugout()
+ self._owner.debug_flags.remove(self.DBG_LINE)
+ for method in self._exported_methods:
+ del self._owner.__dict__[method.__name__]
+ for method in self._old_owners_methods:
+ self._owner.__dict__[method.__name__] = method
+ del self._owner.__dict__[self.__class__.__name__]
+ return ret
+
+ def DEBUG(self, text, severity="info"):
+ """
+ Feed a provided debug line to main instance's debug facility along with our ID string.
+ """
+ self._owner.DEBUG(self.DBG_LINE, text, severity)
diff --git a/xmpp/protocol.py b/xmpp/protocol.py
new file mode 100644
index 0000000..e9927b3
--- /dev/null
+++ b/xmpp/protocol.py
@@ -0,0 +1,1404 @@
+## protocol.py
+##
+## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## 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 General Public License for more details.
+
+# $Id: protocol.py, v1.63 2013/12/06 alkorgun Exp $
+
+"""
+Protocol module contains tools that is needed for processing of
+xmpp-related data structures.
+"""
+
+from simplexml import Node, XML_ls, XMLescape, ustr
+
+import time
+
+NS_ACTIVITY = "http://jabber.org/protocol/activity" # XEP-0108
+NS_ADDRESS = "http://jabber.org/protocol/address" # XEP-0033
+NS_ADMIN = "http://jabber.org/protocol/admin" # XEP-0133
+NS_ADMIN_ADD_USER = NS_ADMIN + "#add-user" # XEP-0133
+NS_ADMIN_DELETE_USER = NS_ADMIN + "#delete-user" # XEP-0133
+NS_ADMIN_DISABLE_USER = NS_ADMIN + "#disable-user" # XEP-0133
+NS_ADMIN_REENABLE_USER = NS_ADMIN + "#reenable-user" # XEP-0133
+NS_ADMIN_END_USER_SESSION = NS_ADMIN + "#end-user-session" # XEP-0133
+NS_ADMIN_GET_USER_PASSWORD = NS_ADMIN + "#get-user-password" # XEP-0133
+NS_ADMIN_CHANGE_USER_PASSWORD = NS_ADMIN + "#change-user-password" # XEP-0133
+NS_ADMIN_GET_USER_ROSTER = NS_ADMIN + "#get-user-roster" # XEP-0133
+NS_ADMIN_GET_USER_LASTLOGIN = NS_ADMIN + "#get-user-lastlogin" # XEP-0133
+NS_ADMIN_USER_STATS = NS_ADMIN + "#user-stats" # XEP-0133
+NS_ADMIN_EDIT_BLACKLIST = NS_ADMIN + "#edit-blacklist" # XEP-0133
+NS_ADMIN_EDIT_WHITELIST = NS_ADMIN + "#edit-whitelist" # XEP-0133
+NS_ADMIN_REGISTERED_USERS_NUM = NS_ADMIN + "#get-registered-users-num" # XEP-0133
+NS_ADMIN_DISABLED_USERS_NUM = NS_ADMIN + "#get-disabled-users-num" # XEP-0133
+NS_ADMIN_ONLINE_USERS_NUM = NS_ADMIN + "#get-online-users-num" # XEP-0133
+NS_ADMIN_ACTIVE_USERS_NUM = NS_ADMIN + "#get-active-users-num" # XEP-0133
+NS_ADMIN_IDLE_USERS_NUM = NS_ADMIN + "#get-idle-users-num" # XEP-0133
+NS_ADMIN_REGISTERED_USERS_LIST = NS_ADMIN + "#get-registered-users-list" # XEP-0133
+NS_ADMIN_DISABLED_USERS_LIST = NS_ADMIN + "#get-disabled-users-list" # XEP-0133
+NS_ADMIN_ONLINE_USERS_LIST = NS_ADMIN + "#get-online-users-list" # XEP-0133
+NS_ADMIN_ACTIVE_USERS_LIST = NS_ADMIN + "#get-active-users-list" # XEP-0133
+NS_ADMIN_IDLE_USERS_LIST = NS_ADMIN + "#get-idle-users-list" # XEP-0133
+NS_ADMIN_ANNOUNCE = NS_ADMIN + "#announce" # XEP-0133
+NS_ADMIN_SET_MOTD = NS_ADMIN + "#set-motd" # XEP-0133
+NS_ADMIN_EDIT_MOTD = NS_ADMIN + "#edit-motd" # XEP-0133
+NS_ADMIN_DELETE_MOTD = NS_ADMIN + "#delete-motd" # XEP-0133
+NS_ADMIN_SET_WELCOME = NS_ADMIN + "#set-welcome" # XEP-0133
+NS_ADMIN_DELETE_WELCOME = NS_ADMIN + "#delete-welcome" # XEP-0133
+NS_ADMIN_EDIT_ADMIN = NS_ADMIN + "#edit-admin" # XEP-0133
+NS_ADMIN_RESTART = NS_ADMIN + "#restart" # XEP-0133
+NS_ADMIN_SHUTDOWN = NS_ADMIN + "#shutdown" # XEP-0133
+NS_AGENTS = "jabber:iq:agents" # XEP-0094 (historical)
+NS_AMP = "http://jabber.org/protocol/amp" # XEP-0079
+NS_AMP_ERRORS = NS_AMP + "#errors" # XEP-0079
+NS_AUTH = "jabber:iq:auth" # XEP-0078
+NS_AVATAR = "jabber:iq:avatar" # XEP-0008 (historical)
+NS_BIND = "urn:ietf:params:xml:ns:xmpp-bind" # RFC 3920
+NS_BROWSE = "jabber:iq:browse" # XEP-0011 (historical)
+NS_BYTESTREAM = "http://jabber.org/protocol/bytestreams" # XEP-0065
+NS_CAPS = "http://jabber.org/protocol/caps" # XEP-0115
+NS_CAPTCHA = "urn:xmpp:captcha" # XEP-0158
+NS_CHATSTATES = "http://jabber.org/protocol/chatstates" # XEP-0085
+NS_CLIENT = "jabber:client" # RFC 3921
+NS_COMMANDS = "http://jabber.org/protocol/commands" # XEP-0050
+NS_COMPONENT_ACCEPT = "jabber:component:accept" # XEP-0114
+NS_COMPONENT_1 = "http://jabberd.jabberstudio.org/ns/component/1.0" # Jabberd2
+NS_COMPRESS = "http://jabber.org/protocol/compress" # XEP-0138
+NS_DATA = "jabber:x:data" # XEP-0004
+NS_DATA_LAYOUT = "http://jabber.org/protocol/xdata-layout" # XEP-0141
+NS_DATA_VALIDATE = "http://jabber.org/protocol/xdata-validate" # XEP-0122
+NS_DELAY = "jabber:x:delay" # XEP-0091 (deprecated)
+NS_DIALBACK = "jabber:server:dialback" # RFC 3921
+NS_DISCO = "http://jabber.org/protocol/disco" # XEP-0030
+NS_DISCO_INFO = NS_DISCO + "#info" # XEP-0030
+NS_DISCO_ITEMS = NS_DISCO + "#items" # XEP-0030
+NS_ENCRYPTED = "jabber:x:encrypted" # XEP-0027
+NS_EVENT = "jabber:x:event" # XEP-0022 (deprecated)
+NS_FEATURE = "http://jabber.org/protocol/feature-neg" # XEP-0020
+NS_FILE = "http://jabber.org/protocol/si/profile/file-transfer" # XEP-0096
+NS_GATEWAY = "jabber:iq:gateway" # XEP-0100
+NS_GEOLOC = "http://jabber.org/protocol/geoloc" # XEP-0080
+NS_GROUPCHAT = "gc-1.0" # XEP-0045
+NS_HTTP_BIND = "http://jabber.org/protocol/httpbind" # XEP-0124
+NS_IBB = "http://jabber.org/protocol/ibb" # XEP-0047
+NS_INVISIBLE = "presence-invisible" # Jabberd2
+NS_IQ = "iq" # Jabberd2
+NS_LAST = "jabber:iq:last" # XEP-0012
+NS_MEDIA = "urn:xmpp:media-element" # XEP-0158
+NS_MESSAGE = "message" # Jabberd2
+NS_MOOD = "http://jabber.org/protocol/mood" # XEP-0107
+NS_MUC = "http://jabber.org/protocol/muc" # XEP-0045
+NS_MUC_ADMIN = NS_MUC + "#admin" # XEP-0045
+NS_MUC_OWNER = NS_MUC + "#owner" # XEP-0045
+NS_MUC_UNIQUE = NS_MUC + "#unique" # XEP-0045
+NS_MUC_USER = NS_MUC + "#user" # XEP-0045
+NS_MUC_REGISTER = NS_MUC + "#register" # XEP-0045
+NS_MUC_REQUEST = NS_MUC + "#request" # XEP-0045
+NS_MUC_ROOMCONFIG = NS_MUC + "#roomconfig" # XEP-0045
+NS_MUC_ROOMINFO = NS_MUC + "#roominfo" # XEP-0045
+NS_MUC_ROOMS = NS_MUC + "#rooms" # XEP-0045
+NS_MUC_TRAFIC = NS_MUC + "#traffic" # XEP-0045
+NS_NICK = "http://jabber.org/protocol/nick" # XEP-0172
+NS_OFFLINE = "http://jabber.org/protocol/offline" # XEP-0013
+NS_PHYSLOC = "http://jabber.org/protocol/physloc" # XEP-0112
+NS_PRESENCE = "presence" # Jabberd2
+NS_PRIVACY = "jabber:iq:privacy" # RFC 3921
+NS_PRIVATE = "jabber:iq:private" # XEP-0049
+NS_PUBSUB = "http://jabber.org/protocol/pubsub" # XEP-0060
+NS_REGISTER = "jabber:iq:register" # XEP-0077
+NS_RC = "http://jabber.org/protocol/rc" # XEP-0146
+NS_ROSTER = "jabber:iq:roster" # RFC 3921
+NS_ROSTERX = "http://jabber.org/protocol/rosterx" # XEP-0144
+NS_RPC = "jabber:iq:rpc" # XEP-0009
+NS_SASL = "urn:ietf:params:xml:ns:xmpp-sasl" # RFC 3920
+NS_SEARCH = "jabber:iq:search" # XEP-0055
+NS_SERVER = "jabber:server" # RFC 3921
+NS_SESSION = "urn:ietf:params:xml:ns:xmpp-session" # RFC 3921
+NS_SI = "http://jabber.org/protocol/si" # XEP-0096
+NS_SI_PUB = "http://jabber.org/protocol/sipub" # XEP-0137
+NS_SIGNED = "jabber:x:signed" # XEP-0027
+NS_STANZAS = "urn:ietf:params:xml:ns:xmpp-stanzas" # RFC 3920
+NS_STREAMS = "http://etherx.jabber.org/streams" # RFC 3920
+NS_TIME = "jabber:iq:time" # XEP-0090 (deprecated)
+NS_TLS = "urn:ietf:params:xml:ns:xmpp-tls" # RFC 3920
+NS_VACATION = "http://jabber.org/protocol/vacation" # XEP-0109
+NS_VCARD = "vcard-temp" # XEP-0054
+NS_VCARD_UPDATE = "vcard-temp:x:update" # XEP-0153
+NS_VERSION = "jabber:iq:version" # XEP-0092
+NS_WAITINGLIST = "http://jabber.org/protocol/waitinglist" # XEP-0130
+NS_XHTML_IM = "http://jabber.org/protocol/xhtml-im" # XEP-0071
+NS_XMPP_STREAMS = "urn:ietf:params:xml:ns:xmpp-streams" # RFC 3920
+NS_STATS = "http://jabber.org/protocol/stats" # XEP-0039
+NS_PING = "urn:xmpp:ping" # XEP-0199
+NS_MUC_FILTER = "http://jabber.ru/muc-filter"
+NS_URN_TIME = "urn:xmpp:time" # XEP-0202
+NS_RECEIPTS = "urn:xmpp:receipts" # XEP-0184
+NS_OOB = "jabber:x:oob" # XEP-0066
+NS_URN_ATTENTION = "urn:xmpp:attention:0" # XEP-0224
+NS_URN_OOB = "urn:xmpp:bob" # XEP-0158
+
+STREAM_NOT_AUTHORIZED = NS_XMPP_STREAMS + " not-authorized"
+STREAM_REMOTE_CONNECTION_FAILED = NS_XMPP_STREAMS + " remote-connection-failed"
+SASL_MECHANISM_TOO_WEAK = NS_SASL + " mechanism-too-weak"
+STREAM_XML_NOT_WELL_FORMED = NS_XMPP_STREAMS + " xml-not-well-formed"
+ERR_JID_MALFORMED = NS_STANZAS + " jid-malformed"
+STREAM_SEE_OTHER_HOST = NS_XMPP_STREAMS + " see-other-host"
+STREAM_BAD_NAMESPACE_PREFIX = NS_XMPP_STREAMS + " bad-namespace-prefix"
+ERR_SERVICE_UNAVAILABLE = NS_STANZAS + " service-unavailable"
+STREAM_CONNECTION_TIMEOUT = NS_XMPP_STREAMS + " connection-timeout"
+STREAM_UNSUPPORTED_VERSION = NS_XMPP_STREAMS + " unsupported-version"
+STREAM_IMPROPER_ADDRESSING = NS_XMPP_STREAMS + " improper-addressing"
+STREAM_UNDEFINED_CONDITION = NS_XMPP_STREAMS + " undefined-condition"
+SASL_NOT_AUTHORIZED = NS_SASL + " not-authorized"
+ERR_GONE = NS_STANZAS + " gone"
+SASL_TEMPORARY_AUTH_FAILURE = NS_SASL + " temporary-auth-failure"
+ERR_REMOTE_SERVER_NOT_FOUND = NS_STANZAS + " remote-server-not-found"
+ERR_UNEXPECTED_REQUEST = NS_STANZAS + " unexpected-request"
+ERR_RECIPIENT_UNAVAILABLE = NS_STANZAS + " recipient-unavailable"
+ERR_CONFLICT = NS_STANZAS + " conflict"
+STREAM_SYSTEM_SHUTDOWN = NS_XMPP_STREAMS + " system-shutdown"
+STREAM_BAD_FORMAT = NS_XMPP_STREAMS + " bad-format"
+ERR_SUBSCRIPTION_REQUIRED = NS_STANZAS + " subscription-required"
+STREAM_INTERNAL_SERVER_ERROR = NS_XMPP_STREAMS + " internal-server-error"
+ERR_NOT_AUTHORIZED = NS_STANZAS + " not-authorized"
+SASL_ABORTED = NS_SASL + " aborted"
+ERR_REGISTRATION_REQUIRED = NS_STANZAS + " registration-required"
+ERR_INTERNAL_SERVER_ERROR = NS_STANZAS + " internal-server-error"
+SASL_INCORRECT_ENCODING = NS_SASL + " incorrect-encoding"
+STREAM_HOST_GONE = NS_XMPP_STREAMS + " host-gone"
+STREAM_POLICY_VIOLATION = NS_XMPP_STREAMS + " policy-violation"
+STREAM_INVALID_XML = NS_XMPP_STREAMS + " invalid-xml"
+STREAM_CONFLICT = NS_XMPP_STREAMS + " conflict"
+STREAM_RESOURCE_CONSTRAINT = NS_XMPP_STREAMS + " resource-constraint"
+STREAM_UNSUPPORTED_ENCODING = NS_XMPP_STREAMS + " unsupported-encoding"
+ERR_NOT_ALLOWED = NS_STANZAS + " not-allowed"
+ERR_ITEM_NOT_FOUND = NS_STANZAS + " item-not-found"
+ERR_NOT_ACCEPTABLE = NS_STANZAS + " not-acceptable"
+STREAM_INVALID_FROM = NS_XMPP_STREAMS + " invalid-from"
+ERR_FEATURE_NOT_IMPLEMENTED = NS_STANZAS + " feature-not-implemented"
+ERR_BAD_REQUEST = NS_STANZAS + " bad-request"
+STREAM_INVALID_ID = NS_XMPP_STREAMS + " invalid-id"
+STREAM_HOST_UNKNOWN = NS_XMPP_STREAMS + " host-unknown"
+ERR_UNDEFINED_CONDITION = NS_STANZAS + " undefined-condition"
+SASL_INVALID_MECHANISM = NS_SASL + " invalid-mechanism"
+STREAM_RESTRICTED_XML = NS_XMPP_STREAMS + " restricted-xml"
+ERR_RESOURCE_CONSTRAINT = NS_STANZAS + " resource-constraint"
+ERR_REMOTE_SERVER_TIMEOUT = NS_STANZAS + " remote-server-timeout"
+SASL_INVALID_AUTHZID = NS_SASL + " invalid-authzid"
+ERR_PAYMENT_REQUIRED = NS_STANZAS + " payment-required"
+STREAM_INVALID_NAMESPACE = NS_XMPP_STREAMS + " invalid-namespace"
+ERR_REDIRECT = NS_STANZAS + " redirect"
+STREAM_UNSUPPORTED_STANZA_TYPE = NS_XMPP_STREAMS + " unsupported-stanza-type"
+ERR_FORBIDDEN = NS_STANZAS + " forbidden"
+
+ERRORS = {
+ "urn:ietf:params:xml:ns:xmpp-sasl not-authorized": ["", "", "The authentication failed because the initiating entity did not provide valid credentials (this includes but is not limited to the case of an unknown username); sent in reply to a <response/> element or an <auth/> element with initial response data."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas payment-required": ["402", "auth", "The requesting entity is not authorized to access the requested service because payment is required."],
+ "urn:ietf:params:xml:ns:xmpp-sasl mechanism-too-weak": ["", "", "The mechanism requested by the initiating entity is weaker than server policy permits for that initiating entity; sent in reply to a <response/> element or an <auth/> element with initial response data."],
+ "urn:ietf:params:xml:ns:xmpp-streams unsupported-encoding": ["", "", "The initiating entity has encoded the stream in an encoding that is not supported by the server."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas remote-server-timeout": ["504", "wait", "A remote server or service specified as part or all of the JID of the intended recipient could not be contacted within a reasonable amount of time."],
+ "urn:ietf:params:xml:ns:xmpp-streams remote-connection-failed": ["", "", "The server is unable to properly connect to a remote resource that is required for authentication or authorization."],
+ "urn:ietf:params:xml:ns:xmpp-streams restricted-xml": ["", "", "The entity has attempted to send restricted XML features such as a comment, processing instruction, DTD, entity reference, or unescaped character."],
+ "urn:ietf:params:xml:ns:xmpp-streams see-other-host": ["", "", "The server will not provide service to the initiating entity but is redirecting traffic to another host."],
+ "urn:ietf:params:xml:ns:xmpp-streams xml-not-well-formed": ["", "", "The initiating entity has sent XML that is not well-formed."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas subscription-required": ["407", "auth", "The requesting entity is not authorized to access the requested service because a subscription is required."],
+ "urn:ietf:params:xml:ns:xmpp-streams internal-server-error": ["", "", "The server has experienced a misconfiguration or an otherwise-undefined internal error that prevents it from servicing the stream."],
+ "urn:ietf:params:xml:ns:xmpp-sasl invalid-mechanism": ["", "", "The initiating entity did not provide a mechanism or requested a mechanism that is not supported by the receiving entity; sent in reply to an <auth/> element."],
+ "urn:ietf:params:xml:ns:xmpp-streams policy-violation": ["", "", "The entity has violated some local service policy."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas conflict": ["409", "cancel", "Access cannot be granted because an existing resource or session exists with the same name or address."],
+ "urn:ietf:params:xml:ns:xmpp-streams unsupported-stanza-type": ["", "", "The initiating entity has sent a first-level child of the stream that is not supported by the server."],
+ "urn:ietf:params:xml:ns:xmpp-sasl incorrect-encoding": ["", "", "The data provided by the initiating entity could not be processed because the [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003. encoding is incorrect (e.g., because the encoding does not adhere to the definition in Section 3 of [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003.); sent in reply to a <response/> element or an <auth/> element with initial response data."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas registration-required": ["407", "auth", "The requesting entity is not authorized to access the requested service because registration is required."],
+ "urn:ietf:params:xml:ns:xmpp-streams invalid-id": ["", "", "The stream ID or dialback ID is invalid or does not match an ID previously provided."],
+ "urn:ietf:params:xml:ns:xmpp-sasl invalid-authzid": ["", "", "The authzid provided by the initiating entity is invalid, either because it is incorrectly formatted or because the initiating entity does not have permissions to authorize that ID; sent in reply to a <response/> element or an <auth/> element with initial response data."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas bad-request": ["400", "modify", "The sender has sent XML that is malformed or that cannot be processed."],
+ "urn:ietf:params:xml:ns:xmpp-streams not-authorized": ["", "", "The entity has attempted to send data before the stream has been authenticated, or otherwise is not authorized to perform an action related to stream negotiation."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas forbidden": ["403", "auth", "The requesting entity does not possess the required permissions to perform the action."],
+ "urn:ietf:params:xml:ns:xmpp-sasl temporary-auth-failure": ["", "", "The authentication failed because of a temporary error condition within the receiving entity; sent in reply to an <auth/> element or <response/> element."],
+ "urn:ietf:params:xml:ns:xmpp-streams invalid-namespace": ["", "", "The streams namespace name is something other than \http://etherx.jabber.org/streams\" or the dialback namespace name is something other than \"jabber:server:dialback\"."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas feature-not-implemented": ["501", "cancel", "The feature requested is not implemented by the recipient or server and therefore cannot be processed."],
+ "urn:ietf:params:xml:ns:xmpp-streams invalid-xml": ["", "", "The entity has sent invalid XML over the stream to a server that performs validation."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas item-not-found": ["404", "cancel", "The addressed JID or item requested cannot be found."],
+ "urn:ietf:params:xml:ns:xmpp-streams host-gone": ["", "", "The value of the \"to\" attribute provided by the initiating entity in the stream header corresponds to a hostname that is no longer hosted by the server."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas recipient-unavailable": ["404", "wait", "The intended recipient is temporarily unavailable."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas not-acceptable": ["406", "cancel", "The recipient or server understands the request but is refusing to process it because it does not meet criteria defined by the recipient or server."],
+ "urn:ietf:params:xml:ns:xmpp-streams invalid-from": ["cancel", "", "The JID or hostname provided in a \"from\" address does not match an authorized JID or validated domain negotiated between servers via SASL or dialback, or between a client and a server via authentication and resource authorization."],
+ "urn:ietf:params:xml:ns:xmpp-streams bad-format": ["", "", "The entity has sent XML that cannot be processed."],
+ "urn:ietf:params:xml:ns:xmpp-streams resource-constraint": ["", "", "The server lacks the system resources necessary to service the stream."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas undefined-condition": ["500", "", ""],
+ "urn:ietf:params:xml:ns:xmpp-stanzas redirect": ["302", "modify", "The recipient or server is redirecting requests for this information to another entity."],
+ "urn:ietf:params:xml:ns:xmpp-streams bad-namespace-prefix": ["", "", "The entity has sent a namespace prefix that is unsupported, or has sent no namespace prefix on an element that requires such a prefix."],
+ "urn:ietf:params:xml:ns:xmpp-streams system-shutdown": ["", "", "The server is being shut down and all active streams are being closed."],
+ "urn:ietf:params:xml:ns:xmpp-streams conflict": ["", "", "The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the existing stream."],
+ "urn:ietf:params:xml:ns:xmpp-streams connection-timeout": ["", "", "The entity has not generated any traffic over the stream for some period of time."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas jid-malformed": ["400", "modify", "The value of the \"to\" attribute in the sender's stanza does not adhere to the syntax defined in Addressing Scheme."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas resource-constraint": ["500", "wait", "The server or recipient lacks the system resources necessary to service the request."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas remote-server-not-found": ["404", "cancel", "A remote server or service specified as part or all of the JID of the intended recipient does not exist."],
+ "urn:ietf:params:xml:ns:xmpp-streams unsupported-version": ["", "", "The value of the \"version\" attribute provided by the initiating entity in the stream header specifies a version of XMPP that is not supported by the server."],
+ "urn:ietf:params:xml:ns:xmpp-streams host-unknown": ["", "", "The value of the \"to\" attribute provided by the initiating entity in the stream header does not correspond to a hostname that is hosted by the server."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas unexpected-request": ["400", "wait", "The recipient or server understood the request but was not expecting it at this time (e.g., the request was out of order)."],
+ "urn:ietf:params:xml:ns:xmpp-streams improper-addressing": ["", "", "A stanza sent between two servers lacks a \"to\" or \"from\" attribute (or the attribute has no value)."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas not-allowed": ["405", "cancel", "The recipient or server does not allow any entity to perform the action."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas internal-server-error": ["500", "wait", "The server could not process the stanza because of a misconfiguration or an otherwise-undefined internal server error."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas gone": ["302", "modify", "The recipient or server can no longer be contacted at this address."],
+ "urn:ietf:params:xml:ns:xmpp-streams undefined-condition": ["", "", "The error condition is not one of those defined by the other conditions in this list."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas service-unavailable": ["503", "cancel", "The server or recipient does not currently provide the requested service."],
+ "urn:ietf:params:xml:ns:xmpp-stanzas not-authorized": ["401", "auth", "The sender must provide proper credentials before being allowed to perform the action, or has provided improper credentials."],
+ "urn:ietf:params:xml:ns:xmpp-sasl aborted": ["", "", "The receiving entity acknowledges an <abort/> element sent by the initiating entity; sent in reply to the <abort/> element."]
+}
+
+_errorcodes = {
+ "302": "redirect",
+ "400": "unexpected-request",
+ "401": "not-authorized",
+ "402": "payment-required",
+ "403": "forbidden",
+ "404": "remote-server-not-found",
+ "405": "not-allowed",
+ "406": "not-acceptable",
+ "407": "subscription-required",
+ "409": "conflict",
+ "500": "undefined-condition",
+ "501": "feature-not-implemented",
+ "503": "service-unavailable",
+ "504": "remote-server-timeout"
+}
+
+def isResultNode(node):
+ """
+ Returns true if the node is a positive reply.
+ """
+ return (node and node.getType() == "result")
+
+def isGetNode(node):
+ """
+ Returns true if the node is a positive reply.
+ """
+ return (node and node.getType() == "get")
+
+def isSetNode(node):
+ """
+ Returns true if the node is a positive reply.
+ """
+ return (node and node.getType() == "set")
+
+def isErrorNode(node):
+ """
+ Returns true if the node is a negative reply.
+ """
+ return (node and node.getType() == "error")
+
+class NodeProcessed(Exception):
+ """
+ Exception that should be raised by handler when the handling should be stopped.
+ """
+
+class StreamError(Exception):
+ """
+ Base exception class for stream errors.
+ """
+
+class BadFormat(StreamError): pass
+
+class BadNamespacePrefix(StreamError): pass
+
+class Conflict(StreamError): pass
+
+class ConnectionTimeout(StreamError): pass
+
+class HostGone(StreamError): pass
+
+class HostUnknown(StreamError): pass
+
+class ImproperAddressing(StreamError): pass
+
+class InternalServerError(StreamError): pass
+
+class InvalidFrom(StreamError): pass
+
+class InvalidID(StreamError): pass
+
+class InvalidNamespace(StreamError): pass
+
+class InvalidXML(StreamError): pass
+
+class NotAuthorized(StreamError): pass
+
+class PolicyViolation(StreamError): pass
+
+class RemoteConnectionFailed(StreamError): pass
+
+class ResourceConstraint(StreamError): pass
+
+class RestrictedXML(StreamError): pass
+
+class SeeOtherHost(StreamError): pass
+
+class SystemShutdown(StreamError): pass
+
+class UndefinedCondition(StreamError): pass
+
+class UnsupportedEncoding(StreamError): pass
+
+class UnsupportedStanzaType(StreamError): pass
+
+class UnsupportedVersion(StreamError): pass
+
+class XMLNotWellFormed(StreamError): pass
+
+stream_exceptions = {
+ "bad-format": BadFormat,
+ "bad-namespace-prefix": BadNamespacePrefix,
+ "conflict": Conflict,
+ "connection-timeout": ConnectionTimeout,
+ "host-gone": HostGone,
+ "host-unknown": HostUnknown,
+ "improper-addressing": ImproperAddressing,
+ "internal-server-error": InternalServerError,
+ "invalid-from": InvalidFrom,
+ "invalid-id": InvalidID,
+ "invalid-namespace": InvalidNamespace,
+ "invalid-xml": InvalidXML,
+ "not-authorized": NotAuthorized,
+ "policy-violation": PolicyViolation,
+ "remote-connection-failed": RemoteConnectionFailed,
+ "resource-constraint": ResourceConstraint,
+ "restricted-xml": RestrictedXML,
+ "see-other-host": SeeOtherHost,
+ "system-shutdown": SystemShutdown,
+ "undefined-condition": UndefinedCondition,
+ "unsupported-encoding": UnsupportedEncoding,
+ "unsupported-stanza-type": UnsupportedStanzaType,
+ "unsupported-version": UnsupportedVersion,
+ "xml-not-well-formed": XMLNotWellFormed
+}
+
+class JID:
+ """
+ JID object. JID can be built from string, modified, compared, serialized into string.
+ """
+ def __init__(self, jid=None, node="", domain="", resource=""):
+ """
+ Constructor. JID can be specified as string (jid argument) or as separate parts.
+ Examples:
+ JID("node@domain/resource")
+ JID(node="node", domain="domain.org")
+ """
+ if not jid and not domain:
+ raise ValueError("JID must contain at least domain name")
+ elif isinstance(jid, self.__class__):
+ self.node, self.domain, self.resource = jid.node, jid.domain, jid.resource
+ elif domain:
+ self.node, self.domain, self.resource = node, domain, resource
+ else:
+ if jid.find("@") + 1:
+ self.node, jid = jid.split("@", 1)
+ else:
+ self.node = ""
+ if jid.find("/") + 1:
+ self.domain, self.resource = jid.split("/", 1)
+ else:
+ self.domain, self.resource = jid, ""
+
+ def getNode(self):
+ """
+ Return the node part of the JID.
+ """
+ return self.node
+
+ def setNode(self, node):
+ """
+ Set the node part of the JID to new value. Specify None to remove the node part.
+ """
+ self.node = node.lower()
+
+ def getDomain(self):
+ """
+ Return the domain part of the JID.
+ """
+ return self.domain
+
+ def setDomain(self, domain):
+ """
+ Set the domain part of the JID to new value.
+ """
+ self.domain = domain.lower()
+
+ def getResource(self):
+ """
+ Return the resource part of the JID.
+ """
+ return self.resource
+
+ def setResource(self, resource):
+ """
+ Set the resource part of the JID to new value. Specify None to remove the resource part.
+ """
+ self.resource = resource
+
+ def getStripped(self):
+ """
+ Return the bare representation of JID. I.e. string value w/o resource.
+ """
+ return self.__str__(0)
+
+ def __eq__(self, other):
+ """
+ Compare the JID to another instance or to string for equality.
+ """
+ try:
+ other = JID(other)
+ except ValueError:
+ return False
+ return self.resource == other.resource and self.__str__(0) == other.__str__(0)
+
+ def __ne__(self, other):
+ """
+ Compare the JID to another instance or to string for non-equality.
+ """
+ return not self.__eq__(other)
+
+ def bareMatch(self, other):
+ """
+ Compare the node and domain parts of the JID's for equality.
+ """
+ return self.__str__(0) == JID(other).__str__(0)
+
+ def __str__(self, wresource=1):
+ """
+ Serialize JID into string.
+ """
+ jid = "@".join((self.node, self.domain)) if self.node else self.domain
+ if wresource and self.resource:
+ jid = "/".join((jid, self.resource))
+ return jid
+
+ def __hash__(self):
+ """
+ Produce hash of the JID, Allows to use JID objects as keys of the dictionary.
+ """
+ return hash(self.__str__())
+
+class Protocol(Node):
+ """
+ A "stanza" object class. Contains methods that are common for presences, iqs and messages.
+ """
+ def __init__(self, name=None, to=None, typ=None, frm=None, attrs={}, payload=[], timestamp=None, xmlns=None, node=None):
+ """
+ Constructor, name is the name of the stanza i.e. "message" or "presence" or "iq".
+ to is the value of "to" attribure, "typ" - "type" attribute
+ frn - from attribure, attrs - other attributes mapping, payload - same meaning as for simplexml payload definition
+ timestamp - the time value that needs to be stamped over stanza
+ xmlns - namespace of top stanza node
+ node - parsed or unparsed stana to be taken as prototype.
+ """
+ if not attrs:
+ attrs = {}
+ if to:
+ attrs["to"] = to
+ if frm:
+ attrs["from"] = frm
+ if typ:
+ attrs["type"] = typ
+ Node.__init__(self, tag=name, attrs=attrs, payload=payload, node=node)
+ if not node and xmlns:
+ self.setNamespace(xmlns)
+ if self["to"]:
+ self.setTo(self["to"])
+ if self["from"]:
+ self.setFrom(self["from"])
+ if node and isinstance(node, self.__class__) and self.__class__ == node.__class__ and self.attrs.has_key("id"):
+ del self.attrs["id"]
+ self.timestamp = None
+ for x in self.getTags("x", namespace=NS_DELAY):
+ try:
+ if not self.getTimestamp() or x.getAttr("stamp") < self.getTimestamp():
+ self.setTimestamp(x.getAttr("stamp"))
+ except:
+ pass
+ if timestamp is not None:
+ self.setTimestamp(timestamp) # To auto-timestamp stanza just pass timestamp=""
+
+ def getTo(self):
+ """
+ Return value of the "to" attribute.
+ """
+ try:
+ to = self["to"]
+ except:
+ to = None
+ return to
+
+ def getFrom(self):
+ """
+ Return value of the "from" attribute.
+ """
+ try:
+ frm = self["from"]
+ except:
+ frm = None
+ return frm
+
+ def getTimestamp(self):
+ """
+ Return the timestamp in the "yyyymmddThhmmss" format.
+ """
+ return self.timestamp
+
+ def getID(self):
+ """
+ Return the value of the "id" attribute.
+ """
+ return self.getAttr("id")
+
+ def setTo(self, val):
+ """
+ Set the value of the "to" attribute.
+ """
+ self.setAttr("to", JID(val))
+
+ def getType(self):
+ """
+ Return the value of the "type" attribute.
+ """
+ return self.getAttr("type")
+
+ def setFrom(self, val):
+ """
+ Set the value of the "from" attribute.
+ """
+ self.setAttr("from", JID(val))
+
+ def setType(self, val):
+ """
+ Set the value of the "type" attribute.
+ """
+ self.setAttr("type", val)
+
+ def setID(self, val):
+ """
+ Set the value of the "id" attribute.
+ """
+ self.setAttr("id", val)
+
+ def getError(self):
+ """
+ Return the error-condition (if present) or the textual description of the error (otherwise).
+ """
+ errtag = self.getTag("error")
+ if errtag:
+ for tag in errtag.getChildren():
+ if tag.getName() != "text":
+ return tag.getName()
+ return errtag.getData()
+
+ def getErrorCode(self):
+ """
+ Return the error code. Obsolette.
+ """
+ return self.getTagAttr("error", "code")
+
+ def setError(self, error, code=None):
+ """
+ Set the error code. Obsolette. Use error-conditions instead.
+ """
+ if code:
+ if str(code) in _errorcodes.keys():
+ error = ErrorNode(_errorcodes[str(code)], text=error)
+ else:
+ error = ErrorNode(ERR_UNDEFINED_CONDITION, code=code, typ="cancel", text=error)
+ elif isinstance(error, basestring):
+ error = ErrorNode(error)
+ self.setType("error")
+ self.addChild(node=error)
+
+ def setTimestamp(self, val=None):
+ """
+ Set the timestamp. timestamp should be the yyyymmddThhmmss string.
+ """
+ if not val:
+ val = time.strftime("%Y%m%dT%H:%M:%S", time.gmtime())
+ self.timestamp = val
+ self.setTag("x", {"stamp": self.timestamp}, namespace=NS_DELAY)
+
+ def getProperties(self):
+ """
+ Return the list of namespaces to which belongs the direct childs of element.
+ """
+ props = []
+ for child in self.getChildren():
+ prop = child.getNamespace()
+ if prop not in props:
+ props.append(prop)
+ return props
+
+ def __setitem__(self, item, val):
+ """
+ Set the item "item" to the value "val".
+ """
+ if item in ["to", "from"]:
+ val = JID(val)
+ return self.setAttr(item, val)
+
+class Message(Protocol):
+ """
+ XMPP Message stanza - "push" mechanism.
+ """
+ def __init__(self, to=None, body=None, typ=None, subject=None, attrs={}, frm=None, payload=[], timestamp=None, xmlns=NS_CLIENT, node=None):
+ """
+ Create message object. You can specify recipient, text of message, type of message
+ any additional attributes, sender of the message, any additional payload (f.e. jabber:x:delay element) and namespace in one go.
+ Alternatively you can pass in the other XML object as the "node" parameted to replicate it as message.
+ """
+ Protocol.__init__(self, "message", to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node)
+ if body:
+ self.setBody(body)
+ if subject:
+ self.setSubject(subject)
+
+ def getBody(self):
+ """
+ Returns text of the message.
+ """
+ return self.getTagData("body")
+
+ def getSubject(self):
+ """
+ Returns subject of the message.
+ """
+ return self.getTagData("subject")
+
+ def getThread(self):
+ """
+ Returns thread of the message.
+ """
+ return self.getTagData("thread")
+
+ def setBody(self, val):
+ """
+ Sets the text of the message.
+ """
+ self.setTagData("body", val)
+
+ def setSubject(self, val):
+ """
+ Sets the subject of the message.
+ """
+ self.setTagData("subject", val)
+
+ def setThread(self, val):
+ """
+ Sets the thread of the message.
+ """
+ self.setTagData("thread", val)
+
+ def buildReply(self, text=None):
+ """
+ Builds and returns another message object with specified text.
+ The to, from and thread properties of new message are pre-set as reply to this message.
+ """
+ msg = Message(to=self.getFrom(), frm=self.getTo(), body=text)
+ thr = self.getThread()
+ if thr:
+ msg.setThread(thr)
+ return msg
+
+class Presence(Protocol):
+ """
+ XMPP Presence object.
+ """
+ def __init__(self, to=None, typ=None, priority=None, show=None, status=None, attrs={}, frm=None, timestamp=None, payload=[], xmlns=NS_CLIENT, node=None):
+ """
+ Create presence object. You can specify recipient, type of message, priority, show and status values
+ any additional attributes, sender of the presence, timestamp, any additional payload (f.e. jabber:x:delay element) and namespace in one go.
+ Alternatively you can pass in the other XML object as the "node" parameted to replicate it as presence.
+ """
+ Protocol.__init__(self, "presence", to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node)
+ if priority:
+ self.setPriority(priority)
+ if show:
+ self.setShow(show)
+ if status:
+ self.setStatus(status)
+
+ def getPriority(self):
+ """
+ Returns the priority of the message.
+ """
+ return self.getTagData("priority")
+
+ def getShow(self):
+ """
+ Returns the show value of the message.
+ """
+ return self.getTagData("show")
+
+ def getStatus(self):
+ """
+ Returns the status string of the message.
+ """
+ return self.getTagData("status")
+
+ def setPriority(self, val):
+ """
+ Sets the priority of the message.
+ """
+ self.setTagData("priority", val)
+
+ def setShow(self, val):
+ """
+ Sets the show value of the message.
+ """
+ self.setTagData("show", val)
+
+ def setStatus(self, val):
+ """
+ Sets the status string of the message.
+ """
+ self.setTagData("status", val)
+
+ def _muc_getItemAttr(self, tag, attr):
+ for xtag in self.getTags("x", namespace=NS_MUC_USER):
+ for child in xtag.getTags(tag):
+ return child.getAttr(attr)
+
+ def _muc_getSubTagDataAttr(self, tag, attr):
+ for xtag in self.getTags("x", namespace=NS_MUC_USER):
+ for child in xtag.getTags("item"):
+ for cchild in child.getTags(tag):
+ return cchild.getData(), cchild.getAttr(attr)
+ return None, None
+
+ def getRole(self):
+ """
+ Returns the presence role (for groupchat).
+ """
+ return self._muc_getItemAttr("item", "role")
+
+ def getAffiliation(self):
+ """Returns the presence affiliation (for groupchat).
+ """
+ return self._muc_getItemAttr("item", "affiliation")
+
+ def getNick(self):
+ """
+ Returns the nick value (for nick change in groupchat).
+ """
+ return self._muc_getItemAttr("item", "nick")
+
+ def getJid(self):
+ """
+ Returns the presence jid (for groupchat).
+ """
+ return self._muc_getItemAttr("item", "jid")
+
+ def getReason(self):
+ """
+ Returns the reason of the presence (for groupchat).
+ """
+ return self._muc_getSubTagDataAttr("reason", "")[0]
+
+ def getActor(self):
+ """
+ Returns the reason of the presence (for groupchat).
+ """
+ return self._muc_getSubTagDataAttr("actor", "jid")[1]
+
+ def getStatusCode(self):
+ """
+ Returns the status code of the presence (for groupchat).
+ """
+ return self._muc_getItemAttr("status", "code")
+
+class Iq(Protocol):
+ """
+ XMPP Iq object - get/set dialog mechanism.
+ """
+ def __init__(self, typ=None, queryNS=None, attrs={}, to=None, frm=None, payload=[], xmlns=NS_CLIENT, node=None):
+ """
+ Create Iq object. You can specify type, query namespace
+ any additional attributes, recipient of the iq, sender of the iq, any additional payload (f.e. jabber:x:data node) and namespace in one go.
+ Alternatively you can pass in the other XML object as the "node" parameted to replicate it as an iq.
+ """
+ Protocol.__init__(self, "iq", to=to, typ=typ, attrs=attrs, frm=frm, xmlns=xmlns, node=node)
+ if payload:
+ self.setQueryPayload(payload)
+ if queryNS:
+ self.setQueryNS(queryNS)
+
+ def getQueryNS(self):
+ """
+ Return the namespace of the "query" child element.
+ """
+ tag = self.getTag("query")
+ if tag:
+ return tag.getNamespace()
+
+ def getQuerynode(self):
+ """
+ Return the "node" attribute value of the "query" child element.
+ """
+ return self.getTagAttr("query", "node")
+
+ def getQueryPayload(self):
+ """
+ Return the "query" child element payload.
+ """
+ tag = self.getTag("query")
+ if tag:
+ return tag.getPayload()
+
+ def getQueryChildren(self):
+ """
+ Return the "query" child element child nodes.
+ """
+ tag = self.getTag("query")
+ if tag:
+ return tag.getChildren()
+
+ def setQueryNS(self, namespace):
+ """
+ Set the namespace of the "query" child element.
+ """
+ self.setTag("query").setNamespace(namespace)
+
+ def setQueryPayload(self, payload):
+ """
+ Set the "query" child element payload.
+ """
+ self.setTag("query").setPayload(payload)
+
+ def setQuerynode(self, node):
+ """
+ Set the "node" attribute value of the "query" child element.
+ """
+ self.setTagAttr("query", "node", node)
+
+ def buildReply(self, typ):
+ """
+ Builds and returns another Iq object of specified type.
+ The to, from and query child node of new Iq are pre-set as reply to this Iq.
+ """
+ iq = Iq(typ, to=self.getFrom(), frm=self.getTo(), attrs={"id": self.getID()})
+ if self.getTag("query"):
+ iq.setQueryNS(self.getQueryNS())
+ return iq
+
+class ErrorNode(Node):
+ """
+ XMPP-style error element.
+ In the case of stanza error should be attached to XMPP stanza.
+ In the case of stream-level errors should be used separately.
+ """
+ def __init__(self, name, code=None, typ=None, text=None):
+ """
+ Create new error node object.
+ Mandatory parameter: name - name of error condition.
+ Optional parameters: code, typ, text. Used for backwards compartibility with older jabber protocol.
+ """
+ if ERRORS.has_key(name):
+ cod, type, txt = ERRORS[name]
+ ns = name.split()[0]
+ else:
+ cod, ns, type, txt = "500", NS_STANZAS, "cancel", ""
+ if typ:
+ type = typ
+ if code:
+ cod = code
+ if text:
+ txt = text
+ Node.__init__(self, "error", {}, [Node(name)])
+ if type:
+ self.setAttr("type", type)
+ if not cod:
+ self.setName("stream:error")
+ if txt:
+ self.addChild(node=Node(ns + " text", {}, [txt]))
+ if cod:
+ self.setAttr("code", cod)
+
+class Error(Protocol):
+ """
+ Used to quickly transform received stanza into error reply.
+ """
+ def __init__(self, node, error, reply=1):
+ """
+ Create error reply basing on the received "node" stanza and the "error" error condition.
+ If the "node" is not the received stanza but locally created ("to" and "from" fields needs not swapping)
+ specify the "reply" argument as false.
+ """
+ if reply:
+ Protocol.__init__(self, to=node.getFrom(), frm=node.getTo(), node=node)
+ else:
+ Protocol.__init__(self, node=node)
+ self.setError(error)
+ if node.getType() == "error":
+ self.__str__ = self.__dupstr__
+
+ def __dupstr__(self, dup1=None, dup2=None):
+ """
+ Dummy function used as preventor of creating error node in reply to error node.
+ I.e. you will not be able to serialize "double" error into string.
+ """
+ return ""
+
+class DataField(Node):
+ """
+ This class is used in the DataForm class to describe the single data item.
+ If you are working with jabber:x:data (XEP-0004, XEP-0068, XEP-0122)
+ then you will need to work with instances of this class.
+ """
+ def __init__(self, name=None, value=None, typ=None, required=0, label=None, desc=None, options=[], node=None):
+ """
+ Create new data field of specified name,value and type. Also "required", "desc" and "options" fields can be set.
+ Alternatively other XML object can be passed in as the "node" parameted to replicate it as a new datafiled.
+ """
+ Node.__init__(self, "field", node=node)
+ if name:
+ self.setVar(name)
+ if isinstance(value, (list, tuple)):
+ self.setValues(value)
+ elif value:
+ self.setValue(value)
+ if typ:
+ self.setType(typ)
+ elif not typ and not node:
+ self.setType("text-single")
+ if required:
+ self.setRequired(required)
+ if label:
+ self.setLabel(label)
+ if desc:
+ self.setDesc(desc)
+ if options:
+ self.setOptions(options)
+
+ def setRequired(self, req=1):
+ """
+ Change the state of the "required" flag.
+ """
+ if req:
+ self.setTag("required")
+ else:
+ try:
+ self.delChild("required")
+ except ValueError:
+ return None
+
+ def isRequired(self):
+ """
+ Returns in this field a required one.
+ """
+ return self.getTag("required")
+
+ def setLabel(self, label):
+ """
+ Set the label of this field.
+ """
+ self.setAttr("label", label)
+
+ def getLabel(self):
+ """
+ Return the label of this field.
+ """
+ return self.getAttr("label")
+
+ def setDesc(self, desc):
+ """
+ Set the description of this field.
+ """
+ self.setTagData("desc", desc)
+
+ def getDesc(self):
+ """
+ Return the description of this field.
+ """
+ return self.getTagData("desc")
+
+ def setValue(self, val):
+ """
+ Set the value of this field.
+ """
+ self.setTagData("value", val)
+
+ def getValue(self):
+ return self.getTagData("value")
+
+ def setValues(self, ls):
+ """
+ Set the values of this field as values-list.
+ Replaces all previous filed values! If you need to just add a value - use addValue method.
+ """
+ while self.getTag("value"):
+ self.delChild("value")
+ for val in ls:
+ self.addValue(val)
+
+ def addValue(self, val):
+ """
+ Add one more value to this field. Used in "get" iq's or such.
+ """
+ self.addChild("value", {}, [val])
+
+ def getValues(self):
+ """
+ Return the list of values associated with this field.
+ """
+ ret = []
+ for tag in self.getTags("value"):
+ ret.append(tag.getData())
+ return ret
+
+ def getOptions(self):
+ """
+ Return label-option pairs list associated with this field.
+ """
+ ret = []
+ for tag in self.getTags("option"):
+ ret.append([tag.getAttr("label"), tag.getTagData("value")])
+ return ret
+
+ def setOptions(self, ls):
+ """
+ Set label-option pairs list associated with this field.
+ """
+ while self.getTag("option"):
+ self.delChild("option")
+ for opt in ls:
+ self.addOption(opt)
+
+ def addOption(self, opt):
+ """
+ Add one more label-option pair to this field.
+ """
+ if isinstance(opt, basestring):
+ self.addChild("option").setTagData("value", opt)
+ else:
+ self.addChild("option", {"label": opt[0]}).setTagData("value", opt[1])
+
+ def getType(self):
+ """
+ Get type of this field.
+ """
+ return self.getAttr("type")
+
+ def setType(self, val):
+ """
+ Set type of this field.
+ """
+ return self.setAttr("type", val)
+
+ def getVar(self):
+ """
+ Get "var" attribute value of this field.
+ """
+ return self.getAttr("var")
+
+ def setVar(self, val):
+ """
+ Set "var" attribute value of this field.
+ """
+ return self.setAttr("var", val)
+
+class DataReported(Node):
+ """
+ This class is used in the DataForm class to describe the "reported data field" data items which are used in
+ "multiple item form results" (as described in XEP-0004).
+ Represents the fields that will be returned from a search. This information is useful when
+ you try to use the jabber:iq:search namespace to return dynamic form information.
+ """
+ def __init__(self, node=None):
+ """
+ Create new empty "reported data" field. However, note that, according XEP-0004:
+ * It MUST contain one or more DataFields.
+ * Contained DataFields SHOULD possess a "type" and "label" attribute in addition to "var" attribute
+ * Contained DataFields SHOULD NOT contain a <value/> element.
+ Alternatively other XML object can be passed in as the "node" parameted to replicate it as a new
+ dataitem.
+ """
+ Node.__init__(self, "reported", node=node)
+ if node:
+ newkids = []
+ for n in self.getChildren():
+ if n.getName() == "field":
+ newkids.append(DataField(node=n))
+ else:
+ newkids.append(n)
+ self.kids = newkids
+
+ def getField(self, name):
+ """
+ Return the datafield object with name "name" (if exists).
+ """
+ return self.getTag("field", attrs={"var": name})
+
+ def setField(self, name, typ=None, label=None):
+ """
+ Create if nessessary or get the existing datafield object with name "name" and return it.
+ If created, attributes "type" and "label" are applied to new datafield.
+ """
+ field = self.getField(name)
+ if not field:
+ field = self.addChild(node=DataField(name, None, typ, 0, label))
+ return field
+
+ def asDict(self):
+ """
+ Represent dataitem as simple dictionary mapping of datafield names to their values.
+ """
+ ret = {}
+ for field in self.getTags("field"):
+ name = field.getAttr("var")
+ typ = field.getType()
+ if isinstance(typ, basestring) and typ.endswith("-multi"):
+ val = []
+ for i in field.getTags("value"):
+ val.append(i.getData())
+ else:
+ val = field.getTagData("value")
+ ret[name] = val
+ if self.getTag("instructions"):
+ ret["instructions"] = self.getInstructions()
+ return ret
+
+ def __getitem__(self, name):
+ """
+ Simple dictionary interface for getting datafields values by their names.
+ """
+ item = self.getField(name)
+ if item:
+ return item.getValue()
+ raise IndexError("No such field")
+
+ def __setitem__(self, name, val):
+ """
+ Simple dictionary interface for setting datafields values by their names.
+ """
+ return self.setField(name).setValue(val)
+
+class DataItem(Node):
+ """
+ This class is used in the DataForm class to describe data items which are used in "multiple
+ item form results" (as described in XEP-0004).
+ """
+ def __init__(self, node=None):
+ """
+ Create new empty data item. However, note that, according XEP-0004, DataItem MUST contain ALL
+ DataFields described in DataReported.
+ Alternatively other XML object can be passed in as the "node" parameted to replicate it as a new
+ dataitem.
+ """
+ Node.__init__(self, "item", node=node)
+ if node:
+ newkids = []
+ for n in self.getChildren():
+ if n.getName() == "field":
+ newkids.append(DataField(node=n))
+ else:
+ newkids.append(n)
+ self.kids = newkids
+
+ def getField(self, name):
+ """
+ Return the datafield object with name "name" (if exists).
+ """
+ return self.getTag("field", attrs={"var": name})
+
+ def setField(self, name, value=None, typ=None):
+ """
+ Create if nessessary or get the existing datafield object with name "name" and return it.
+ """
+ field = self.getField(name)
+ if not field:
+ field = self.addChild(node=DataField(name, value, typ))
+ return field
+
+ def asDict(self):
+ """
+ Represent dataitem as simple dictionary mapping of datafield names to their values.
+ """
+ ret = {}
+ for field in self.getTags("field"):
+ name = field.getAttr("var")
+ typ = field.getType()
+ if isinstance(typ, basestring) and typ.endswith("-multi"):
+ val = []
+ for i in field.getTags("value"):
+ val.append(i.getData())
+ else:
+ val = field.getTagData("value")
+ ret[name] = val
+ if self.getTag("instructions"):
+ ret["instructions"] = self.getInstructions()
+ return ret
+
+ def __getitem__(self, name):
+ """
+ Simple dictionary interface for getting datafields values by their names.
+ """
+ item = self.getField(name)
+ if item:
+ return item.getValue()
+ raise IndexError("No such field")
+
+ def __setitem__(self, name, val):
+ """
+ Simple dictionary interface for setting datafields values by their names.
+ """
+ return self.setField(name).setValue(val)
+
+class DataForm(Node):
+ """
+ DataForm class. Used for manipulating dataforms in XMPP.
+ Relevant XEPs: 0004, 0068, 0122.
+ Can be used in disco, pub-sub and many other applications.
+ """
+ def __init__(self, typ=None, data=[], title=None, node=None):
+ """
+ Create new dataform of type "typ"; "data" is the list of DataReported,
+ DataItem and DataField instances that this dataform contains; "title"
+ is the title string.
+ You can specify the "node" argument as the other node to be used as
+ base for constructing this dataform.
+
+ Title and instructions is optional and SHOULD NOT contain newlines.
+ Several instructions MAY be present.
+ "typ" can be one of ("form" | "submit" | "cancel" | "result" )
+ "typ" of reply iq can be ( "result" | "set" | "set" | "result" ) respectively.
+ "cancel" form can not contain any fields. All other forms contains AT LEAST one field.
+ "title" MAY be included in forms of type "form" and "result".
+ """
+ Node.__init__(self, "x", node=node)
+ if node:
+ newkids = []
+ for n in self.getChildren():
+ if n.getName() == "field":
+ newkids.append(DataField(node=n))
+ elif n.getName() == "item":
+ newkids.append(DataItem(node=n))
+ elif n.getName() == "reported":
+ newkids.append(DataReported(node=n))
+ else:
+ newkids.append(n)
+ self.kids = newkids
+ if typ:
+ self.setType(typ)
+ self.setNamespace(NS_DATA)
+ if title:
+ self.setTitle(title)
+ if isinstance(data, dict):
+ newdata = []
+ for name in data.keys():
+ newdata.append(DataField(name, data[name]))
+ data = newdata
+ for child in data:
+ if isinstance(child, basestring):
+ self.addInstructions(child)
+ elif isinstance(child, DataField):
+ self.kids.append(child)
+ elif isinstance(child, DataItem):
+ self.kids.append(child)
+ elif isinstance(child, DataReported):
+ self.kids.append(child)
+ else:
+ self.kids.append(DataField(node=child))
+
+ def getType(self):
+ """
+ Return the type of dataform.
+ """
+ return self.getAttr("type")
+
+ def setType(self, typ):
+ """
+ Set the type of dataform.
+ """
+ self.setAttr("type", typ)
+
+ def getTitle(self):
+ """
+ Return the title of dataform.
+ """
+ return self.getTagData("title")
+
+ def setTitle(self, text):
+ """
+ Set the title of dataform.
+ """
+ self.setTagData("title", text)
+
+ def getInstructions(self):
+ """
+ Return the instructions of dataform.
+ """
+ return self.getTagData("instructions")
+
+ def setInstructions(self, text):
+ """
+ Set the instructions of dataform.
+ """
+ self.setTagData("instructions", text)
+
+ def addInstructions(self, text):
+ """
+ Add one more instruction to the dataform.
+ """
+ self.addChild("instructions", {}, [text])
+
+ def getField(self, name):
+ """
+ Return the datafield object with name "name" (if exists).
+ """
+ return self.getTag("field", attrs={"var": name})
+
+ def setField(self, name, value=None, typ=None):
+ """
+ Create if nessessary or get the existing datafield object with name "name" and return it.
+ """
+ field = self.getField(name)
+ if not field:
+ field = self.addChild(node=DataField(name, value, typ))
+ return field
+
+ def asDict(self):
+ """
+ Represent dataform as simple dictionary mapping of datafield names to their values.
+ """
+ ret = {}
+ for field in self.getTags("field"):
+ name = field.getAttr("var")
+ typ = field.getType()
+ if isinstance(typ, basestring) and typ.endswith("-multi"):
+ val = []
+ for i in field.getTags("value"):
+ val.append(i.getData())
+ else:
+ val = field.getTagData("value")
+ ret[name] = val
+ if self.getTag("instructions"):
+ ret["instructions"] = self.getInstructions()
+ return ret
+
+ def __getitem__(self, name):
+ """
+ Simple dictionary interface for getting datafields values by their names.
+ """
+ item = self.getField(name)
+ if item:
+ return item.getValue()
+ raise IndexError("No such field")
+
+ def __setitem__(self, name, val):
+ """
+ Simple dictionary interface for setting datafields values by their names.
+ """
+ return self.setField(name).setValue(val)
diff --git a/xmpp/roster.py b/xmpp/roster.py
new file mode 100644
index 0000000..1cf737f
--- /dev/null
+++ b/xmpp/roster.py
@@ -0,0 +1,280 @@
+## roster.py
+##
+## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## 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 General Public License for more details.
+
+# $Id: roster.py, v1.21 2013/10/21 alkorgun Exp $
+
+"""
+Simple roster implementation. Can be used though for different tasks like
+mass-renaming of contacts.
+"""
+
+from plugin import PlugIn
+from protocol import *
+
+class Roster(PlugIn):
+ """
+ Defines a plenty of methods that will allow you to manage roster.
+ Also automatically track presences from remote JIDs taking into
+ account that every JID can have multiple resources connected. Does not
+ currently support "error" presences.
+ You can also use mapping interface for access to the internal representation of
+ contacts in roster.
+ """
+ def __init__(self):
+ """
+ Init internal variables.
+ """
+ PlugIn.__init__(self)
+ self.DBG_LINE = "roster"
+ self._data = {}
+ self.set = None
+ self._exported_methods = [self.getRoster]
+
+ def plugin(self, owner, request=1):
+ """
+ Register presence and subscription trackers in the owner's dispatcher.
+ Also request roster from server if the "request" argument is set.
+ Used internally.
+ """
+ self._owner.RegisterHandler("iq", self.RosterIqHandler, "result", NS_ROSTER)
+ self._owner.RegisterHandler("iq", self.RosterIqHandler, "set", NS_ROSTER)
+ self._owner.RegisterHandler("presence", self.PresenceHandler)
+ if request:
+ self.Request()
+
+ def Request(self, force=0):
+ """
+ Request roster from server if it were not yet requested
+ (or if the "force" argument is set).
+ """
+ if self.set is None:
+ self.set = 0
+ elif not force:
+ return None
+ self._owner.send(Iq("get", NS_ROSTER))
+ self.DEBUG("Roster requested from server", "start")
+
+ def getRoster(self):
+ """
+ Requests roster from server if neccessary and returns self.
+ """
+ if not self.set:
+ self.Request()
+ while not self.set:
+ self._owner.Process(10)
+ return self
+
+ def RosterIqHandler(self, dis, stanza):
+ """
+ Subscription tracker. Used internally for setting items state in
+ internal roster representation.
+ """
+ for item in stanza.getTag("query").getTags("item"):
+ jid = item.getAttr("jid")
+ if item.getAttr("subscription") == "remove":
+ if self._data.has_key(jid):
+ del self._data[jid]
+ raise NodeProcessed() # a MUST
+ self.DEBUG("Setting roster item %s..." % jid, "ok")
+ if jid not in self._data:
+ self._data[jid] = {}
+ self._data[jid]["name"] = item.getAttr("name")
+ self._data[jid]["ask"] = item.getAttr("ask")
+ self._data[jid]["subscription"] = item.getAttr("subscription")
+ self._data[jid]["groups"] = []
+ if not self._data[jid].has_key("resources"):
+ self._data[jid]["resources"] = {}
+ for group in item.getTags("group"):
+ self._data[jid]["groups"].append(group.getData())
+ self._data["@".join((self._owner.User, self._owner.Server))] = {"resources": {}, "name": None, "ask": None, "subscription": None, "groups": None, }
+ self.set = 1
+ raise NodeProcessed() # a MUST. Otherwise you'll get back an <iq type='error'/>
+
+ def PresenceHandler(self, dis, pres):
+ """
+ Presence tracker. Used internally for setting items' resources state in
+ internal roster representation.
+ """
+ jid = JID(pres.getFrom())
+ if not self._data.has_key(jid.getStripped()):
+ self._data[jid.getStripped()] = {"name": None, "ask": None, "subscription": "none", "groups": ["Not in roster"], "resources": {}}
+ item = self._data[jid.getStripped()]
+ typ = pres.getType()
+ if not typ:
+ self.DEBUG("Setting roster item %s for resource %s..." % (jid.getStripped(), jid.getResource()), "ok")
+ item["resources"][jid.getResource()] = res = {"show": None, "status": None, "priority": "0", "timestamp": None}
+ if pres.getTag("show"):
+ res["show"] = pres.getShow()
+ if pres.getTag("status"):
+ res["status"] = pres.getStatus()
+ if pres.getTag("priority"):
+ res["priority"] = pres.getPriority()
+ if not pres.getTimestamp():
+ pres.setTimestamp()
+ res["timestamp"] = pres.getTimestamp()
+ elif typ == "unavailable" and item["resources"].has_key(jid.getResource()):
+ del item["resources"][jid.getResource()]
+ # Need to handle type="error" also
+
+ def _getItemData(self, jid, dataname):
+ """
+ Return specific jid's representation in internal format. Used internally.
+ """
+ jid = jid[:(jid + "/").find("/")]
+ return self._data[jid][dataname]
+
+ def _getResourceData(self, jid, dataname):
+ """
+ Return specific jid's resource representation in internal format. Used internally.
+ """
+ if jid.find("/") + 1:
+ jid, resource = jid.split("/", 1)
+ if self._data[jid]["resources"].has_key(resource):
+ return self._data[jid]["resources"][resource][dataname]
+ elif self._data[jid]["resources"].keys():
+ lastpri = -129
+ for r in self._data[jid]["resources"].keys():
+ if int(self._data[jid]["resources"][r]["priority"]) > lastpri:
+ resource, lastpri = r, int(self._data[jid]["resources"][r]["priority"])
+ return self._data[jid]["resources"][resource][dataname]
+
+ def delItem(self, jid):
+ """
+ Delete contact "jid" from roster.
+ """
+ self._owner.send(Iq("set", NS_ROSTER, payload=[Node("item", {"jid": jid, "subscription": "remove"})]))
+
+ def getAsk(self, jid):
+ """
+ Returns "ask" value of contact "jid".
+ """
+ return self._getItemData(jid, "ask")
+
+ def getGroups(self, jid):
+ """
+ Returns groups list that contact "jid" belongs to.
+ """
+ return self._getItemData(jid, "groups")
+
+ def getName(self, jid):
+ """
+ Returns name of contact "jid".
+ """
+ return self._getItemData(jid, "name")
+
+ def getPriority(self, jid):
+ """
+ Returns priority of contact "jid". "jid" should be a full (not bare) JID.
+ """
+ return self._getResourceData(jid, "priority")
+
+ def getRawRoster(self):
+ """
+ Returns roster representation in internal format.
+ """
+ return self._data
+
+ def getRawItem(self, jid):
+ """
+ Returns roster item "jid" representation in internal format.
+ """
+ return self._data[jid[:(jid + "/").find("/")]]
+
+ def getShow(self, jid):
+ """
+ Returns "show" value of contact "jid". "jid" should be a full (not bare) JID.
+ """
+ return self._getResourceData(jid, "show")
+
+ def getStatus(self, jid):
+ """
+ Returns "status" value of contact "jid". "jid" should be a full (not bare) JID.
+ """
+ return self._getResourceData(jid, "status")
+
+ def getSubscription(self, jid):
+ """
+ Returns "subscription" value of contact "jid".
+ """
+ return self._getItemData(jid, "subscription")
+
+ def getResources(self, jid):
+ """
+ Returns list of connected resources of contact "jid".
+ """
+ return self._data[jid[:(jid + "/").find("/")]]["resources"].keys()
+
+ def setItem(self, jid, name=None, groups=[]):
+ """
+ Creates/renames contact "jid" and sets the groups list that it now belongs to.
+ """
+ iq = Iq("set", NS_ROSTER)
+ query = iq.getTag("query")
+ attrs = {"jid": jid}
+ if name:
+ attrs["name"] = name
+ item = query.setTag("item", attrs)
+ for group in groups:
+ item.addChild(node=Node("group", payload=[group]))
+ self._owner.send(iq)
+
+ def getItems(self):
+ """
+ Return list of all [bare] JIDs that the roster is currently tracks.
+ """
+ return self._data.keys()
+
+ def keys(self):
+ """
+ Same as getItems. Provided for the sake of dictionary interface.
+ """
+ return self._data.keys()
+
+ def __getitem__(self, item):
+ """
+ Get the contact in the internal format. Raises KeyError if JID "item" is not in roster.
+ """
+ return self._data[item]
+
+ def getItem(self, item):
+ """
+ Get the contact in the internal format (or None if JID "item" is not in roster).
+ """
+ if self._data.has_key(item):
+ return self._data[item]
+
+ def Subscribe(self, jid):
+ """
+ Send subscription request to JID "jid".
+ """
+ self._owner.send(Presence(jid, "subscribe"))
+
+ def Unsubscribe(self, jid):
+ """
+ Ask for removing our subscription for JID "jid".
+ """
+ self._owner.send(Presence(jid, "unsubscribe"))
+
+ def Authorize(self, jid):
+ """
+ Authorise JID "jid". Works only if these JID requested auth previously.
+ """
+ self._owner.send(Presence(jid, "subscribed"))
+
+ def Unauthorize(self, jid):
+ """
+ Unauthorise JID "jid". Use for declining authorisation request
+ or for removing existing authorization.
+ """
+ self._owner.send(Presence(jid, "unsubscribed"))
diff --git a/xmpp/simplexml.py b/xmpp/simplexml.py
new file mode 100644
index 0000000..19dcb29
--- /dev/null
+++ b/xmpp/simplexml.py
@@ -0,0 +1,704 @@
+## simplexml.py based on Mattew Allum's xmlstream.py
+##
+## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## 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 General Public License for more details.
+
+# $Id: simplexml.py, v1.35 2013/10/21 alkorgun Exp $
+
+"""
+Simplexml module provides xmpppy library with all needed tools to handle
+XML nodes and XML streams.
+I'm personally using it in many other separate projects.
+It is designed to be as standalone as possible.
+"""
+
+import xml.parsers.expat
+
+XML_ls = (
+ ("&", "&amp;"),
+ ("\x0C", ""),
+ ("\x1B", ""),
+ ("<", "&lt;"),
+ (">", "&gt;"),
+ ('"', "&quot;"),
+ ("'", "&apos;")
+)
+
+def XMLescape(body):
+ for char, edef in XML_ls:
+ body = body.replace(char, edef)
+ return body.strip()
+
+ENCODING = "utf-8"
+
+def ustr(what):
+ """
+ Converts object "what" to unicode string using it's own __str__ method if accessible or unicode method otherwise.
+ """
+ if isinstance(what, unicode):
+ return what
+ try:
+ what = what.__str__()
+ except AttributeError:
+ what = str(what)
+ if not isinstance(what, unicode):
+ return unicode(what, ENCODING)
+ return what
+
+class Node(object):
+ """
+ Node class describes syntax of separate XML Node. It have a constructor that permits node creation
+ from set of "namespace name", attributes and payload of text strings and other nodes.
+ It does not natively support building node from text string and uses NodeBuilder class for that purpose.
+ After creation node can be mangled in many ways so it can be completely changed.
+ Also node can be serialized into string in one of two modes: default (where the textual representation
+ of node describes it exactly) and "fancy" - with whitespace added to make indentation and thus make
+ result more readable by human.
+
+ Node class have attribute FORCE_NODE_RECREATION that is defaults to False thus enabling fast node
+ replication from the some other node. The drawback of the fast way is that new node shares some
+ info with the "original" node that is changing the one node may influence the other. Though it is
+ rarely needed (in xmpppy it is never needed at all since I'm usually never using original node after
+ replication (and using replication only to move upwards on the classes tree).
+ """
+ FORCE_NODE_RECREATION = 0
+
+ def __init__(self, tag=None, attrs={}, payload=[], parent=None, nsp=None, node_built=False, node=None):
+ """
+ Takes "tag" argument as the name of node (prepended by namespace, if needed and separated from it
+ by a space), attrs dictionary as the set of arguments, payload list as the set of textual strings
+ and child nodes that this node carries within itself and "parent" argument that is another node
+ that this one will be the child of. Also the __init__ can be provided with "node" argument that is
+ either a text string containing exactly one node or another Node instance to begin with. If both
+ "node" and other arguments is provided then the node initially created as replica of "node"
+ provided and then modified to be compliant with other arguments.
+ """
+ if node:
+ if self.FORCE_NODE_RECREATION and isinstance(node, Node):
+ node = str(node)
+ if not isinstance(node, Node):
+ node = NodeBuilder(node, self)
+ node_built = True
+ else:
+ self.name, self.namespace, self.attrs, self.data, self.kids, self.parent, self.nsd = node.name, node.namespace, {}, [], [], node.parent, {}
+ for key in node.attrs.keys():
+ self.attrs[key] = node.attrs[key]
+ for data in node.data:
+ self.data.append(data)
+ for kid in node.kids:
+ self.kids.append(kid)
+ for k, v in node.nsd.items():
+ self.nsd[k] = v
+ else:
+ self.name, self.namespace, self.attrs, self.data, self.kids, self.parent, self.nsd = "tag", "", {}, [], [], None, {}
+ if parent:
+ self.parent = parent
+ self.nsp_cache = {}
+ if nsp:
+ for k, v in nsp.items():
+ self.nsp_cache[k] = v
+ for attr, val in attrs.items():
+ if attr == "xmlns":
+ self.nsd[u""] = val
+ elif attr.startswith("xmlns:"):
+ self.nsd[attr[6:]] = val
+ self.attrs[attr] = attrs[attr]
+ if tag:
+ if node_built:
+ pfx, self.name = ([""] + tag.split(":"))[-2:]
+ self.namespace = self.lookup_nsp(pfx)
+ elif " " in tag:
+ self.namespace, self.name = tag.split()
+ else:
+ self.name = tag
+ if isinstance(payload, basestring):
+ payload = [payload]
+ for i in payload:
+ if isinstance(i, Node):
+ self.addChild(node=i)
+ else:
+ self.data.append(ustr(i))
+
+ def lookup_nsp(self, pfx=""):
+ ns = self.nsd.get(pfx, None)
+ if ns is None:
+ ns = self.nsp_cache.get(pfx, None)
+ if ns is None:
+ if self.parent:
+ ns = self.parent.lookup_nsp(pfx)
+ self.nsp_cache[pfx] = ns
+ else:
+ return "http://www.gajim.org/xmlns/undeclared"
+ return ns
+
+ def __str__(self, fancy=0):
+ """
+ Method used to dump node into textual representation.
+ if "fancy" argument is set to True produces indented output for readability.
+ """
+ s = (fancy - 1) * 2 * " " + "<" + self.name
+ if self.namespace:
+ if not self.parent or self.parent.namespace != self.namespace:
+ if "xmlns" not in self.attrs:
+ s = s + " xmlns=\"%s\"" % self.namespace
+ for key in self.attrs.keys():
+ val = ustr(self.attrs[key])
+ s = s + " %s=\"%s\"" % (key, XMLescape(val))
+ s = s + ">"
+ cnt = 0
+ if self.kids:
+ if fancy:
+ s = s + "\n"
+ for a in self.kids:
+ if not fancy and (len(self.data) - 1) >= cnt:
+ s = s + XMLescape(self.data[cnt])
+ elif (len(self.data) - 1) >= cnt:
+ s = s + XMLescape(self.data[cnt].strip())
+ if isinstance(a, Node):
+ s = s + a.__str__(fancy and fancy + 1)
+ elif a:
+ s = s + a.__str__()
+ cnt = cnt + 1
+ if not fancy and (len(self.data) - 1) >= cnt:
+ s = s + XMLescape(self.data[cnt])
+ elif (len(self.data) - 1) >= cnt:
+ s = s + XMLescape(self.data[cnt].strip())
+ if not self.kids and s.endswith(">"):
+ s = s[:-1] + " />"
+ if fancy:
+ s = s + "\n"
+ else:
+ if fancy and not self.data:
+ s = s + (fancy - 1) * 2 * " "
+ s = s + "</" + self.name + ">"
+ if fancy:
+ s = s + "\n"
+ return s
+
+ def getCDATA(self):
+ """
+ Serialize node, dropping all tags and leaving CDATA intact.
+ That is effectively kills all formatting, leaving only text were contained in XML.
+ """
+ s = ""
+ cnt = 0
+ if self.kids:
+ for a in self.kids:
+ s = s + self.data[cnt]
+ if a:
+ s = s + a.getCDATA()
+ cnt = cnt + 1
+ if (len(self.data) - 1) >= cnt:
+ s = s + self.data[cnt]
+ return s
+
+ def addChild(self, name=None, attrs={}, payload=[], namespace=None, node=None):
+ """
+ If "node" argument is provided, adds it as child node. Else creates new node from
+ the other arguments' values and adds it as well.
+ """
+ if "xmlns" in attrs:
+ raise AttributeError("Use namespace=x instead of attrs={\"xmlns\": x}")
+ if node:
+ newnode = node
+ node.parent = self
+ else:
+ newnode = Node(tag=name, parent=self, attrs=attrs, payload=payload)
+ if namespace:
+ newnode.setNamespace(namespace)
+ self.kids.append(newnode)
+ self.data.append(u"")
+ return newnode
+
+ def addData(self, data):
+ """
+ Adds some CDATA to node.
+ """
+ self.data.append(ustr(data))
+ self.kids.append(None)
+
+ def clearData(self):
+ """
+ Removes all CDATA from the node.
+ """
+ self.data = []
+
+ def delAttr(self, key):
+ """
+ Deletes an attribute "key"
+ """
+ del self.attrs[key]
+
+ def delChild(self, node, attrs={}):
+ """
+ Deletes the "node" from the node's childs list, if "node" is an instance.
+ Else deletes the first node that have specified name and (optionally) attributes.
+ """
+ if not isinstance(node, Node):
+ node = self.getTag(node, attrs)
+ self.kids[self.kids.index(node)] = None
+ return node
+
+ def getAttrs(self):
+ """
+ Returns all node's attributes as dictionary.
+ """
+ return self.attrs
+
+ def getAttr(self, key):
+ """
+ Returns value of specified attribute.
+ """
+ try:
+ attr = self.attrs[key]
+ except:
+ attr = None
+ return attr
+
+ def getChildren(self):
+ """
+ Returns all node's child nodes as list.
+ """
+ return self.kids
+
+ def getData(self):
+ """
+ Returns all node CDATA as string (concatenated).
+ """
+ return "".join(self.data)
+
+ def getName(self):
+ """
+ Returns the name of node.
+ """
+ return self.name
+
+ def getNamespace(self):
+ """
+ Returns the namespace of node.
+ """
+ return self.namespace
+
+ def getParent(self):
+ """
+ Returns the parent of node (if present).
+ """
+ return self.parent
+
+ def getPayload(self):
+ """
+ Return the payload of node i.e. list of child nodes and CDATA entries.
+ F.e. for "<node>text1<nodea/><nodeb/> text2</node>" will be returned list:
+ ["text1", <nodea instance>, <nodeb instance>, " text2"].
+ """
+ pl = []
+ for i in range(max(len(self.data), len(self.kids))):
+ if i < len(self.data) and self.data[i]:
+ pl.append(self.data[i])
+ if i < len(self.kids) and self.kids[i]:
+ pl.append(self.kids[i])
+ return pl
+
+ def getTag(self, name, attrs={}, namespace=None):
+ """
+ Filters all child nodes using specified arguments as filter.
+ Returns the first found or None if not found.
+ """
+ return self.getTags(name, attrs, namespace, one=1)
+
+ def getTagAttr(self, tag, attr):
+ """
+ Returns attribute value of the child with specified name (or None if no such attribute).
+ """
+ try:
+ attr = self.getTag(tag).attrs[attr]
+ except:
+ attr = None
+ return attr
+
+ def getTagData(self, tag):
+ """
+ Returns cocatenated CDATA of the child with specified name.
+ """
+ try:
+ data = self.getTag(tag).getData()
+ except:
+ data = None
+ return data
+
+ def getTags(self, name, attrs={}, namespace=None, one=0):
+ """
+ Filters all child nodes using specified arguments as filter.
+ Returns the list of nodes found.
+ """
+ nodes = []
+ for node in self.kids:
+ if not node:
+ continue
+ if namespace and namespace != node.getNamespace():
+ continue
+ if node.getName() == name:
+ for key in attrs.keys():
+ if key not in node.attrs or node.attrs[key] != attrs[key]:
+ break
+ else:
+ nodes.append(node)
+ if one and nodes:
+ return nodes[0]
+ if not one:
+ return nodes
+
+ def iterTags(self, name, attrs={}, namespace=None):
+ """
+ Iterate over all children using specified arguments as filter.
+ """
+ for node in self.kids:
+ if not node:
+ continue
+ if namespace is not None and namespace != node.getNamespace():
+ continue
+ if node.getName() == name:
+ for key in attrs.keys():
+ if key not in node.attrs or node.attrs[key] != attrs[key]:
+ break
+ else:
+ yield node
+
+ def setAttr(self, key, val):
+ """
+ Sets attribute "key" with the value "val".
+ """
+ self.attrs[key] = val
+
+ def setData(self, data):
+ """
+ Sets node's CDATA to provided string. Resets all previous CDATA!
+ """
+ self.data = [ustr(data)]
+
+ def setName(self, val):
+ """
+ Changes the node name.
+ """
+ self.name = val
+
+ def setNamespace(self, namespace):
+ """
+ Changes the node namespace.
+ """
+ self.namespace = namespace
+
+ def setParent(self, node):
+ """
+ Sets node's parent to "node". WARNING: do not checks if the parent already present
+ and not removes the node from the list of childs of previous parent.
+ """
+ self.parent = node
+
+ def setPayload(self, payload, add=0):
+ """
+ Sets node payload according to the list specified. WARNING: completely replaces all node's
+ previous content. If you wish just to add child or CDATA - use addData or addChild methods.
+ """
+ if isinstance(payload, basestring):
+ payload = [payload]
+ if add:
+ self.kids += payload
+ else:
+ self.kids = payload
+
+ def setTag(self, name, attrs={}, namespace=None):
+ """
+ Same as getTag but if the node with specified namespace/attributes not found, creates such
+ node and returns it.
+ """
+ node = self.getTags(name, attrs, namespace=namespace, one=1)
+ if not node:
+ node = self.addChild(name, attrs, namespace=namespace)
+ return node
+
+ def setTagAttr(self, tag, attr, val):
+ """
+ Creates new node (if not already present) with name "tag"
+ and sets it's attribute "attr" to value "val".
+ """
+ try:
+ self.getTag(tag).attrs[attr] = val
+ except:
+ self.addChild(tag, attrs={attr: val})
+
+ def setTagData(self, tag, val, attrs={}):
+ """
+ Creates new node (if not already present) with name "tag"
+ and (optionally) attributes "attrs" and sets it's CDATA to string "val".
+ """
+ try:
+ self.getTag(tag, attrs).setData(ustr(val))
+ except:
+ self.addChild(tag, attrs, payload=[ustr(val)])
+
+ def has_attr(self, key):
+ """
+ Checks if node have attribute "key".
+ """
+ return key in self.attrs
+
+ def __getitem__(self, item):
+ """
+ Returns node's attribute "item" value.
+ """
+ return self.getAttr(item)
+
+ def __setitem__(self, item, val):
+ """
+ Sets node's attribute "item" value.
+ """
+ return self.setAttr(item, val)
+
+ def __delitem__(self, item):
+ """
+ Deletes node's attribute "item".
+ """
+ return self.delAttr(item)
+
+ def __getattr__(self, attr):
+ """
+ Reduce memory usage caused by T/NT classes - use memory only when needed.
+ """
+ if attr == "T":
+ self.T = T(self)
+ return self.T
+ if attr == "NT":
+ self.NT = NT(self)
+ return self.NT
+ raise AttributeError()
+
+class T:
+ """
+ Auxiliary class used to quick access to node's child nodes.
+ """
+ def __init__(self, node):
+ self.__dict__["node"] = node
+
+ def __getattr__(self, attr):
+ return self.node.getTag(attr)
+
+ def __setattr__(self, attr, val):
+ if isinstance(val, Node):
+ Node.__init__(self.node.setTag(attr), node=val)
+ else:
+ return self.node.setTagData(attr, val)
+
+ def __delattr__(self, attr):
+ return self.node.delChild(attr)
+
+class NT(T):
+ """
+ Auxiliary class used to quick create node's child nodes.
+ """
+ def __getattr__(self, attr):
+ return self.node.addChild(attr)
+
+ def __setattr__(self, attr, val):
+ if isinstance(val, Node):
+ self.node.addChild(attr, node=val)
+ else:
+ return self.node.addChild(attr, payload=[val])
+
+DBG_NODEBUILDER = "nodebuilder"
+
+class NodeBuilder:
+ """
+ Builds a Node class minidom from data parsed to it. This class used for two purposes:
+ 1. Creation an XML Node from a textual representation. F.e. reading a config file. See an XML2Node method.
+ 2. Handling an incoming XML stream. This is done by mangling
+ the __dispatch_depth parameter and redefining the dispatch method.
+ You do not need to use this class directly if you do not designing your own XML handler.
+ """
+ def __init__(self, data=None, initial_node=None):
+ """
+ Takes two optional parameters: "data" and "initial_node".
+ By default class initialised with empty Node class instance.
+ Though, if "initial_node" is provided it used as "starting point".
+ You can think about it as of "node upgrade".
+ "data" (if provided) feeded to parser immidiatedly after instance init.
+ """
+ self.DEBUG(DBG_NODEBUILDER, "Preparing to handle incoming XML stream.", "start")
+ self._parser = xml.parsers.expat.ParserCreate()
+ self._parser.StartElementHandler = self.starttag
+ self._parser.EndElementHandler = self.endtag
+ self._parser.CharacterDataHandler = self.handle_cdata
+ self._parser.StartNamespaceDeclHandler = self.handle_namespace_start
+ self._parser.buffer_text = True
+ self.Parse = self._parser.Parse
+ self.__depth = 0
+ self.__last_depth = 0
+ self.__max_depth = 0
+ self._dispatch_depth = 1
+ self._document_attrs = None
+ self._document_nsp = None
+ self._mini_dom = initial_node
+ self.last_is_data = 1
+ self._ptr = None
+ self.data_buffer = None
+ self.streamError = ""
+ if data:
+ self._parser.Parse(data, 1)
+
+ def check_data_buffer(self):
+ if self.data_buffer:
+ self._ptr.data.append("".join(self.data_buffer))
+ del self.data_buffer[:]
+ self.data_buffer = None
+
+ def destroy(self):
+ """
+ Method used to allow class instance to be garbage-collected.
+ """
+ self.check_data_buffer()
+ self._parser.StartElementHandler = None
+ self._parser.EndElementHandler = None
+ self._parser.CharacterDataHandler = None
+ self._parser.StartNamespaceDeclHandler = None
+
+ def starttag(self, tag, attrs):
+ """
+ XML Parser callback. Used internally.
+ """
+ self.check_data_buffer()
+ self._inc_depth()
+ self.DEBUG(DBG_NODEBUILDER, "DEPTH -> %i , tag -> %s, attrs -> %s" % (self.__depth, tag, repr(attrs)), "down")
+ if self.__depth == self._dispatch_depth:
+ if not self._mini_dom:
+ self._mini_dom = Node(tag=tag, attrs=attrs, nsp=self._document_nsp, node_built=True)
+ else:
+ Node.__init__(self._mini_dom, tag=tag, attrs=attrs, nsp=self._document_nsp, node_built=True)
+ self._ptr = self._mini_dom
+ elif self.__depth > self._dispatch_depth:
+ self._ptr.kids.append(Node(tag=tag, parent=self._ptr, attrs=attrs, node_built=True))
+ self._ptr = self._ptr.kids[-1]
+ if self.__depth == 1:
+ self._document_attrs = {}
+ self._document_nsp = {}
+ nsp, name = ([""] + tag.split(":"))[-2:]
+ for attr, val in attrs.items():
+ if attr == "xmlns":
+ self._document_nsp[""] = val
+ elif attr.startswith("xmlns:"):
+ self._document_nsp[attr[6:]] = val
+ else:
+ self._document_attrs[attr] = val
+ ns = self._document_nsp.get(nsp, "http://www.gajim.org/xmlns/undeclared-root")
+ try:
+ self.stream_header_received(ns, name, attrs)
+ except ValueError as e:
+ self._document_attrs = None
+ raise ValueError(str(e))
+ if not self.last_is_data and self._ptr.parent:
+ self._ptr.parent.data.append("")
+ self.last_is_data = 0
+
+ def endtag(self, tag):
+ """
+ XML Parser callback. Used internally.
+ """
+ self.DEBUG(DBG_NODEBUILDER, "DEPTH -> %i , tag -> %s" % (self.__depth, tag), "up")
+ self.check_data_buffer()
+ if self.__depth == self._dispatch_depth:
+ if self._mini_dom and self._mini_dom.getName() == "error":
+ self.streamError = self._mini_dom.getChildren()[0].getName()
+ self.dispatch(self._mini_dom)
+ elif self.__depth > self._dispatch_depth:
+ self._ptr = self._ptr.parent
+ else:
+ self.DEBUG(DBG_NODEBUILDER, "Got higher than dispatch level. Stream terminated?", "stop")
+ self._dec_depth()
+ self.last_is_data = 0
+ if not self.__depth:
+ self.stream_footer_received()
+
+ def handle_cdata(self, data):
+ """
+ XML Parser callback. Used internally.
+ """
+ self.DEBUG(DBG_NODEBUILDER, data, "data")
+ if self.last_is_data:
+ if self.data_buffer:
+ self.data_buffer.append(data)
+ elif self._ptr:
+ self.data_buffer = [data]
+ self.last_is_data = 1
+
+ def handle_namespace_start(self, prefix, uri):
+ """
+ XML Parser callback. Used internally.
+ """
+ self.check_data_buffer()
+
+ def DEBUG(self, level, text, comment=None):
+ """
+ Gets all NodeBuilder walking events. Can be used for debugging if redefined.
+ """
+ def getDom(self):
+ """
+ Returns just built Node.
+ """
+ self.check_data_buffer()
+ return self._mini_dom
+
+ def dispatch(self, stanza):
+ """
+ Gets called when the NodeBuilder reaches some level of depth on it's way up with the built
+ node as argument. Can be redefined to convert incoming XML stanzas to program events.
+ """
+
+ def stream_header_received(self, ns, tag, attrs):
+ """
+ Method called when stream just opened.
+ """
+ self.check_data_buffer()
+
+ def stream_footer_received(self):
+ """
+ Method called when stream just closed.
+ """
+ self.check_data_buffer()
+
+ def has_received_endtag(self, level=0):
+ """
+ Return True if at least one end tag was seen (at level).
+ """
+ return self.__depth <= level and self.__max_depth > level
+
+ def _inc_depth(self):
+ self.__last_depth = self.__depth
+ self.__depth += 1
+ self.__max_depth = max(self.__depth, self.__max_depth)
+
+ def _dec_depth(self):
+ self.__last_depth = self.__depth
+ self.__depth -= 1
+
+def XML2Node(xml):
+ """
+ Converts supplied textual string into XML node. Handy f.e. for reading configuration file.
+ Raises xml.parser.expat.parsererror if provided string is not well-formed XML.
+ """
+ return NodeBuilder(xml).getDom()
+
+def BadXML2Node(xml):
+ """
+ Converts supplied textual string into XML node. Survives if xml data is cutted half way round.
+ I.e. "<html>some text <br>some more text". Will raise xml.parser.expat.parsererror on misplaced
+ tags though. F.e. "<b>some text <br>some more text</b>" will not work.
+ """
+ return NodeBuilder(xml).getDom()
diff --git a/xmpp/transports.py b/xmpp/transports.py
new file mode 100644
index 0000000..0a12a74
--- /dev/null
+++ b/xmpp/transports.py
@@ -0,0 +1,403 @@
+## transports.py
+##
+## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2, or (at your option)
+## any later version.
+##
+## 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 General Public License for more details.
+
+# $Id: transports.py, v1.36 2013/11/03 alkorgun Exp $
+
+"""
+This module contains the low-level implementations of xmpppy connect methods or
+(in other words) transports for xmpp-stanzas.
+Currently here is three transports:
+direct TCP connect - TCPsocket class
+proxied TCP connect - HTTPPROXYsocket class (CONNECT proxies)
+TLS connection - TLS class. Can be used for SSL connections also.
+
+Transports are stackable so you - f.e. TLS use HTPPROXYsocket or TCPsocket as more low-level transport.
+
+Also exception 'error' is defined to allow capture of this module specific exceptions.
+"""
+
+import sys
+import socket
+import dispatcher
+
+from base64 import encodestring
+from select import select
+from simplexml import ustr
+from plugin import PlugIn
+from protocol import *
+
+# http://pydns.sourceforge.net
+try:
+ import dns
+except ImportError:
+ dns = None
+
+DATA_RECEIVED = 'DATA RECEIVED'
+DATA_SENT = 'DATA SENT'
+DBG_CONNECT_PROXY = 'CONNECTproxy'
+
+BUFLEN = 1024
+
+class error:
+ """
+ An exception to be raised in case of low-level errors in methods of 'transports' module.
+ """
+ def __init__(self, comment):
+ """
+ Cache the descriptive string.
+ """
+ self._comment = comment
+
+ def __str__(self):
+ """
+ Serialize exception into pre-cached descriptive string.
+ """
+ return self._comment
+
+class TCPsocket(PlugIn):
+ """
+ This class defines direct TCP connection method.
+ """
+ def __init__(self, server=None, use_srv=True):
+ """
+ Cache connection point 'server'. 'server' is the tuple of (host, port)
+ absolutely the same as standard tcp socket uses. However library will lookup for
+ ('_xmpp-client._tcp.' + host) SRV record in DNS and connect to the found (if it is)
+ server instead.
+ """
+ PlugIn.__init__(self)
+ self.DBG_LINE = "socket"
+ self._exported_methods = [self.send, self.disconnect]
+ self._server, self.use_srv = server, use_srv
+
+ def srv_lookup(self, server):
+ """
+ SRV resolver. Takes server=(host, port) as argument. Returns new (host, port) pair.
+ """
+ if dns:
+ query = "_xmpp-client._tcp.%s" % server[0]
+ try:
+ dns.DiscoverNameServers()
+ dns__ = dns.Request()
+ response = dns__.req(query, qtype="SRV")
+ if response.answers:
+ (port, host) = response.answers[0]["data"][2:]
+ server = str(host), int(port)
+ except dns.DNSError:
+ self.DEBUG("An error occurred while looking up %s." % query, "warn")
+ return server
+
+ def plugin(self, owner):
+ """
+ Fire up connection. Return non-empty string on success.
+ Also registers self.disconnected method in the owner's dispatcher.
+ Called internally.
+ """
+ if not self._server:
+ self._server = (self._owner.Server, 5222)
+ if self.use_srv:
+ server = self.srv_lookup(self._server)
+ else:
+ server = self._server
+ if not self.connect(server):
+ return None
+ self._owner.Connection = self
+ self._owner.RegisterDisconnectHandler(self.disconnected)
+ return "ok"
+
+ def getHost(self):
+ """
+ Returns the 'host' value that is connection is [will be] made to.
+ """
+ return self._server[0]
+
+ def getPort(self):
+ """
+ Returns the 'port' value that is connection is [will be] made to.
+ """
+ return self._server[1]
+
+ def connect(self, server=None):
+ """
+ Try to connect to the given host/port. Does not lookup for SRV record.
+ Returns non-empty string on success.
+ """
+ if not server:
+ server = self._server
+ host, port = server
+ server = (host, int(port))
+ if ":" in host:
+ sock = socket.AF_INET6
+ server = server.__add__((0, 0))
+ else:
+ sock = socket.AF_INET
+ try:
+ self._sock = socket.socket(sock, socket.SOCK_STREAM)
+ self._sock.connect(server)
+ self._send = self._sock.sendall
+ self._recv = self._sock.recv
+ except socket.error, (errno, strerror):
+ self.DEBUG("Failed to connect to remote host %s: %s (%s)" % (repr(server), strerror, errno), "error")
+ except:
+ pass
+ else:
+ self.DEBUG("Successfully connected to remote host %s." % repr(server), "start")
+ return "ok"
+
+ def plugout(self):
+ """
+ Disconnect from the remote server and unregister self.disconnected method from
+ the owner's dispatcher.
+ """
+ self._sock.close()
+ if hasattr(self._owner, "Connection"):
+ del self._owner.Connection
+ self._owner.UnregisterDisconnectHandler(self.disconnected)
+
+ def receive(self):
+ """
+ Reads all pending incoming data.
+ In case of disconnection calls owner's disconnected() method and then raises IOError exception.
+ """
+ try:
+ data = self._recv(BUFLEN)
+ except socket.sslerror as e:
+ self._seen_data = 0
+ if e[0] in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE):
+ return ""
+ self.DEBUG("Socket error while receiving data.", "error")
+ sys.exc_clear()
+ self._owner.disconnected()
+ raise IOError("Disconnected!")
+ except:
+ data = ""
+ while self.pending_data(0):
+ try:
+ add = self._recv(BUFLEN)
+ except:
+ break
+ if not add:
+ break
+ data += add
+ if data:
+ self._seen_data = 1
+ self.DEBUG(data, "got")
+ if hasattr(self._owner, "Dispatcher"):
+ self._owner.Dispatcher.Event("", DATA_RECEIVED, data)
+ else:
+ self.DEBUG("Socket error while receiving data.", "error")
+ sys.exc_clear()
+ self._owner.disconnected()
+ raise IOError("Disconnected!")
+ return data
+
+ def send(self, data):
+ """
+ Writes raw outgoing data. Blocks until done.
+ If supplied data is unicode string, encodes it to utf-8 before send.
+ """
+ if isinstance(data, unicode):
+ data = data.encode("utf-8")
+ elif not isinstance(data, str):
+ data = ustr(data).encode("utf-8")
+ try:
+ self._send(data)
+ except:
+ self.DEBUG("Socket error while sending data.", "error")
+ self._owner.disconnected()
+ else:
+ if not data.strip():
+ data = repr(data)
+ self.DEBUG(data, "sent")
+ if hasattr(self._owner, "Dispatcher"):
+ self._owner.Dispatcher.Event("", DATA_SENT, data)
+
+ def pending_data(self, timeout=0):
+ """
+ Returns true if there is a data ready to be read.
+ """
+ return select([self._sock], [], [], timeout)[0]
+
+ def disconnect(self):
+ """
+ Closes the socket.
+ """
+ self.DEBUG("Closing socket.", "stop")
+ self._sock.close()
+
+ def disconnected(self):
+ """
+ Called when a Network Error or disconnection occurs.
+ Designed to be overidden.
+ """
+ self.DEBUG("Socket operation failed.", "error")
+
+class HTTPPROXYsocket(TCPsocket):
+ """
+ HTTP (CONNECT) proxy connection class. Uses TCPsocket as the base class
+ redefines only connect method. Allows to use HTTP proxies like squid with
+ (optionally) simple authentication (using login and password).
+ """
+ def __init__(self, proxy, server, use_srv=True):
+ """
+ Caches proxy and target addresses.
+ 'proxy' argument is a dictionary with mandatory keys 'host' and 'port' (proxy address)
+ and optional keys 'user' and 'password' to use for authentication.
+ 'server' argument is a tuple of host and port - just like TCPsocket uses.
+ """
+ TCPsocket.__init__(self, server, use_srv)
+ self.DBG_LINE = DBG_CONNECT_PROXY
+ self._proxy = proxy
+
+ def plugin(self, owner):
+ """
+ Starts connection. Used interally. Returns non-empty string on success.
+ """
+ owner.debug_flags.append(DBG_CONNECT_PROXY)
+ return TCPsocket.plugin(self, owner)
+
+ def connect(self, dupe=None):
+ """
+ Starts connection. Connects to proxy, supplies login and password to it
+ (if were specified while creating instance). Instructs proxy to make
+ connection to the target server. Returns non-empty sting on success.
+ """
+ if not TCPsocket.connect(self, (self._proxy["host"], self._proxy["port"])):
+ return None
+ self.DEBUG("Proxy server contacted, performing authentification.", "start")
+ connector = [
+ "CONNECT %s:%s HTTP/1.0" % self._server,
+ "Proxy-Connection: Keep-Alive",
+ "Pragma: no-cache",
+ "Host: %s:%s" % self._server,
+ "User-Agent: HTTPPROXYsocket/v0.1"
+ ]
+ if "user" in self._proxy and "password" in self._proxy:
+ credentials = "%s:%s" % (self._proxy["user"], self._proxy["password"])
+ credentials = encodestring(credentials).strip()
+ connector.append("Proxy-Authorization: Basic " + credentials)
+ connector.append("\r\n")
+ self.send("\r\n".join(connector))
+ try:
+ reply = self.receive().replace("\r", "")
+ except IOError:
+ self.DEBUG("Proxy suddenly disconnected.", "error")
+ self._owner.disconnected()
+ return None
+ try:
+ proto, code, desc = reply.split("\n")[0].split(" ", 2)
+ except:
+ raise error("Invalid proxy reply")
+ if code != "200":
+ self.DEBUG("Invalid proxy reply: %s %s %s" % (proto, code, desc), "error")
+ self._owner.disconnected()
+ return None
+ while reply.find("\n\n") == -1:
+ try:
+ reply += self.receive().replace("\r", "")
+ except IOError:
+ self.DEBUG("Proxy suddenly disconnected.", "error")
+ self._owner.disconnected()
+ return None
+ self.DEBUG("Authentification successfull. Jabber server contacted.", "ok")
+ return "ok"
+
+ def DEBUG(self, text, severity):
+ """
+ Overwrites DEBUG tag to allow debug output be presented as 'CONNECTproxy'.
+ """
+ return self._owner.DEBUG(DBG_CONNECT_PROXY, text, severity)
+
+class TLS(PlugIn):
+ """
+ TLS connection used to encrypts already estabilished tcp connection.
+ """
+ def PlugIn(self, owner, now=0):
+ """
+ If the 'now' argument is true then starts using encryption immidiatedly.
+ If 'now' in false then starts encryption as soon as TLS feature is
+ declared by the server (if it were already declared - it is ok).
+ """
+ if hasattr(owner, "TLS"):
+ return None
+ PlugIn.PlugIn(self, owner)
+ DBG_LINE = "TLS"
+ if now:
+ return self._startSSL()
+ if self._owner.Dispatcher.Stream.features:
+ try:
+ self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features)
+ except NodeProcessed:
+ pass
+ else:
+ self._owner.RegisterHandlerOnce("features", self.FeaturesHandler, xmlns=NS_STREAMS)
+ self.starttls = None
+
+ def plugout(self, now=0):
+ """
+ Unregisters TLS handler's from owner's dispatcher. Take note that encription
+ can not be stopped once started. You can only break the connection and start over.
+ """
+ self._owner.UnregisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS)
+ self._owner.UnregisterHandler("proceed", self.StartTLSHandler, xmlns=NS_TLS)
+ self._owner.UnregisterHandler("failure", self.StartTLSHandler, xmlns=NS_TLS)
+
+ def FeaturesHandler(self, conn, feats):
+ """
+ Used to analyse server <features/> tag for TLS support.
+ If TLS is supported starts the encryption negotiation. Used internally.
+ """
+ if not feats.getTag("starttls", namespace=NS_TLS):
+ self.DEBUG("TLS unsupported by remote server.", "warn")
+ return None
+ self.DEBUG("TLS supported by remote server. Requesting TLS start.", "ok")
+ self._owner.RegisterHandlerOnce("proceed", self.StartTLSHandler, xmlns=NS_TLS)
+ self._owner.RegisterHandlerOnce("failure", self.StartTLSHandler, xmlns=NS_TLS)
+ self._owner.Connection.send("<starttls xmlns=\"%s\"/>" % NS_TLS)
+ raise NodeProcessed()
+
+ def pending_data(self, timeout=0):
+ """
+ Returns true if there possible is a data ready to be read.
+ """
+ return self._tcpsock._seen_data or select([self._tcpsock._sock], [], [], timeout)[0]
+
+ def _startSSL(self):
+ tcpsock = self._owner.Connection
+ tcpsock._sslObj = socket.ssl(tcpsock._sock, None, None)
+ tcpsock._sslIssuer = tcpsock._sslObj.issuer()
+ tcpsock._sslServer = tcpsock._sslObj.server()
+ tcpsock._recv = tcpsock._sslObj.read
+ tcpsock._send = tcpsock._sslObj.write
+ tcpsock._seen_data = 1
+ self._tcpsock = tcpsock
+ tcpsock.pending_data = self.pending_data
+ tcpsock._sock.setblocking(0)
+ self.starttls = "success"
+
+ def StartTLSHandler(self, conn, starttls):
+ """
+ Handle server reply if TLS is allowed to process. Behaves accordingly.
+ Used internally.
+ """
+ if starttls.getNamespace() != NS_TLS:
+ return None
+ self.starttls = starttls.getName()
+ if self.starttls == "failure":
+ self.DEBUG("Got starttls response: " + self.starttls, "error")
+ return None
+ self.DEBUG("Got starttls proceed response. Switching to TLS/SSL...", "ok")
+ self._startSSL()
+ self._owner.Dispatcher.PlugOut()
+ dispatcher.Dispatcher().PlugIn(self._owner)