diff options
author | alkorgun <alkorgun@gmail.com> | 2013-12-07 00:05:22 +0400 |
---|---|---|
committer | alkorgun <alkorgun@gmail.com> | 2013-12-07 00:05:22 +0400 |
commit | 85e01717e7c9866bbe574fbf5ad89689b6b2fe93 (patch) | |
tree | bcfd33447a46721ff4683c59924192352f05890c /xmpp | |
parent | 20eea0ce73e9e5a29a2fb5f6b0e5f3535117cbec (diff) |
initial commit
Diffstat (limited to 'xmpp')
-rw-r--r-- | xmpp/LICENSE | 675 | ||||
-rw-r--r-- | xmpp/__init__.py | 42 | ||||
-rw-r--r-- | xmpp/auth.py | 413 | ||||
-rw-r--r-- | xmpp/browser.py | 261 | ||||
-rw-r--r-- | xmpp/client.py | 374 | ||||
-rw-r--r-- | xmpp/commands.py | 448 | ||||
-rw-r--r-- | xmpp/debug.py | 314 | ||||
-rw-r--r-- | xmpp/dispatcher.py | 477 | ||||
-rw-r--r-- | xmpp/features.py | 230 | ||||
-rw-r--r-- | xmpp/filetransfer.py | 226 | ||||
-rw-r--r-- | xmpp/plugin.py | 69 | ||||
-rw-r--r-- | xmpp/protocol.py | 1404 | ||||
-rw-r--r-- | xmpp/roster.py | 280 | ||||
-rw-r--r-- | xmpp/simplexml.py | 704 | ||||
-rw-r--r-- | xmpp/transports.py | 403 |
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 = ( + ("&", "&"), + ("\x0C", ""), + ("\x1B", ""), + ("<", "<"), + (">", ">"), + ('"', """), + ("'", "'") +) + +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) |