From dc49a30d1635bc1ddee995f3ea4b23e05172989b Mon Sep 17 00:00:00 2001 From: Christopher Zorn Date: Tue, 16 Sep 2008 18:46:36 +0000 Subject: [PATCH] importing punjab, no need for history --- INSTALL.txt | 96 +++++ LICENSE.txt | 284 +++++++++++++++ MANIFEST.in | 8 + NOTES.txt | 0 README.txt | 29 ++ html/favicon.ico | Bin 0 -> 5838 bytes html/index.html | 13 + html/punjab.gif | Bin 0 -> 14395 bytes punjab.tac | 19 + punjab/__init__.py | 86 +++++ punjab/error.py | 65 ++++ punjab/httpb.py | 734 ++++++++++++++++++++++++++++++++++++++ punjab/jabber.py | 176 +++++++++ punjab/session.py | 708 ++++++++++++++++++++++++++++++++++++ punjab/stream.py | 74 ++++ punjab/tap.py | 24 ++ punjab/xmpp/__init__.py | 0 punjab/xmpp/error.py | 35 ++ punjab/xmpp/ns.py | 24 ++ punjab/xmpp/server.py | 221 ++++++++++++ setup.py | 12 + tests/httpb_client.py | 428 ++++++++++++++++++++++ tests/testparser.py | 174 +++++++++ tests/xep124.py | 315 ++++++++++++++++ tests/xep206.py | 29 ++ twisted/plugins/punjab.py | 8 + 26 files changed, 3562 insertions(+) create mode 100644 INSTALL.txt create mode 100644 LICENSE.txt create mode 100644 MANIFEST.in create mode 100644 NOTES.txt create mode 100644 README.txt create mode 100644 html/favicon.ico create mode 100644 html/index.html create mode 100644 html/punjab.gif create mode 100644 punjab.tac create mode 100644 punjab/__init__.py create mode 100644 punjab/error.py create mode 100644 punjab/httpb.py create mode 100644 punjab/jabber.py create mode 100644 punjab/session.py create mode 100644 punjab/stream.py create mode 100644 punjab/tap.py create mode 100644 punjab/xmpp/__init__.py create mode 100644 punjab/xmpp/error.py create mode 100644 punjab/xmpp/ns.py create mode 100644 punjab/xmpp/server.py create mode 100644 setup.py create mode 100644 tests/httpb_client.py create mode 100644 tests/testparser.py create mode 100644 tests/xep124.py create mode 100644 tests/xep206.py create mode 100644 twisted/plugins/punjab.py diff --git a/INSTALL.txt b/INSTALL.txt new file mode 100644 index 0000000..3d1478b --- /dev/null +++ b/INSTALL.txt @@ -0,0 +1,96 @@ +================= +INSTALLING PUNJAB +================= + +This document contains instructions on installing punjab +on your system. + +------------- +Obtaining Punjab +------------- + +Punjab can be located at the punjab web site, + + http://www.butterfat.net/wiki/Projects/PunJab + + +------------- +Dependencies +------------- + +Please make sure all dependencies are met before submitting a troubleshooting question. + +- Python 2.5> + +- Twisted >= 2.5 + + - Twisted-words >= 0.6.0 + - Twisted-web >= 0.5.0 + + Recommended + - Twisted-conch >= 0.5.0 + + NOTE : + You can download all of these packages with the Twisted Sumo package. + http://twistedmatrix.com/ + +- pyopenssl - if you want tls to work. + +- A jabber server like jabberd2 + + +------------- +Installing Punjab +------------- + +1. Untar the current punjab-X.X.tar.gz file in a directory you would + like punjab to reside. + +shell>tar vxzf punjab-X.X.tar.gz + +2. Run setup to install + +shell>python setup.py install + +3. Configure punjab - using a tac file. + +shell>edit punjab.tac +# punjab tac file +from twisted.web import server, resource, static +from twisted.application import service, internet + +from punjab.httpb import Httpb, HttpbService + +root = static.File("./html") # a static html directory + +b = resource.IResource(HttpbService(1)) +root.putChild('bosh', b) # url for BOSH + +site = server.Site(root) + +application = service.Application("punjab") +internet.TCPServer(5280, site).setServiceParent(application) + + +4. Run punjab + +shell>twistd -y punjab.tac + +5. HAVE FUN!! + + +-------------- +Using a tac file. + + +--------------------------- +Extending Punjab +--------------------------- + +You are able to extend punjab in many ways. + +------------- +Support +------------- + + http://www.butterfat.net/wiki/Projects/PunJab diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..8e2a96c --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,284 @@ +Unless otherwise noted with in the source file all source code contained +within this product is subject to the GNU GPL listed below. + + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) 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 +this service 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 make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. 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. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE 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. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..e7f76cd --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,8 @@ +include LICENSE.txt README.txt TODO.txt INSTALL.txt NOTES.txt MANIFEST +include html/* +recursive-include punjab * +recursive-include twisted * +recursive-include html/js * +recursive-include html/css * +recursive-include html/images * +global-exclude *~ .cvs* *.pyc .svn* \ No newline at end of file diff --git a/NOTES.txt b/NOTES.txt new file mode 100644 index 0000000..e69de29 diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..96d25c5 --- /dev/null +++ b/README.txt @@ -0,0 +1,29 @@ +Copyright (C) 2001-2008 Christopher Zorn , tofu@thetofu.com + + 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 + 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, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +GENERAL INFORMATION + +PunJab is a HTTP jabber client interface. It is a BOSH server that +allows persistent client connections to a XMPP server. + +For more information about punjab see the following URL : + +http://www.butterfat.net/wiki/Projects/PunJab + + +CONTRIBUTORS + +Jack Moffitt xmpp:jackm@jabber.org - Improved HTTP Binding and Polling diff --git a/html/favicon.ico b/html/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e1c25b10f443cda2127a589436e7b656ab3d5c1f GIT binary patch literal 5838 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 literal 0 HcmV?d00001 diff --git a/html/index.html b/html/index.html new file mode 100644 index 0000000..6eeecc9 --- /dev/null +++ b/html/index.html @@ -0,0 +1,13 @@ + + + + Punjab + + + + A XEP-0124 + - BOSH - component manager. + + + + diff --git a/html/punjab.gif b/html/punjab.gif new file mode 100644 index 0000000000000000000000000000000000000000..2a37b3efc39ae09f0926625e487b9585b7ceb4ac GIT binary patch literal 14395 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
->ZO9!Oa5#^a=hq@ zk^ps>ON6*CKpo?Qa$z1HfH#n@m*$ZQbXeXAB3ZU}uta7f*)@_5tAXRjXU~NwX3?rq+Lxe4Y3awN;(EM;Ukl1#Ju1zq(s*?fw`&j z#1+MR2|FH1sTE1V-}s#4TrlhSLIREO+GLxeqDGv^2F$ToUkpdq%2VSJlCGu{CznVE z3jG9L(9SUE0Yc1|c^?JGUehtKuS)_#JVBBTDf1hjOVA;QJXSZ!pyp~S8!)lDN6~OZ zU1D5urPY=vjunzGPPVeSD0QSwRyN1!XspoTq<&Cm%pOc8TB%UdJVUy50)~lkE{4UK zo2@ihP?e_qLl7CxuHP!&$*7ym@coe0e)Jq4R7zV-707k>Qys$pF z8*zqdsK>cD+eNsZWF0V|4#XCP>_4zI2d!>e^H0@#5%qy9O3J;((%bW-!<7_+56+ZL zc2_`*>`}^Di)h8ES=y`mbq=M2o$U%=kEzr2ueyWAw|m8M{N!MosH@*9*=aOy@~ffR z=9#?(sGF}etNc($Lf6I;bMzFfkNN@zJ?Pa9l1`1!L+UUcqX^@PaM><3Ms*r6(HAn% zTT#ELsXk`vut6nWM61hAN`WCp-DGT46K%vr?>#zm?iwGJ4qEFIsxUPWb*PjdiK7$E zfwF4%8?mEeJfse2Vda`KDcZydGKyg6B<2ptb(fSA7R-A@ z9ZGT=?LZ}L9(ugg;fV}upVzW{Xk^CE_%NHPb%Fn1>Kwtgdd-S{9-b>FQsbMXU!}03 zy(fT@PS|;ze%vybk)BgGuF$WFgF$vSqt2x2Lnl}jx9E&+XV7!%*wJJDAxFpYyaSI? z#x?ztti-j=C7$+hz+~%mE;l~APYhqkz7NC0m^yLoU{QQT0G4NkzLm>E4)PmSOgPu}HxRN~+uL^qhT>4J)Iihm;igF>t|wKllm;v z-sq}mDJ?}Y1G$brZgCmz5~xGZ0ZWEY6t77GwBAV(Ia{_TM6aRb^AO1;PzOV@(4R#; zFA-J2kk`7WLr*l3M&wh}Z#}Yy#^)(@(8y1p3#d(s2@NMExxK@K8jszYl8KEloQn3M8euTmTAff^t{R`r+^j%C*He~@ zgMw%3qxF3GWK~pnzx#KKWSxLIN@&rFBV-5*`7D)d(x!)zEhXu|oUA@II`srSTc548 z&_b>wB0Z{<_Qdow>*#3ur2~l)57+(LBk^KC#nS*?Q|AVHY6v=$YGB_o@1ls7-!Ax+ z*-hi~oI0|^&kkSvi?#LIuh4pyhUC)fC7-(*W7tTrSCHr&;9Y`7m}m2ubK3aaoCeN9 zMGg3UcZrBYxE+q5h>uJ4o!1ZtTpR-)1 ZguDOYb9@n{J_>aJ_EG" + + self.currElem.children.append(e) + e.parent = self.currElem + + self.currRawElem = self.currRawElem + raw_xml + self.currElem = e + # New document + else: + self.currRawElem = u'' + self.documentStarted = 1 + self.DocumentStartEvent(e) + + def _onEndElement(self, _): + # Check for null current elem; end of doc + if self.currElem is None: + self.DocumentEndEvent() + + # Check for parent that is None; that's + # the top of the stack + elif self.currElem.parent is None: + if len(self.currElem.children)>0: + self.currRawElem = self.currRawElem + "" + else: + self.currRawElem = self.currRawElem + "/>" + self.ElementEvent(self.currElem, self.currRawElem) + self.currElem = None + self.currRawElem = u'' + # Anything else is just some element in the current + # packet wrapping up + else: + if len(self.currElem.children)==0: + self.currRawElem = self.currRawElem + "/>" + else: + self.currRawElem = self.currRawElem + "" + self.currElem = self.currElem.parent + + def _onCdata(self, data): + if self.currElem != None: + if len(self.currElem.children)==0: + self.currRawElem = self.currRawElem + ">" + domish.escapeToXml(data) + #self.currRawElem = self.currRawElem + ">" + data + else: + self.currRawElem = self.currRawElem + domish.escapeToXml(data) + #self.currRawElem = self.currRawElem + data + + self.currElem.addContent(data) + + def _onStartNamespace(self, prefix, uri): + # If this is the default namespace, put + # it on the stack + if prefix is None: + self.defaultNsStack.append(uri) + else: + self.localPrefixes[prefix] = uri + + def _onEndNamespace(self, prefix): + # Remove last element on the stack + if prefix is None: + self.defaultNsStack.pop() + +def elementStream(): + """ Preferred method to construct an ElementStream + + Uses Expat-based stream if available, and falls back to Sux if necessary. + """ + try: + es = HttpbElementStream() + return es + except ImportError: + if domish.SuxElementStream is None: + raise Exception("No parsers available :(") + es = domish.SuxElementStream() + return es + +# make httpb body class, similar to xmlrpclib +# +class HttpbParse: + """ + An xml parser for parsing the body elements. + """ + def __init__(self, use_t=False): + """ + Call reset to initialize object + """ + self.use_t = use_t # use domish element stream + self._reset() + + + def parse(self, buf): + """ + Parse incoming xml and return the body and its children in a list + """ + self.stream.parse(buf) + + # return the doc element and its children in a list + return self.body, self.xmpp_elements + + def serialize(self, obj): + """ + Turn object into a string type + """ + if isinstance(obj, domish.Element): + obj = obj.toXml() + return obj + + def onDocumentStart(self, rootelem): + """ + The body document has started. + + This should be a body. + """ + if rootelem.name == 'body': + self.body = rootelem + + def onElement(self, element, raw_element = None): + """ + A child element has been found. + """ + if isinstance(element, domish.Element): + if raw_element: + self.xmpp_elements.append(raw_element) + else: + self.xmpp_elements.append(element) + else: + pass + + def _reset(self): + """ + Setup the parser + """ + if not self.use_t: + self.stream = elementStream() + else: + self.stream = domish.elementStream() + + self.stream.DocumentStartEvent = self.onDocumentStart + self.stream.ElementEvent = self.onElement + self.stream.DocumentEndEvent = self.onDocumentEnd + self.body = "" + self.xmpp_elements = [] + + + def onDocumentEnd(self): + """ + Body End + """ + pass + +class IHttpbService(Interface): + """ + Interface for http binding class + """ + def __init__(self, verbose): + """ """ + + def startSession(self, body): + """ Start a punjab jabber session """ + + def endSession(self, session): + """ end a punjab jabber session """ + + def onExpire(self, session_id): + """ preform actions based on when the jabber connection expires """ + + def parseBody(self, body): + """ parse a body element """ + + + def error(self, error): + """ send a body error element """ + + + def inSession(self, body): + """ """ + + def getXmppElements(self, body, session): + """ """ + + + +class IHttpbFactory(Interface): + """ + Factory class for generating binding sessions. + """ + def startSession(self): + """ Start a punjab jabber session """ + + def endSession(self, session): + """ end a punjab jabber session """ + + def parseBody(self, body): + """ parse an body element """ + + def buildProtocol(self, addr): + """Return a protocol """ + + + +class Httpb(resource.Resource): + """ + Http resource to handle BOSH requests. + """ + isLeaf = True + def __init__(self, service, v = 0): + """Initialize. + """ + resource.Resource.__init__(self) + self.service = service + self.hp = None + self.children = {} + self.client = 0 + self.verbose = v + + self.polling = self.service.polling or 15 + + def render_GET(self, request): + """ + GET is not used, print docs. + """ + return """ + + XEP-0124 - BOSH + + """ + + def render_POST(self, request): + """ + Parse received xml + """ + request.content.seek(0, 0) + if self.service.v: + log.msg('HEADERS %s:' % (str(time.time()),)) + log.msg(request.received_headers) + log.msg("HTTPB POST : ") + log.msg(str(request.content.read())) + request.content.seek(0, 0) + + self.hp = HttpbParse() + try: + body_tag, xmpp_elements = self.hp.parse(request.content.read()) + self.hp._reset() + + if getattr(body_tag, 'name', '') != "body": + log.msg('Client sent bad POST data') + self.send_http_error(400, request) + return server.NOT_DONE_YET + except domish.ParserError: + log.msg('ERROR: Xml Parse Error') + self.hp._reset() + self.send_http_error(400, request) + return server.NOT_DONE_YET + except: + log.err() + # reset parser, just in case + self.hp._reset() + self.send_http_error(400, request) + return server.NOT_DONE_YET + else: + if self.service.inSession(body_tag): + # sid is an existing session + if body_tag.getAttribute('rid'): + request.rid = body_tag['rid'] + log.msg(request.rid) + + s, d = self.service.parseBody(body_tag, xmpp_elements) + d.addCallback(self.return_httpb, s, request) + elif body_tag.hasAttribute('sid'): + log.msg("no sid is found but the body element has a 'sid' attribute") + # This is an error, no sid is found but the body element has a 'sid' attribute + self.send_http_error(404, request) + return server.NOT_DONE_YET + else: + # start session + s, d = self.service.startSession(body_tag, xmpp_elements) + d.addCallback(self.return_session, s, request) + + # Add an error back for returned errors + d.addErrback(self.return_error, request) + return server.NOT_DONE_YET + + + def return_session(self, data, session, request): + # create body + if session.xmlstream is None: + self.send_http_error(200, request, 'remote-connection-failed', + 'terminate') + return server.NOT_DONE_YET + + b = domish.Element((NS_BIND, "body")) + # if we don't have an authid, we have to fail + if session.authid != 0: + b['authid'] = session.authid + else: + self.send_http_error(500, request, 'internal-server-error', + 'terminate') + return server.NOT_DONE_YET + + b['sid'] = session.sid + b['wait'] = str(session.wait) + if session.secure == 0: + b['secure'] = 'false' + else: + b['secure'] = 'true' + + b['inactivity'] = str(session.inactivity) + ##b['polling'] = '15' # TODO: make this configurable + b['polling'] = str(self.polling) + b['requests'] = str(session.hold + 1) + b['window'] = str(session.window) + + punjab.uriCheck(b, NS_BIND) + if session.attrs.has_key('content'): + b['content'] = session.attrs['content'] + + # We need to send features + while len(data) > 0: + felem = data.pop(0) + if isinstance(felem, domish.Element): + b.addChild(felem) + else: + b.addRawXml(felem) + + self.return_body(request, b) + + def return_httpb(self, data, session, request): + # create body + b = domish.Element((NS_BIND, "body")) + punjab.uriCheck(b, NS_BIND) + session.touch() + if getattr(session,'terminated', False): + b['type'] = 'terminate' + if data: + b.children += data + + self.return_body(request, b, session.charset) + + + def return_error(self, e, request): + echildren = [] + + try: + # TODO - clean this up and make errors better + if getattr(e.value,'stanza_error',None): + ec = getattr(e.value, 'children', None) + if ec: + echildren = ec + + self.send_http_error(error.conditions[str(e.value.stanza_error)]['code'], + request, + condition = str(e.value.stanza_error), + typ = error.conditions[str(e.value.stanza_error)]['type'], + children=echildren) + + return server.NOT_DONE_YET + elif e.value: + self.send_http_error(error.conditions[str(e.value)]['code'], + request, + str(e.value), + error.conditions[str(e.value)]['type']) + return server.NOT_DONE_YET + else: + self.send_http_error(500, request, 'internal-server-error', 'error', e) + except: + log.err() + pass + + + def return_body(self, request, b, charset="utf-8"): + request.setResponseCode(200) + bxml = b.toXml(prefixes=ns.XMPP_PREFIXES.copy()).encode(charset,'replace') + request.setHeader('content-type', 'text/xml') + request.setHeader('content-length', len(bxml)) + if self.service.v: + log.msg('\n\nRETURN HTTPB %s:' % (str(time.time()),)) + log.msg(bxml) + if getattr(request, 'rid', None): + log.msg(request.rid) + request.write(bxml) + request.finish() + + def send_http_error(self, code, request, condition = 'undefined-condition', typ = 'terminate', data = '', charset = 'utf-8', children=None): + request.setResponseCode(int(code)) + + b = domish.Element((NS_BIND, "body")) + if condition: + b['condition'] = str(condition) + else: + b['condition'] = 'undefined-condition' + + if typ: + b['type'] = str(typ) + else: + b['type'] = 'terminate' + punjab.uriCheck(b, NS_BIND) + bxml = b.toXml().encode(charset, 'replace') + + if children: + b.children += children + + log.msg('HTTPB Error %d' %(int(code),)) + + if int(code) != 400 and int(code) != 404 and int(code) != 403: + if data != '': + if condition == 'see-other-uri': + b.addElement('uri', None, content = str(data)) + else: + t = b.addElement('text', content = str(data)) + t['xmlns'] = 'urn:ietf:params:xml:ns:xmpp-streams' + + bxml = b.toXml().encode(charset, 'replace') + log.msg('HTTPB Return Error: ' + str(code) + ' -> ' + bxml) + request.setHeader("content-type", "text/xml") + request.setHeader("content-length", len(bxml)) + request.write(bxml) + else: + request.setHeader("content-length", "0") + request.finish() + + +components.registerAdapter(Httpb, IHttpbService, resource.IResource) + + +class HttpbService(punjab.Service): + + implements(IHttpbService) + + def __init__(self, verbose = 0, polling = 15, use_raw = False): + self.v = verbose + self.sessions = {} + self.counter = 0 + self.polling = polling + # self.expired = {} + self.use_raw = use_raw + + # run a looping call to do pollTimeouts on sessions + self.poll_timeouts = task.LoopingCall(self._doPollTimeOuts) + + self.poll_timeouts.start(3) # run every 3 seconds + + def _doPollTimeOuts(self): + """ + Call poll time outs on sessions that have waited too long. + """ + time_now = time.time() + 2.9 # need a number to offset the poll timeouts + for session in self.sessions.itervalues(): + if len(session.waiting_requests)>0: + for wr in session.waiting_requests: + if time_now - wr.wait_start >= wr.timeout: + wr.delayedcall(wr.deferred) + + + def startSession(self, body, xmpp_elements): + """ Start a punjab jabber session """ + + # look for rid + if not body.hasAttribute('rid') or body['rid']=='': + log.msg('start session called but we had a rid') + return None, defer.fail(error.NotFound) + + # look for to + if not body.hasAttribute('to') or body['to']=='': + return None, defer.fail(error.BadRequest) + + # look for wait + if not body.hasAttribute('wait') or body['wait']=='': + body['wait'] = 3 + + # look for lang + lang = None + if not body.hasAttribute("xml:lang") or body['xml:lang']=='': + for k in body.attributes: + if isinstance(k, tuple): + if str(k[1])=='lang' and body.getAttribute(k) !='': + lang = body.getAttribute(k) + if lang: + body['lang'] = lang + if not body.hasAttribute('inactivity'): + body['inactivity'] = 60 + + return make_session(self, body.attributes) + + + def parseBody(self, body, xmpp_elements): + try: + # grab session + if body.hasAttribute('sid'): + sid = str(body['sid']) + else: + log.msg('Session ID not found') + return None, defer.fail(error.NotFound) + + if self.inSession(body): + s = self.sessions[sid] + s.touch() # any connection should be a renew on wait + else: + log.msg('session does not exist?') + return None, defer.fail(error.NotFound) + ## XXX this seems to break xmpp:restart='true' --vargas + ## (cf. http://www.xmpp.org/extensions/xep-0206.html#preconditions-sasl [Example 10]) +## if body.hasAttribute('to') and body['to']!='': +## return s, defer.fail(error.BadRequest) + + # check for keys + # TODO - clean this up + foundNewKey = False + + if body.hasAttribute('newkey'): + newkey = body['newkey'] + s.key = newkey + foundNewKey = True + try: + if body.hasAttribute('key') and not foundNewKey: + if s.key is not None: + nk = sha.new(body['key']) + key = nk.hexdigest() + next_key = body['key'] + if key == s.key: + s.key = next_key + else: + log.msg('Error in key') + return s, defer.fail(error.NotFound) + else: + log.err() + raise s, defer.fail(error.NotFound) + + except: + log.msg('HTTPB ERROR: ') + log.err() + return s, defer.fail(error.NotFound) + + # need to check if this is a valid rid (within tolerance) + if body.hasAttribute('rid') and body['rid']!='': + if s.cache_data.has_key(int(body['rid'])): + s.touch() + # implements issue 32 and returns the data returned on a dropped connection + return s, defer.succeed(s.cache_data[int(body['rid'])]) + if abs(int(body['rid']) - int(s.rid)) > s.window: + log.msg('This rid is invalid %s %s ' % (str(body['rid']), str(s.rid),)) + return s, defer.fail(error.NotFound) + else: + log.msg('There is no rid on this request') + return s, defer.fail(error.NotFound) + + return s, self._parse(s, body, xmpp_elements) + + + except: + log.err() + return s, defer.fail(error.InternalServerError) + + + def onExpire(self, session_id): + """ preform actions based on when the jabber connection expires """ + log.msg('expire (%s)' % (str(session_id),)) + log.msg(len(self.sessions.keys())) + + def _parse(self, session, body_tag, xmpp_elements): + # increment the request counter + session.rid = session.rid + 1 + dont_poll = False + d = None + + if getattr(session, 'stream_error', None) != None: + # set up waiting request + d = defer.Deferred() + d.errback(session.stream_error) + session.elems = [] + session.terminate() + + dont_poll = True + else: + # send all the elements + for el in xmpp_elements: + if not isinstance(el, domish.Element): + session.sendRawXml(el) + continue + + # something is wrong here, need to figure out what + # the xmlns will be lost if this is not done + # punjab.uriCheck(el,NS_BIND) + # if el.uri and el.uri != NS_BIND: + # el['xmlns'] = el.uri + # TODO - get rid of this when we stop supporting old versions + # of twisted.words + if el.uri == NS_BIND: + el.uri = None + if el.defaultUri == NS_BIND: + el.defaultUri = None + + session.sendRawXml(el) + + + if body_tag.hasAttribute('type') and \ + body_tag['type'] == 'terminate': + d = session.terminate() + elif not dont_poll: + # normal request + d = session.poll(d, rid = int(body_tag['rid'])) + + return d + + def _returnIq(self, cur_session, d, iq): + """ + A callback from auth iqs + """ + return cur_session.poll(d) + + def _cbIq(self, iq, cur_session, d): + """ + A callback from auth iqs + """ + + # session.elems.append(iq) + return cur_session.poll(d) + + + def inSession(self, body): + """ """ + if body.hasAttribute('sid'): + if self.sessions.has_key(body['sid']): + return True + return False + + def getXmppElements(self, b, session): + """ + Get waiting xmpp elements + """ + for i, obj in enumerate(session.msgs): + m = session.msgs.pop(0) + b.addChild(m) + for i, obj in enumerate(session.prs): + p = session.prs.pop(0) + b.addChild(p) + for i, obj in enumerate(session.iqs): + iq = session.iqs.pop(0) + b.addChild(iq) + + return b + + def endSession(self, cur_session): + """ end a punjab jabber session """ + d = cur_session.terminate() + return d + diff --git a/punjab/jabber.py b/punjab/jabber.py new file mode 100644 index 0000000..8c1a4d6 --- /dev/null +++ b/punjab/jabber.py @@ -0,0 +1,176 @@ +# punjab's jabber client +from twisted.internet import reactor, error +from twisted.words.protocols.jabber import client, jid +from twisted.python import log +from copy import deepcopy + +from twisted.words import version +hasNewTwisted = version.major >= 8 +if version.major == 0 and version.minor < 5: raise Exception, "Unsupported Version of Twisted Words" + +from twisted.words.xish import domish +from twisted.words.protocols.jabber import xmlstream + + +INVALID_USER_EVENT = "//event/client/basicauth/invaliduser" +AUTH_FAILED_EVENT = "//event/client/basicauth/authfailed" +REGISTER_FAILED_EVENT = "//event/client/basicauth/registerfailed" + +# event funtions + +from punjab.xmpp.ns import XMPP_PREFIXES + +def basic_connect(jabberid, secret, host, port, cb, v=0): + myJid = jid.JID(jabberid) + factory = client.basicClientFactory(myJid,secret) + factory.v = v + factory.addBootstrap('//event/stream/authd',cb) + reactor.connectTCP(host,port,factory) + return factory + +def basic_disconnect(f, xmlstream): + sh = "" + xmlstream.send(sh) + f.stopTrying() + xmlstream = None + + +class JabberClientFactory(xmlstream.XmlStreamFactory): + def __init__(self, host, v=0): + """ Initialize + """ + p = self.authenticator = PunjabAuthenticator(host) + xmlstream.XmlStreamFactory.__init__(self, p) + + self.pending = {} + self.maxRetries = 2 + self.host = host + self.jid = "" + self.raw_buffer = "" + + if v!=0: + self.v = v + self.rawDataOutFn = self.rawDataOut + self.rawDataInFn = self.rawDataIn + + def clientConnectionFailed(self, connector, reason, d = None): + if self.continueTrying: + self.connector = connector + if not reason.check(error.UserError): + self.retry() + if self.maxRetries and (self.retries > self.maxRetries): + if d: + d.errback(reason) + + + + def rawDataIn(self, buf): + log.msg("RECV: %s" % unicode(buf, 'utf-8').encode('ascii', 'replace')) + + + def rawDataOut(self, buf): + log.msg("SEND: %s" % unicode(buf, 'utf-8').encode('ascii', 'replace')) + + + +class PunjabAuthenticator(xmlstream.ConnectAuthenticator): + namespace = "jabber:client" + version = '1.0' + useTls = 1 + def connectionMade(self): + host = self.otherHost + self.streamHost = host + + self.xmlstream.useTls = self.useTls + self.xmlstream.namespace = self.namespace + self.xmlstream.otherHost = self.otherHost + if hasNewTwisted: + self.xmlstream.otherEntity = jid.internJID(self.otherHost) + self.xmlstream.prefixes = deepcopy(XMPP_PREFIXES) + self.xmlstream.sendHeader() + + def streamStarted(self, rootelem = None): + + if hasNewTwisted: # This is here for backwards compatibility + xmlstream.ConnectAuthenticator.streamStarted(self, rootelem) + else: + xmlstream.ConnectAuthenticator.streamStarted(self) + if rootelem is None: + self.xversion = 3 + return + + self.xversion = 0 + if rootelem.hasAttribute('version'): + self.version = rootelem['version'] + else: + self.version = 0.0 + + def associateWithStream(self, xs): + + xmlstream.ConnectAuthenticator.associateWithStream(self, xs) + + inits = [ (xmlstream.TLSInitiatingInitializer, False), + # (IQAuthInitializer, True), + ] + + for initClass, required in inits: + init = initClass(xs) + init.required = required + xs.initializers.append(init) + + + def _reset(self): + log.msg('\n\n======================= reset ====================\n\n') + # need this to be in xmlstream + self.xmlstream.stream = domish.elementStream() + self.xmlstream.stream.DocumentStartEvent = self.xmlstream.onDocumentStart + self.xmlstream.stream.ElementEvent = self.xmlstream.onElement + self.xmlstream.stream.DocumentEndEvent = self.xmlstream.onDocumentEnd + self.xmlstream.prefixes = deepcopy(XMPP_PREFIXES) + # Generate stream header + + if self.version != 0.0: + sh = "" % \ + (self.namespace,self.version, self.streamHost.encode('utf-8')) + + self.xmlstream.send(str(sh)) + + + def sendAuth(self, jid, passwd, callback, errback = None): + self.jid = jid + self.passwd = passwd + if errback: + self.xmlstream.addObserver(INVALID_USER_EVENT,errback) + self.xmlstream.addObserver(AUTH_FAILED_EVENT,errback) + if self.version != '1.0': + + iq = client.IQ(self.xmlstream, "get") + iq.addElement(("jabber:iq:auth", "query")) + iq.query.addElement("username", content = jid.user) + iq.addCallback(callback) + iq.send() + + + def authQueryResultEvent(self, iq, callback): + if iq["type"] == "result": + # Construct auth request + iq = client.IQ(self.xmlstream, "set") + iq.addElement(("jabber:iq:auth", "query")) + iq.query.addElement("username", content = self.jid.user) + iq.query.addElement("resource", content = self.jid.resource) + + # Prefer digest over plaintext + if client.DigestAuthQry.matches(iq): + digest = xmlstream.hashPassword(self.xmlstream.sid, self.passwd) + iq.query.addElement("digest", content = digest) + else: + iq.query.addElement("password", content = self.passwd) + + iq.addCallback(callback) + iq.send() + else: + # Check for 401 -- Invalid user + if iq.error["code"] == "401": + self.xmlstream.dispatch(iq, INVALID_USER_EVENT) + else: + self.xmlstream.dispatch(iq, AUTH_FAILED_EVENT) diff --git a/punjab/session.py b/punjab/session.py new file mode 100644 index 0000000..dd36ef6 --- /dev/null +++ b/punjab/session.py @@ -0,0 +1,708 @@ +""" + session stuff for jabber connections + +""" +from twisted.internet import defer, reactor +from twisted.python import log +from twisted.web import server +from twisted.names.srvconnect import SRVConnector + +try: + from twisted.words.xish import domish, xmlstream +except ImportError: + from twisted.xish import domish, xmlstream + + +import traceback +import random +import md5 +from punjab import jabber +from punjab.xmpp import ns + +import time +import error + + + +class XMPPClientConnector(SRVConnector): + """ + A jabber connection to find srv records for xmpp client connections. + """ + def __init__(self, client_reactor, domain, factory): + """ Init """ + SRVConnector.__init__(self, client_reactor, 'xmpp-client', domain, factory) + + + def pickServer(self): + """ + Pick a server and port to make the connection. + """ + host, port = SRVConnector.pickServer(self) + + if not self.servers and not self.orderedServers: + # no SRV record, fall back.. + port = 5222 + if port == 5223 and xmlstream.ssl: + context = xmlstream.ssl.ClientContextFactory() + context.method = xmlstream.ssl.SSL.SSLv23_METHOD + + self.connectFunc = 'connectSSL' + self.connectFuncArgs = (context) + return host, port + +def make_session(pint, attrs, session_type='BOSH'): + """ + pint - punjab session interface class + attrs - attributes sent from the body tag + """ + + # this may need some work, idea, code taken from twisted.web.server + pint.counter = pint.counter + 1 + sid = md5.new("%s_%s_%s" % (str(time.time()), str(random.random()) , str(pint.counter))).hexdigest() + + + s = Session(pint, sid, attrs) + + s.addBootstrap(xmlstream.STREAM_START_EVENT, s.streamStart) + s.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT, s.connectEvent) + s.addBootstrap(xmlstream.STREAM_ERROR_EVENT, s.streamError) + s.addBootstrap(xmlstream.STREAM_END_EVENT, s.connectError) + + if attrs.has_key('inactivity'): + s.inactivity = attrs['inactivity'] + else: + s.inactivity = 900 # 15 mins + + s.secure = 0 + s.use_raw = getattr(pint, 'use_raw', False) # use raw buffers + + if attrs.has_key('secure') and attrs['secure'] == 'true': + s.secure = 1 + s.authenticator.useTls = 1 + else: + s.authenticator.useTls = 0 + # reactor.connectTCP(s.hostname, s.port, s) + if pint.v: + log.msg('================================== %s connect ==================================' % (str(time.time()),)) + if attrs.has_key('route'): + reactor.connectTCP(s.hostname, s.port, s) + else: + connector = XMPPClientConnector(reactor, s.hostname, s) + connector.connect() + # timeout + reactor.callLater(s.inactivity, s.checkExpired) + + pint.sessions[sid] = s + + return s, s.waiting_requests[0].deferred + + +class WaitingRequest(object): + """A helper object for managing waiting requests.""" + + def __init__(self, deferred, delayedcall, timeout = 30, startup = False, rid = None): + """ """ + self.deferred = deferred + self.delayedcall = delayedcall + self.startup = startup + self.timeout = timeout + self.wait_start = time.time() + self.rid = rid + + def doCallback(self, data): + """ """ + self.deferred.callback(data) + + def doErrback(self, data): + """ """ + self.deferred.errback(data) + + +class Session(jabber.JabberClientFactory, server.Session): + """ Jabber Client Session class for client XMPP connections. """ + def __init__(self, pint, sid, attrs): + """ + Initialize the session + """ + if attrs.has_key('charset'): + self.charset = str(attrs['charset']) + else: + self.charset = 'utf-8' + + self.to = attrs['to'] + self.port = 5222 + self.inactivity = 900 + if self.to != '' and self.to.find(":") != -1: + # Check if port is in the 'to' string + to, port = self.to.split(':') + + if port: + self.to = to + self.port = int(port) + else: + self.port = 5222 + + jabber.JabberClientFactory.__init__(self, self.to, pint.v) + server.Session.__init__(self, pint, sid) + self.pint = pint + + self.sid = sid + self.attrs = attrs + self.s = None + + self.elems = [] + rid = int(attrs['rid']) + + self.waiting_requests = [] + self.use_raw = attrs.get('raw', False) + + self.raw_buffer = u"" + self.xmpp_node = '' + self.success = 0 + self.secure = 0 + self.mechanisms = [] + self.xmlstream = None + self.features = None + self.session = None + + self.cache_data = {} + self.verbose = self.pint.v + + self.version = attrs.get('version', 0.0) + + if attrs.has_key('newkey'): + newkey = attrs['newkey'] + self.key = newkey + + self.wait = int(attrs.get('wait', 0)) + + self.hold = int(attrs.get('hold', 0)) + + if attrs.has_key('window'): + self.window = int(attrs['window']) + else: + self.window = self.hold + 2 + + if attrs.has_key('polling'): + self.polling = int(attrs['polling']) + else: + self.polling = 0 + + if attrs.has_key('port'): + self.port = int(attrs['port']) + + if attrs.has_key('hostname'): + self.hostname = attrs['hostname'] + else: + self.hostname = self.to + + if attrs.has_key('route'): + if attrs['route'].startswith("xmpp:"): + self.route = attrs['route'][5:] + if self.route.startswith("//"): + self.route = self.route[2:] + + # route format change, see http://www.xmpp.org/extensions/xep-0124.html#session-request + rhostname, rport = self.route.split(":") + self.port = int(rport) + self.hostname = rhostname + self.resource = '' + else: + raise error.Error('internal-server-error') + + + self.authid = 0 + self.rid = rid + 1 + self.connected = 0 # number of clients connected on this session + + self.notifyOnExpire(self.onExpire) + self.stream_error = None + if pint.v: + log.msg('Session Created : %s %s' % (str(self.sid),str(time.time()), )) + + # create the first waiting request + d = defer.Deferred() + timeout = 30 + self.waiting_requests.append( + WaitingRequest(d, + self._startup_timeout, + startup = True, + timeout = timeout, + rid = self.rid - 1)) + + + def rawDataIn(self, buf): + """ Log incoming data on the xmlstream """ + if self.pint.v: + try: + log.msg("SID: %s => RECV: %s" % (self.sid, unicode(buf, 'utf-8').encode('ascii', 'replace'))) + except: + log.err() + if self.use_raw and self.authid: + if type(buf) == type(''): + buf = unicode(buf, 'utf-8') + # add some raw data + self.raw_buffer = self.raw_buffer + buf + + + def rawDataOut(self, buf): + """ Log outgoing data on the xmlstream """ + try: + log.msg("SID: %s => SEND: %s" % (self.sid, unicode(buf, 'utf-8').encode('ascii', 'replace'))) + except: + log.err() + + def onExpire(self): + """ When the session expires call this. """ + if 'onExpire' in dir(self.pint): + self.pint.onExpire(self.sid) + if self.verbose and not getattr(self, 'terminated', False): + log.msg(self.sid) + log.msg(self.rid) + log.msg('SESSION -> We have expired') + self.disconnect() + + def terminate(self): + """Terminates the session.""" + self.wait = 0 + self.terminated = True + if self.verbose: + log.msg('SESSION -> Terminate') + # if there are any elements hanging around and waiting + # requests, send those off + while len(self.elems) > 0 and len(self.waiting_requests) > 0: + data = self.elems + self.elems = [] + wr = self.waiting_requests.pop(0) + wr.doCallback(data) + + # if there are any waiting requests, send them back blank + while len(self.waiting_requests) > 0: + wr = self.waiting_requests.pop(0) + wr.doCallback([]) + try: + self.expire() + except: + self.onExpire() + + + return defer.succeed(self.elems) + + def poll(self, d = None, rid = None): + """Handles the responses to requests. + + This function is called for every request except session setup + and session termination. It handles the reply portion of the + request by returning a deferred which will get called back + when there is data or when the wait timeout expires. + """ + + # queue this request + if d is None: + d = defer.Deferred() + if self.pint.error: + d.addErrback(self.pint.error) + if not rid: + rid = self.rid - 1 + self.waiting_requests.append( + WaitingRequest(d, + self._pollTimeout, + timeout = self.wait, + rid = rid)) + + # check if there is any data to send back to a request + if len(self.elems) > 0: + data = self.elems + # pop off a request and send and reply + if len(self.waiting_requests)>0: + self.elems = [] + wr = self.waiting_requests.pop(0) + wr.doCallback(data) + self._cacheData(wr.rid, data) + + # make sure we aren't queueing too many requests + while len(self.waiting_requests) > self.hold: + if len(self.elems) > 0: + data = self.elems + else: + data = [] + wr = self.waiting_requests.pop(0) + wr.doCallback(data) + self._cacheData(wr.rid, data) + + return d + + def _pollTimeout(self, d): + """Handle request timeouts. + + Since the timeout function is called, we must return an empty + reply as there is no data to send back. + """ + + # find the request that timed out and reply + pop_eye = [] + for i in range(len(self.waiting_requests)): + if self.waiting_requests[i].deferred == d: + wr = self.waiting_requests[i] + pop_eye.append(i) + self.touch() + wr.doCallback([]) + self._cacheData(wr.rid, []) + for i in pop_eye: + wr = self.waiting_requests.pop(i) + + def _pollForId(self, d): + if self.xmlstream.sid: + self.authid = self.xmlstream.sid + self._pollTimeout(d) + + + + def connectEvent(self, xs): + + self.version = self.authenticator.version + self.xmlstream = xs + if self.pint.v: + # add logging for verbose output + + self.xmlstream.rawDataOutFn = self.rawDataOut + self.xmlstream.rawDataInFn = self.rawDataIn + + if self.version == '1.0': + self.xmlstream.addObserver("/features", self.featuresHandler) + + + + def streamStart(self, xs): + """ + A xmpp stream has started + """ + # This is done to fix the stream id problem, I should submit a bug to twisted bugs + + try: + + self.authid = self.xmlstream.sid + + if not self.attrs.has_key('no_events'): + + self.xmlstream.addOnetimeObserver("/auth", self.stanzaHandler) + self.xmlstream.addOnetimeObserver("/response", self.stanzaHandler) + self.xmlstream.addOnetimeObserver("/success", self._saslSuccess) + self.xmlstream.addOnetimeObserver("/failure", self._saslError) + + self.xmlstream.addObserver("/iq/bind", self.bindHandler) + self.xmlstream.addObserver("/bind", self.stanzaHandler) + + self.xmlstream.addObserver("/challenge", self.stanzaHandler) + self.xmlstream.addObserver("/message", self.stanzaHandler) + self.xmlstream.addObserver("/iq", self.stanzaHandler) + self.xmlstream.addObserver("/presence", self.stanzaHandler) + # TODO - we should do something like this + # self.xmlstream.addObserver("/*", self.stanzaHandler) + + except: + log.err(traceback.print_exc()) + wr = self.waiting_requests.pop(0) + + wr.doErrback(error.Error("remote-connection-failed")) + + self.disconnect() + + + def featuresHandler(self, f): + """ + handle stream:features + """ + f.prefixes = ns.XMPP_PREFIXES.copy() + + #check for tls + self.f = {} + for feature in f.elements(): + self.f[(feature.uri, feature.name)] = feature + + starttls = (ns.TLS_XMLNS, 'starttls') in self.f + + initializers = getattr(self.xmlstream, 'initializers', []) + self.features = f + self.xmlstream.features = f + + # There is a tls initializer added by us, if it is available we need to try it + if len(initializers)>0 and starttls: + self.secure = 1 + + if self.authid is None: + self.authid = self.xmlstream.sid + + + # If we get tls, then we should start tls, wait and then return + # Here we wait, the tls initializer will start it + if starttls and self.secure: + return + self.elems.append(f) + if len(self.waiting_requests) > 0: + wr = self.waiting_requests.pop(0) + wr.doCallback(self.elems) + self._cacheData(wr.rid, self.elems) + self.elems = [] # reset elems + self.raw_buffer = u"" # reset raw buffer, features should not be in it + + def bindHandler(self, stz): + """bind debugger for punjab, this is temporary! """ + if self.verbose: + try: + log.msg('BIND: %s %s' % (str(self.sid), str(stz.bind.jid))) + except: + log.err() + if self.use_raw: + self.raw_buffer = stz.toXml() + + def stanzaHandler(self, stz): + """generic stanza handler for httpbind and httppoll""" + stz.prefixes = ns.XMPP_PREFIXES + if self.use_raw and self.authid: + stz = domish.SerializedXML(self.raw_buffer) + self.raw_buffer = u"" + + if self.waiting_requests and len(self.waiting_requests) > 0: + # if there are any waiting requests, give them all the + # data so far, plus this new data + + wr = self.waiting_requests.pop(0) + + data = self.elems + [stz] + self.elems = [] + + wr.doCallback(data) + self._cacheData(wr.rid, data) + else: + # since there are no waiting requests, just queue the data + self.elems.append(stz) + + def _startup_timeout(self, d): + # this can be called if connection failed, or if we connected + # but never got a stream features before the timeout + if self.pint.v: + log.msg('================================== %s %s startup timeout ==================================' % (str(self.sid), str(time.time()),)) + for i in range(len(self.waiting_requests)): + if self.waiting_requests[i].deferred == d: + wr = self.waiting_requests.pop(i) + # check if we really failed or not + if self.authid: + if len(self.elems) > 0: + data = self.elems + else: + data = [] + wr.doCallback(data) + self._cacheData(wr.rid, data) + else: + wr.doErrback(error.Error("remote-connection-failed")) + + + def buildRemoteError(self, err_elem=None): + e = error.Error('remote-stream-error') + e.error_stanza = 'remote-stream-error' + e.children = [] + if err_elem: + e.children.append(err_elem) + return e + + def streamError(self, streamerror): + """called when we get a stream:error stanza""" + + try: # a workaround for a bug in twisted.words.protocols.jabber.error + err_elem = streamerror.value.getElement() + err_elem.toXml() + except: # no matter what the exception we just return None + err_elem = None + + e = self.buildRemoteError(err_elem) + do_expire = True + + if len(self.waiting_requests) > 0: + wr = self.waiting_requests.pop(0) + wr.doErrback(e) + else: # need to wait for a new request and then expire + do_expire = False + + if self.pint and self.pint.sessions.has_key(self.sid): + if do_expire: + try: + self.expire() + except: + self.onExpire() + else: + s = self.pint.sessions.get(self.sid) + s.stream_error = e + + + def connectError(self, xs): + """called when we get disconnected""" + + # FIXME: we should really only send the error event back if + # attempts to reconnect fail. There's no reason temporary + # connection failures should be exposed upstream + if self.verbose: + log.msg('connect ERROR') + try: + log.msg(xs) + + except: + pass + + if self.waiting_requests: + + if len(self.waiting_requests) > 0: + wr = self.waiting_requests.pop(0) + wr.doErrback(error.Error('remote-connection-failed')) + + if self.pint and self.pint.sessions.has_key(self.sid): + try: + self.expire() + except: + self.onExpire() + + + def sendRawXml(self, obj): + """ + Send a raw xml string, not a domish.Element + """ + self.touch() + self._send(obj) + + + def _send(self, xml): + """ + Send valid data over the xmlstream + """ + if self.xmlstream: # FIXME this happens on an expired session and the post has something to send + if isinstance(xml, domish.Element): + xml.localPrefixes = {} + self.xmlstream.send(xml) + + def _removeObservers(self, typ = ''): + if typ == 'event': + observers = self.xmlstream._eventObservers + else: + observers = self.xmlstream._xpathObservers + emptyLists = [] + for priority, priorityObservers in observers.iteritems(): + for query, callbacklist in priorityObservers.iteritems(): + callbacklist.callbacks = [] + emptyLists.append((priority, query)) + + for priority, query in emptyLists: + del observers[priority][query] + + def disconnect(self): + """ + Disconnect from the xmpp server. + """ + if not getattr(self, 'xmlstream',None): + return + + if self.xmlstream: + #sh = "" + sh = "" + self.xmlstream.send(sh) + + self.stopTrying() + if self.xmlstream: + self.xmlstream.transport.loseConnection() + + del self.xmlstream + self.connected = 0 + self.pint = None + self.elems = [] + + if self.waiting_requests: + for i in range(len(self.waiting_requests)): + wr = self.waiting_requests.pop(i) + wr.doCallback() + self._cacheData(wr.rid, []) + del self.waiting_requests + self.mechanisms = None + self.features = None + + + + def checkExpired(self): + """ + Check if the session or xmpp connection has expired + """ + # send this so we do not timeout from servers + if getattr(self, 'xmlstream', None): + self.xmlstream.send(' ') + if self.inactivity is None: + wait = 900 + elif self.inactivity == 0: + wait = time.time() + + else: + wait = self.inactivity + + if self.waiting_requests and len(self.waiting_requests)>0: + wait += self.wait # if we have pending requests we need to add the wait time + + if time.time() - self.lastModified > wait+(0.1): + if self.site.sessions.has_key(self.uid): + self.terminate() + else: + pass + + else: + reactor.callLater(wait, self.checkExpired) + + + def _cacheData(self, rid, data): + if len(self.cache_data.keys())>=3: + # remove the first one in + keys = self.cache_data.keys() + keys.sort() + del self.cache_data[keys[0]] + + self.cache_data[int(rid)] = data + +# This stuff will leave when SASL and TLS are implemented correctly +# session stuff + + def _sessionResultEvent(self, iq): + """ """ + if len(self.waiting_requests)>0: + wr = self.waiting_requests.pop(0) + d = wr.deferred + else: + d = None + + if iq["type"] == "result": + if d: + d.callback(self) + else: + if d: + d.errback(self) + + + def _saslSuccess(self, s): + """ """ + self.success = 1 + self.s = s + # return success to the client + if len(self.waiting_requests)>0: + wr = self.waiting_requests.pop(0) + wr.doCallback([s]) + self._cacheData(wr.rid, [s]) + self.authenticator._reset() + if self.use_raw: + self.raw_buffer = u"" + + + + def _saslError(self, sasl_error, d = None): + """ SASL error """ + + if d: + d.errback(self) + if len(self.waiting_requests)>0: + wr = self.waiting_requests.pop(0) + wr.doCallback([sasl_error]) + self._cacheData(wr.rid, [sasl_error]) + diff --git a/punjab/stream.py b/punjab/stream.py new file mode 100644 index 0000000..116645c --- /dev/null +++ b/punjab/stream.py @@ -0,0 +1,74 @@ +""" + +""" +from twisted.words import domish + + +class PunjabElementStream(domish.ExpatElementStream): + """ + + We need to store the raw unicode data to bypass serialization. + + """ + + def _onStartElement(self, name, attrs): + # Generate a qname tuple from the provided name + qname = name.split(" ") + if len(qname) == 1: + qname = ('', name) + + # Process attributes + for k, v in attrs.items(): + if k.find(" ") != -1: + aqname = k.split(" ") + attrs[(aqname[0], aqname[1])] = v + del attrs[k] + + # Construct the new element + e = domish.Element(qname, self.defaultNsStack[-1], attrs, self.localPrefixes) + self.localPrefixes = {} + + # Document already started + if self.documentStarted == 1: + if self.currElem != None: + self.currElem.children.append(e) + e.parent = self.currElem + self.currElem = e + + # New document + else: + self.documentStarted = 1 + self.DocumentStartEvent(e) + + def _onEndElement(self, _): + # Check for null current elem; end of doc + if self.currElem is None: + self.DocumentEndEvent() + + # Check for parent that is None; that's + # the top of the stack + elif self.currElem.parent is None: + self.ElementEvent(self.currElem) + self.currElem = None + + # Anything else is just some element in the current + # packet wrapping up + else: + self.currElem = self.currElem.parent + + def _onCdata(self, data): + if self.currElem != None: + self.currElem.addContent(data) + + def _onStartNamespace(self, prefix, uri): + # If this is the default namespace, put + # it on the stack + if prefix is None: + self.defaultNsStack.append(uri) + else: + self.localPrefixes[prefix] = uri + + def _onEndNamespace(self, prefix): + # Remove last element on the stack + if prefix is None: + self.defaultNsStack.pop() diff --git a/punjab/tap.py b/punjab/tap.py new file mode 100644 index 0000000..0c3183e --- /dev/null +++ b/punjab/tap.py @@ -0,0 +1,24 @@ + +from twisted.python import usage +import punjab + +class Options(usage.Options): + optParameters = [ + ('host', None, 'localhost'), + ('port', None, 5280), + ('httpb', 'b', None), + ('polling',None,'15'), + ('html_dir', None, "./html"), + ('ssl',None,None), + ('ssl_privkey',None,"ssl.key"), + ('ssl_cert',None,"ssl.crt"), + + ] + + optFlags = [ + ('verbose', 'v', 'Show traffic'), + ] + + +def makeService(config): + return punjab.makeService(config) diff --git a/punjab/xmpp/__init__.py b/punjab/xmpp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/punjab/xmpp/error.py b/punjab/xmpp/error.py new file mode 100644 index 0000000..2e633db --- /dev/null +++ b/punjab/xmpp/error.py @@ -0,0 +1,35 @@ +# Some code from idavoll, thanks Ralph!! +NS_XMPP_STANZAS = "urn:ietf:params:xml:ns:xmpp-stanzas" + + + +conditions = { + 'bad-request': {'code': '400', 'type': 'modify'}, + 'not-authorized': {'code': '401', 'type': 'cancel'}, + 'item-not-found': {'code': '404', 'type': 'cancel'}, + 'not-acceptable': {'code': '406', 'type': 'modify'}, + 'conflict': {'code': '409', 'type': 'cancel'}, + 'internal-server-error': {'code': '500', 'type': 'wait'}, + 'feature-not-implemented': {'code': '501', 'type': 'cancel'}, + 'service-unavailable': {'code': '503', 'type': 'cancel'}, +} + +def error_from_iq(iq, condition, text = '', type = None): + iq.swapAttributeValues("to", "from") + iq["type"] = 'error' + e = iq.addElement("error") + + c = e.addElement((NS_XMPP_STANZAS, condition), NS_XMPP_STANZAS) + + if type == None: + type = conditions[condition]['type'] + + code = conditions[condition]['code'] + + e["code"] = code + e["type"] = type + + if text: + t = e.addElement((NS_XMPP_STANZAS, "text"), NS_XMPP_STANZAS, text) + + return iq diff --git a/punjab/xmpp/ns.py b/punjab/xmpp/ns.py new file mode 100644 index 0000000..279b187 --- /dev/null +++ b/punjab/xmpp/ns.py @@ -0,0 +1,24 @@ + +NS_CLIENT = 'jabber:client' +NS_ROSTER = 'jabber:iq:roster' +NS_AUTH = 'jabber:iq:auth' +NS_STREAMS = 'http://etherx.jabber.org/streams' +NS_XMPP_TLS = 'urn:ietf:params:xml:ns:xmpp-tls' +NS_COMMANDS = 'http://jabber.org/protocol/commands' + +TLS_XMLNS = 'urn:ietf:params:xml:ns:xmpp-tls' +SASL_XMLNS = 'urn:ietf:params:xml:ns:xmpp-sasl' +BIND_XMLNS = 'urn:ietf:params:xml:ns:xmpp-bind' +SESSION_XMLNS = 'urn:ietf:params:xml:ns:xmpp-session' +STREAMS_XMLNS = 'urn:ietf:params:xml:ns:xmpp-streams' + +IQ_GET = "/iq[@type='get']" +IQ_SET = "/iq[@type='set']" + +IQ_GET_AUTH = IQ_GET+"/query[@xmlns='%s']" % (NS_AUTH,) +IQ_SET_AUTH = IQ_SET+"/query[@xmlns='%s']" % (NS_AUTH,) + + +XMPP_PREFIXES = {NS_STREAMS:'stream'} +# NS_COMMANDS: 'commands'} + diff --git a/punjab/xmpp/server.py b/punjab/xmpp/server.py new file mode 100644 index 0000000..ea937ee --- /dev/null +++ b/punjab/xmpp/server.py @@ -0,0 +1,221 @@ +# XMPP server class + +from twisted.application import service +from twisted.python import components + +from twisted.internet import reactor + + +from twisted.words.xish import domish, xpath, xmlstream +from twisted.words.protocols.jabber import jid + +from punjab.xmpp import ns + +SASL_XMLNS = 'urn:ietf:params:xml:ns:xmpp-sasl' +COMP_XMLNS = 'http://jabberd.jabberstudio.org/ns/component/1.0' +STREAMS_XMLNS = 'urn:ietf:params:xml:ns:xmpp-streams' + +from zope.interface import Interface, implements + +# interfaces +class IXMPPServerService(Interface): + pass + +class IXMPPServerFactory(Interface): + pass + +class IXMPPFeature(Interface): + pass + +class IXMPPAuthenticationFeature(IXMPPFeature): + pass + +class IQAuthFeature(object): + """ XEP-0078 : http://www.xmpp.org/extensions/xep-0078.html""" + + implements(IXMPPAuthenticationFeature) + + + IQ_GET_AUTH = xpath.internQuery(ns.IQ_GET_AUTH) + IQ_SET_AUTH = xpath.internQuery(ns.IQ_SET_AUTH) + + + def associateWithStream(self, xs): + """Add a streamm start event observer. + And do other things to associate with the xmlstream if necessary. + """ + self.xmlstream = xs + self.xmlstream.addOnetimeObserver(xmlstream.STREAM_START_EVENT, + self.streamStarted) + + def disassociateWithStream(self, xs): + self.xmlstream.removeObserver(self.IQ_GET_AUTH, + self.authRequested) + self.xmlstream.removeObserver(self.IQ_SET_AUTH, + self.auth) + self.xmlstream = None + + + def streamStarted(self, elm): + """ + Called when client sends stream:stream + """ + self.xmlstream.addObserver(self.IQ_GET_AUTH, + self.authRequested) + self.xmlstream.addObserver(self.IQ_SET_AUTH, + self.auth) + + def authRequested(self, elem): + """Return the supported auth type. + + """ + resp = domish.Element(('iq', ns.NS_CLIENT)) + resp['type'] = 'result' + resp['id'] = elem['id'] + q = resp.addElement("query", ns.NS_AUTH) + q.addElement("username", content=str(elem.query.username)) + q.addElement("digest") + q.addElement("password") + q.addElement("resource") + + self.xmlstream.send(resp) + + def auth(self, elem): + """Do not auth the user, anyone can log in""" + + username = elem.query.username.__str__() + resource = elem.query.resource.__str__() + + user = jid.internJID(username+'@'+self.xmlstream.host+'/'+resource) + + resp = domish.Element(('iq', ns.NS_CLIENT)) + resp['type'] = 'result' + resp['id'] = elem['id'] + + self.xmlstream.send(resp) + + self.xmlstream.authenticated(user) + + + +class XMPPServerProtocol(xmlstream.XmlStream): + """ Basic dummy server protocol """ + host = "localhost" + user = None + initialized = False + id = 'Punjab123' + features = [IQAuthFeature()] + delay_features = 0 + + def connectionMade(self): + """ + a client connection has been made + """ + xmlstream.XmlStream.connectionMade(self) + + self.bootstraps = [ + (xmlstream.STREAM_CONNECTED_EVENT, self.streamConnected), + (xmlstream.STREAM_START_EVENT, self.streamStarted), + (xmlstream.STREAM_END_EVENT, self.streamEnded), + (xmlstream.STREAM_ERROR_EVENT, self.streamErrored), + ] + + for event, fn in self.bootstraps: + self.addObserver(event, fn) + + # load up the authentication features + for f in self.features: + if IXMPPAuthenticationFeature.implementedBy(f.__class__): + f.associateWithStream(self) + + def send(self, obj): + if not self.initialized: + self.transport.write("""\n""") + self.initialized = True + xmlstream.XmlStream.send(self, obj) + + + def streamConnected(self, elm): + print "stream connected" + + def streamStarted(self, elm): + """stream has started, we need to respond + + """ + if self.delay_features == 0: + self.send("""""" % (ns.NS_CLIENT, self.host, self.id,)) + else: + self.send("""""" % (ns.NS_CLIENT, self.host, self.id,)) + reactor.callLater(self.delay_features, self.send, """""") + + def streamEnded(self, elm): + self.send("""""") + + def streamErrored(self, elm): + self.send("""""") + + def authenticated(self, user): + """User has authenticated. + """ + self.user = user + + def onElement(self, element): + try: + xmlstream.XmlStream.onElement(self, element) + except Exception, e: + print "Exception!", e + raise e + + def onDocumentEnd(self): + pass + + def connectionLost(self, reason): + xmlstream.XmlStream.connectionLost(self, reason) + pass + + def triggerChallenge(self): + """ send a fake challenge for testing + """ + + self.send("""cmVhbG09ImNoZXNzcGFyay5jb20iLG5vbmNlPSJ0YUhIM0FHQkpQSE40eXNvNEt5cFlBPT0iLHFvcD0iYXV0aCxhdXRoLWludCIsY2hhcnNldD11dGYtOCxhbGdvcml0aG09bWQ1LXNlc3M=""") + + + def triggerStreamError(self): + """ send a stream error + """ + self.send(""" +""") + self.streamEnded(None) + + + +class XMPPServerFactoryFromService(xmlstream.XmlStreamFactory): + implements(IXMPPServerFactory) + + protocol = XMPPServerProtocol + + def __init__(self, service): + xmlstream.XmlStreamFactory.__init__(self) + self.service = service + + + def buildProtocol(self, addr): + self.resetDelay() + xs = self.protocol() + xs.factory = self + for event, fn in self.bootstraps: + xs.addObserver(event, fn) + return xs + + +components.registerAdapter(XMPPServerFactoryFromService, + IXMPPServerService, + IXMPPServerFactory) + + +class XMPPServerService(service.Service): + + implements(IXMPPServerService) + + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..991b0fa --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +from distutils.core import setup + + +setup(name='punjab', + version='0.12', + description='Punjab, a twisted HTTP server with interfaces to XMPP.', + author='Christopher Zorn', + author_email='tofu@thetofu.com', + url='http://www.butterfat.net/wiki/Projects/PunJab', + packages=['punjab','punjab.xmpp', 'twisted.plugins'], + package_data={'twisted.plugins': ['twisted/plugins/punjab.py']} + ) diff --git a/tests/httpb_client.py b/tests/httpb_client.py new file mode 100644 index 0000000..48a6970 --- /dev/null +++ b/tests/httpb_client.py @@ -0,0 +1,428 @@ +from twisted.internet import defer, protocol, reactor, stdio +from twisted.python import log, reflect +try: + from twisted.words.xish import domish, utility +except: + from twisted.xish import domish, utility +from twisted.web import http + +from twisted.words.protocols.jabber import xmlstream, client, jid + +from twisted.protocols import basic +import urlparse +import random, binascii, base64, md5, sha, time, os, random + +import os,sys + + +from punjab.httpb import HttpbParse # maybe use something else to seperate from punjab + +TLS_XMLNS = 'urn:ietf:params:xml:ns:xmpp-tls' +SASL_XMLNS = 'urn:ietf:params:xml:ns:xmpp-sasl' +BIND_XMLNS = 'urn:ietf:params:xml:ns:xmpp-bind' +SESSION_XMLNS = 'urn:ietf:params:xml:ns:xmpp-session' + +NS_HTTP_BIND = "http://jabber.org/protocol/httpbind" + +class Error(Exception): + stanza_error = '' + punjab_error = '' + msg = '' + def __init__(self,msg = None): + self.stanza_error = msg + self.punjab_error = msg + self.msg = msg + + def __str__(self): + return self.stanza_error + + +class RemoteConnectionFailed(Error): + msg = 'remote-connection-failed' + stanza_error = 'remote-connection-failed' + + +class NodeNotFound(Error): + msg = '404 not found' + +class NotAuthorized(Error): + pass + +class NotImplemented(Error): + pass + + +class XMPPAuthenticator(client.XMPPAuthenticator): + """ + Authenticate against an xmpp server using BOSH + """ + +class QueryProtocol(http.HTTPClient): + noisy = False + def connectionMade(self): + self.factory.sendConnected(self) + self.sendBody(self.factory.cb) + + def sendCommand(self, command, path): + self.transport.write('%s %s HTTP/1.1\r\n' % (command, path)) + + def sendBody(self, b, close = 0): + if isinstance(b, domish.Element): + bdata = b.toXml().encode('utf-8') + else: + bdata = b + + self.sendCommand('POST', self.factory.url) + self.sendHeader('User-Agent', 'Twisted/XEP-0124') + self.sendHeader('Host', self.factory.host) + self.sendHeader('Content-type', 'text/xml') + self.sendHeader('Content-length', str(len(bdata))) + self.endHeaders() + self.transport.write(bdata) + + def handleStatus(self, version, status, message): + if status != '200': + self.factory.badStatus(status, message) + + def handleResponse(self, contents): + self.factory.parseResponse(contents, self) + + def lineReceived(self, line): + if self.firstLine: + self.firstLine = 0 + l = line.split(None, 2) + version = l[0] + status = l[1] + try: + message = l[2] + except IndexError: + # sometimes there is no message + message = "" + self.handleStatus(version, status, message) + return + if line: + key, val = line.split(':', 1) + val = val.lstrip() + self.handleHeader(key, val) + if key.lower() == 'content-length': + self.length = int(val) + else: + self.__buffer = [] + self.handleEndHeaders() + self.setRawMode() + + def handleResponseEnd(self): + self.firstLine = 1 + if self.__buffer != None: + b = ''.join(self.__buffer) + + self.__buffer = None + self.handleResponse(b) + + def handleResponsePart(self, data): + self.__buffer.append(data) + + + def connectionLost(self, reason): + #log.msg(dir(reason)) + #log.msg(reason) + pass + + +class QueryFactory(protocol.ClientFactory): + """ a factory to create http client connections. + """ + deferred = None + noisy = False + protocol = QueryProtocol + def __init__(self, url, host, b): + self.url, self.host = url, host + self.deferred = defer.Deferred() + self.cb = b + + def send(self,b): + self.deferred = defer.Deferred() + + self.client.sendBody(b) + + return self.deferred + + def parseResponse(self, contents, protocol): + self.client = protocol + hp = HttpbParse(True) + + try: + body_tag,elements = hp.parse(contents) + except: + raise + else: + if body_tag.hasAttribute('type') and body_tag['type'] == 'terminate': + if self.deferred.called: + return defer.fail((body_tag,elements)) + else: + self.deferred.errback((body_tag,elements)) + return + if self.deferred.called: + return defer.succeed((body_tag,elements)) + else: + self.deferred.callback((body_tag,elements)) + + + def sendConnected(self, q): + self.q = q + + + + def clientConnectionLost(self, _, reason): + try: + self.client = None + if not self.deferred.called: + self.deferred.errback(reason) + + except: + return reason + + clientConnectionFailed = clientConnectionLost + + def badStatus(self, status, message): + if not self.deferred.called: + self.deferred.errback(ValueError(status, message)) + + + +import random, sha, md5 + +class Keys: + """ A class to generate keys for http binding """ + def __init__(self): + self.set_keys() + + + def set_keys(self): + seed = random.randint(30,1000000) + self.num_keys = random.randint(55,255) + self.k = [] + self.k.append(seed) + for i in range(self.num_keys-1): + x = i + 1 + self.k.append(sha.new(str(self.k[x-1])).hexdigest()) + + self.key_index = self.num_keys - 1 + + def getKey(self): + self.key_index = self.key_index - 1 + return self.k.pop(self.key_index) + + def firstKey(self): + if self.key_index == self.num_keys - 1: + return 1 + else: + return 0 + + def lastKey(self): + if self.key_index == 0: + return 1 + else: + return 0 + + +class Proxy: + """A Proxy for making HTTP Binding calls. + + Pass the URL of the remote HTTP Binding server to the constructor. + + """ + + def __init__(self, url): + """ + Parse the given url and find the host and port to connect to. + """ + parts = urlparse.urlparse(url) + self.url = urlparse.urlunparse(('', '')+parts[2:]) + if self.url == "": + self.url = "/" + if ':' in parts[1]: + self.host, self.port = parts[1].split(':') + self.port = int(self.port) + else: + self.host, self.port = parts[1], None + self.secure = parts[0] == 'https' + + + def connect(self, b): + """ + Make a connection to the web server and send along the data. + """ + self.factory = QueryFactory(self.url, self.host, b) + + if self.secure: + from twisted.internet import ssl + self.rid = reactor.connectSSL(self.host, self.port or 443, + self.factory, ssl.ClientContextFactory()) + else: + self.rid = reactor.connectTCP(self.host, self.port or 80, self.factory) + + + return self.factory.deferred + + + def send(self,b): + """ Send data to the web server. """ + + # if keepalive is off we need a new query factory + # TODO - put a check to reuse the factory, right now we open a new one. + d = self.connect(b) + return d + +class HTTPBClientConnector: + """ + A HTTP Binding client connector. + """ + def __init__(self, url): + self.url = url + + def connect(self, factory): + self.proxy = Proxy(self.url) + self.xs = factory.buildProtocol(self.proxy.host) + self.xs.proxy = self.proxy + self.xs.connectionMade() + + + def disconnect(self): + self.xs.connectionLost('disconnect') + self.xs = None + + +class HTTPBindingStream(xmlstream.XmlStream): + """ + HTTP Binding wrapper that acts like xmlstream + + """ + + def __init__(self, authenticator): + xmlstream.XmlStream.__init__(self, authenticator) + self.base_url = '/xmpp-httpbind/' + self.host = 'dev.chesspark.com' + self.mechanism = 'PLAIN' + # request id + self.rid = random.randint(0, 10000000) + # session id + self.session_id = 0 + # keys + self.keys = Keys() + self.initialized = False + self.requests = [] + + def _cbConnect(self, result): + r,e = result + ms = '' + self.initialized = True + # log.msg('======================================== cbConnect ====================') + self.session_id = r['sid'] + self.authid = r['authid'] + self.namespace = self.authenticator.namespace + self.otherHost = self.authenticator.otherHost + self.dispatch(self, xmlstream.STREAM_START_EVENT) + # Setup observer for stream errors + self.addOnetimeObserver("/error[@xmlns='%s']" % xmlstream.NS_STREAMS, + self.onStreamError) + + if len(e)>0 and e[0].name == 'features': + # log.msg('============================= on features ==============================') + self.onFeatures(e[0]) + else: + self.authenticator.streamStarted() + + def _ebError(self, e): + log.err(e.printTraceback()) + + + def _initializeStream(self): + """ Initialize binding session. + + Just need to create a session once, this can be done elsewhere, but here will do for now. + """ + + if not self.initialized: + b = domish.Element((NS_HTTP_BIND,'body')) + + b['content'] = 'text/xml; charset=utf-8' + b['hold'] = '1' + b['rid'] = str(self.rid) + b['to'] = self.authenticator.jid.host + b['wait'] = '60' + b['xml:lang'] = 'en' + # FIXME - there is an issue with the keys + # b = self.key(b) + + # Connection test + d = self.proxy.connect(b) + d.addCallback(self._cbConnect) + d.addErrback(self._ebError) + return d + else: + self.authenticator.initializeStream() + + + def key(self,b): + if self.keys.lastKey(): + self.keys.setKeys() + + if self.keys.firstKey(): + b['newkey'] = self.keys.getKey() + else: + b['key'] = self.keys.getKey() + return b + + def _cbSend(self, result): + body, elements = result + if body.hasAttribute('type') and body['type'] == 'terminate': + reactor.close() + self.requests.pop(0) + for e in elements: + if self.rawDataInFn: + self.rawDataInFn(str(e.toXml())) + if e.name == 'features': + self.onFeatures(e) + else: + self.onElement(e) + # if no elements lets send out another poll + if len(self.requests)==0: + self.send() + + + def send(self, obj = None): + if self.session_id == 0: + return defer.succeed(False) + + b = domish.Element((NS_HTTP_BIND,"body")) + b['content'] = 'text/xml; charset=utf-8' + self.rid = self.rid + 1 + b['rid'] = str(self.rid) + b['sid'] = str(self.session_id) + b['xml:lang'] = 'en' + + if obj is not None: + if domish.IElement.providedBy(obj): + if self.rawDataOutFn: + self.rawDataOutFn(str(obj.toXml())) + b.addChild(obj) + #b = self.key(b) + self.requests.append(b) + d = self.proxy.send(b) + d.addCallback(self._cbSend) + return d + + +class HTTPBindingStreamFactory(xmlstream.XmlStreamFactory): + """ + Factory for HTTPBindingStream protocol objects. + """ + + def buildProtocol(self, _): + self.resetDelay() + xs = HTTPBindingStream(self.authenticator) + xs.factory = self + for event, fn in self.bootstraps: xs.addObserver(event, fn) + return xs + diff --git a/tests/testparser.py b/tests/testparser.py new file mode 100644 index 0000000..cfd54e8 --- /dev/null +++ b/tests/testparser.py @@ -0,0 +1,174 @@ + +import os +import sys, sha, random +from twisted.trial import unittest +import time +from twisted.web import server, resource, static, http, client +from twisted.words.protocols.jabber import jid +from twisted.internet import defer, protocol, reactor +from twisted.application import internet, service +from twisted.words.xish import domish, xpath + +from twisted.python import log + +from punjab.httpb import HttpbParse + + + +class ParseTestCase(unittest.TestCase): + """ + Tests for Punjab compatability with http://www.xmpp.org/extensions/xep-0124.html + """ + + def testTime(self): + XML = """ + +""" + t = time.time() + + for i in range(0, 10000): + hp = HttpbParse(use_t=True) + b, elems = hp.parse(XML) + for e in elems: + x = e.toXml() + td = time.time() - t + + + t = time.time() + for i in range(0, 10000): + hp = HttpbParse() + b, elems = hp.parse(XML) + for e in elems: + if type(u'') == type(e): + x = e + + ntd = time.time() - t + + self.failUnless(td>ntd, 'Not faster') + + + + def testGtBug(self): + XML = """ 500500 +""" + hp = HttpbParse() + + b, e = hp.parse(XML) + + # need tests here + self.failUnless(e[0]==u"",'invalid xml') + self.failUnless(e[1]==u"500", 'invalid xml') + self.failUnless(e[2]==u"500", 'invalid xml') + + + def testParse(self): + XML = """ + +""" + hp = HttpbParse() + + b, e = hp.parse(XML) + + # need tests here + self.failUnless(e[0]==u"", 'invalid xml') + self.failUnless(e[1]==u"", 'invalid xml') + + + def testPrefixes(self): + XML = """""" + + hp = HttpbParse() + + b, e = hp.parse(XML) + + self.failUnless(e[0]==u"", 'invalid xml') + + + + def testEscapedCDATA(self): + XML = """> """ + + hp = HttpbParse() + + b, e = hp.parse(XML) + + XML = """ i type > and i see >>>""" + + hp = HttpbParse() + + b, e = hp.parse(XML) + + self.failUnless(e[0]==u"i type > and i see >>>", 'Invalid Xml') + + + def testCDATA(self): + XML = """dXNlcm5hbWU9InRvZnUiLHJlYWxtPSJkZXYuY2hlc3NwYXJrLmNvbSIsbm9uY2U9Ik5SaW5HQkNaWjg0U09Ea1BzMWpxd1E9PSIsY25vbmNlPSJkNDFkOGNkOThmMDBiMjA0ZTk4MDA5OThlY2Y4NDI3ZSIsbmM9IjAwMDAwMDAxIixxb3A9ImF1dGgiLGRpZ2VzdC11cmk9InhtcHAvZGV2LmNoZXNzcGFyay5jb20iLHJlc3BvbnNlPSIxNGQ3NWE5YmU2MzdkOTdkOTM1YjU2Y2M4ZWZhODk4OSIsY2hhcnNldD0idXRmLTgi""" + + hp = HttpbParse() + + b, e = hp.parse(XML) + + self.failUnless(e[0]==u"dXNlcm5hbWU9InRvZnUiLHJlYWxtPSJkZXYuY2hlc3NwYXJrLmNvbSIsbm9uY2U9Ik5SaW5HQkNaWjg0U09Ea1BzMWpxd1E9PSIsY25vbmNlPSJkNDFkOGNkOThmMDBiMjA0ZTk4MDA5OThlY2Y4NDI3ZSIsbmM9IjAwMDAwMDAxIixxb3A9ImF1dGgiLGRpZ2VzdC11cmk9InhtcHAvZGV2LmNoZXNzcGFyay5jb20iLHJlc3BvbnNlPSIxNGQ3NWE5YmU2MzdkOTdkOTM1YjU2Y2M4ZWZhODk4OSIsY2hhcnNldD0idXRmLTgi", 'Invalid xml') + + + + def testPrefsCdata(self): + + XML = """ + + + + + test2 + test1 + + + + + + + + + + + + + + + + + + + + + + + pro@chat.chesspark.com + + + + + + + + + + + + + + + + + + + + + """ + + + hp = HttpbParse() + + b, e = hp.parse(XML) + + self.failUnless(e[0]==u"\n \n \n \n test2\n test1\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n pro@chat.chesspark.com\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ", 'invalid xml') diff --git a/tests/xep124.py b/tests/xep124.py new file mode 100644 index 0000000..17bcdfb --- /dev/null +++ b/tests/xep124.py @@ -0,0 +1,315 @@ + +import os +import sys, sha, random +from twisted.trial import unittest +import time +from twisted.web import server, resource, static, http, client +from twisted.words.protocols.jabber import jid +from twisted.internet import defer, protocol, reactor +from twisted.application import internet, service +from twisted.words.xish import domish, xpath + +from twisted.python import log + +from punjab.httpb import HttpbService +from punjab.xmpp import server as xmppserver +import httpb_client + +class DummyTransport: + + def __init__(self): + self.data = [] + + def write(self, bytes): + self.data.append(bytes) + + def loseConnection(self, *args, **kwargs): + self.data = [] + +class XEP0124TestCase(unittest.TestCase): + """ + Tests for Punjab compatability with http://www.xmpp.org/extensions/xep-0124.html + """ + + def setUp(self): + # set up punjab + os.mkdir("./html") # create directory in _trial_temp + self.root = static.File("./html") # make _trial_temp/html the root html directory + self.rid = random.randint(0,10000000) + self.b = resource.IResource(HttpbService(1)) + self.root.putChild('xmpp-bosh', self.b) + + self.site = server.Site(self.root) + + self.p = reactor.listenTCP(0, self.site, interface="127.0.0.1") + self.port = self.p.getHost().port + + # set up proxy + + self.proxy = httpb_client.Proxy(self.getURL()) + self.sid = None + self.keys = httpb_client.Keys() + + # set up dummy xmpp server + + self.server_service = xmppserver.XMPPServerService() + self.server_factory = xmppserver.IXMPPServerFactory(self.server_service) + self.server = reactor.listenTCP(5222, self.server_factory, interface="127.0.0.1") + + # Hook the server's buildProtocol to make the protocol instance + # accessible to tests. + buildProtocol = self.server_factory.buildProtocol + d1 = defer.Deferred() + def _rememberProtocolInstance(addr): + self.server_protocol = buildProtocol(addr) + # keeping this around because we may want to wrap this specific to tests + # self.server_protocol = protocol.wrappedProtocol + d1.callback(None) + return self.server_protocol + self.server_factory.buildProtocol = _rememberProtocolInstance + + + def getURL(self, path = "xmpp-bosh"): + return "http://127.0.0.1:%d/%s" % (self.port, path) + + + def key(self,b): + if self.keys.lastKey(): + self.keys.setKeys() + + if self.keys.firstKey(): + b['newkey'] = self.keys.getKey() + else: + b['key'] = self.keys.getKey() + return b + + def resend(self, ext = None): + self.rid = self.rid - 1 + return self.send(ext) + + def send(self, ext = None): + b = domish.Element(("http://jabber.org/protocol/httpbind","body")) + b['content'] = 'text/xml; charset=utf-8' + self.rid = self.rid + 1 + b['rid'] = str(self.rid) + b['sid'] = str(self.sid) + b['xml:lang'] = 'en' + + if ext is not None: + if isinstance(ext, domish.Element): + b.addChild(ext) + else: + b.addRawXml(ext) + + b = self.key(b) + + d = self.proxy.send(b) + return d + + def testCreateSession(self): + """ + Test Section 7.1 of BOSH xep : http://www.xmpp.org/extensions/xep-0124.html#session + """ + + def _testSessionCreate(res): + self.failUnless(res[0].name=='body', 'Wrong element') + + self.failUnless(res[0].hasAttribute('sid'),'Not session id') + + + BOSH_XML = """ + """ + + d = self.proxy.connect(BOSH_XML).addCallback(_testSessionCreate) + + return d + + def testStreamError(self): + """ + This is to test if we get stream errors when there are no waiting requests. + """ + + def _testStreamError(res): + self.failUnless(res.value[0].hasAttribute('condition'), 'No attribute condition') + self.failUnless(res.value[0]['condition'] == 'remote-stream-error', 'Condition should be remote stream error') + self.failUnless(res.value[1][0].children[0].name == 'policy-violation', 'Error should be policy violation') + + + + def _failStreamError(res): + self.fail('A stream error needs to be returned') + + def _testSessionCreate(res): + self.sid = res[0]['sid'] + # this xml is valid, just for testing + # the point is to wait for a stream error + d = self.send('') + d.addCallback(_failStreamError) + d.addErrback(_testStreamError) + self.server_protocol.triggerStreamError() + + return d + + BOSH_XML = """ + """ % (self.rid,) + + d = self.proxy.connect(BOSH_XML).addCallback(_testSessionCreate) + + return d + + + def testFeaturesError(self): + """ + This is to test if we get stream features and NOT twice + """ + + def _testError(res): + self.failUnless(res[1][0].name=='challenge','Did not get correct challenge stanza') + + def _testSessionCreate(res): + self.sid = res[0]['sid'] + # this xml is valid, just for testing + # the point is to wait for a stream error + self.failUnless(res[1][0].name=='features','Did not get initial features') + + # self.send("") + d = self.send("") + d.addCallback(_testError) + reactor.callLater(1, self.server_protocol.triggerChallenge) + + return d + + BOSH_XML = """ + """ % (self.rid,) + + self.server_factory.protocol.delay_features = 10 + + d = self.proxy.connect(BOSH_XML).addCallback(_testSessionCreate) + # NOTE : to trigger this bug there needs to be 0 waiting requests. + + return d + + + def testRidCountBug(self): + """ + This is to test if rid becomes off on resends + """ + @defer.inlineCallbacks + def _testError(res): + self.failUnless(res[1][0].name=='challenge','Did not get correct challenge stanza') + for r in range(5): + # send auth to bump up rid + res = yield self.send("") + # resend auth + for r in range(5): + res = yield self.resend("") + + res = yield self.resend("") + + + def _testSessionCreate(res): + self.sid = res[0]['sid'] + # this xml is valid, just for testing + # the point is to wait for a stream error + self.failUnless(res[1][0].name=='features','Did not get initial features') + + # self.send("") + d = self.send("") + d.addCallback(_testError) + reactor.callLater(1, self.server_protocol.triggerChallenge) + + return d + + BOSH_XML = """ + """ % (self.rid,) + + self.server_factory.protocol.delay_features = 10 + + d = self.proxy.connect(BOSH_XML).addCallback(_testSessionCreate) + # NOTE : to trigger this bug there needs to be 0 waiting requests. + + return d + + + def _error(self, e): + # self.fail(e) + pass + + def _cleanPending(self): + pending = reactor.getDelayedCalls() + if pending: + for p in pending: + if p.active(): + p.cancel() + + def _cleanSelectables(self): + removedSelectables = reactor.removeAll() + # Below is commented out to remind us how to see what selectable is sticking around + #if removedSelectables: + # for sel in removedSelectables: + # # del sel + # print sel.__class__ + # print dir(sel) + + def tearDown(self): + def cbStopListening(result=None): + + self.root = None + self.site = None + self.proxy.factory.stopFactory() + self.server_factory.stopFactory() + self.server = None + self._cleanPending() + self._cleanSelectables() + + os.rmdir("./html") # remove directory from _trial_temp + self.b.service.poll_timeouts.stop() + self.b.service.stopService() + self.p.stopListening() + for s in self.b.service.sessions.keys(): + self.b.service.endSession(self.b.service.sessions[s]) + if hasattr(self.proxy.factory,'client'): + self.proxy.factory.client.transport.stopConnecting() + + + d = defer.maybeDeferred(self.server.stopListening) + d.addCallback(cbStopListening) + + return d + diff --git a/tests/xep206.py b/tests/xep206.py new file mode 100644 index 0000000..52bd671 --- /dev/null +++ b/tests/xep206.py @@ -0,0 +1,29 @@ + +import os +import sys, sha +from twisted.trial import unittest +import time +from twisted.words.protocols.jabber import jid +from twisted.internet import defer, protocol, reactor +from twisted.application import internet, service +from twisted.words.xish import domish, xpath + +from twisted.python import log + +class DummyClient: + """ + a client for testing + """ + +class DummyTransport: + """ + a transport for testing + """ + + + +class XEP0206TestCase(unittest.TestCase): + """ + Tests for Punjab compatability with http://www.xmpp.org/extensions/xep-0206.html + """ + diff --git a/twisted/plugins/punjab.py b/twisted/plugins/punjab.py new file mode 100644 index 0000000..4eebfc3 --- /dev/null +++ b/twisted/plugins/punjab.py @@ -0,0 +1,8 @@ + +from twisted.scripts.mktap import _tapHelper + +Punjab = _tapHelper( + "Punjab", + "punjab.tap", + "A HTTP XMPP client interface", + "punjab")