diff options
author | louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13 <louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13> | 2010-01-31 15:09:58 +0000 |
---|---|---|
committer | louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13 <louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13> | 2010-01-31 15:09:58 +0000 |
commit | d6458b66aab2b84ff7d5a800b1e603f25181d723 (patch) | |
tree | 60a5dee7263bde102c32aa1420ee6f2d9206733e | |
parent | 98efd30d3077e12bef459dba2dff179302116a5d (diff) | |
download | poezio-d6458b66aab2b84ff7d5a800b1e603f25181d723.tar.gz poezio-d6458b66aab2b84ff7d5a800b1e603f25181d723.tar.bz2 poezio-d6458b66aab2b84ff7d5a800b1e603f25181d723.tar.xz poezio-d6458b66aab2b84ff7d5a800b1e603f25181d723.zip |
inclus xmppy0.5-RC1 avec les sources, sinon c'est chiant.
48 files changed, 12196 insertions, 2 deletions
@@ -7,16 +7,19 @@ MANDIR=$(DATADIR)/man INSTALL=install all: Makefile + cd src/xmpppy-0.5.0rc1 && pwd && python setup.py build && cp -r xmpp .. + clean: Makefile install: - $(INSTALL) -d $(LOCALEDIR) $(BINDIR) $(DATADIR)/poezio $(DATADIR)/poezio/data $(DATADIR)/poezio/src + $(INSTALL) -d $(LOCALEDIR) $(BINDIR) $(DATADIR)/poezio $(DATADIR)/poezio/data $(DATADIR)/poezio/src $(DATADIR)/poezio/src/xmpp $(INSTALL) -m644 data/* $(DATADIR)/poezio/data/ for sourcefile in `find src/ -maxdepth 1 -type f | grep -v '.svn' | grep -v '.pyc'` ; do \ $(INSTALL) -m644 $$sourcefile $(DATADIR)/poezio/src; \ done + $(INSTALL) -m644 src/xmpp/* $(DATADIR)/poezio/src/xmpp/ echo "#!/bin/sh" > $(BINDIR)/poezio echo "cd $(DATADIR)/poezio/src/ && python client.py" >> $(BINDIR)/poezio diff --git a/src/connection.py b/src/connection.py index c55c292c..68030e7c 100644 --- a/src/connection.py +++ b/src/connection.py @@ -65,7 +65,7 @@ class Connection(Thread): def authenticate(self, anon=True): if anon: - return self.client.auth(None, None, self.resource) + return self.client.auth(None, "", self.resource) else: log.error('Non-anonymous connections not handled currently') return None diff --git a/src/xmpppy-0.5.0rc1/ChangeLog b/src/xmpppy-0.5.0rc1/ChangeLog new file mode 100644 index 00000000..b2092f7f --- /dev/null +++ b/src/xmpppy-0.5.0rc1/ChangeLog @@ -0,0 +1,1599 @@ +2009-04-07 15:14 snakeru + + * xmpp/protocol.py: Merged XEP-0004.Multiple.Items.Form.Results + patch. Thanks to Iván Lloro + +2009-04-07 12:34 snakeru + + * xmpp/transports.py: Merged a fix for DNS SRV lookup on win32 + +2009-04-07 11:11 snakeru + + * xmpp/features.py: Fixed crash on whitespace-containing disco + <iq/> reply + +2009-04-07 10:19 snakeru + + * xmpp/client.py: Fixed documentation bug in Component.__init__ + +2009-04-07 09:59 snakeru + + * xmpp/transports.py: Fix for fresh bug: self.server => + self._server + +2009-04-07 09:52 snakeru + + * xmpp/: session.py, transports.py: Replaced couple of 'print' + statements with proper use of self.DEBUG + +2009-03-03 13:24 normanr + + * xmpp/simplexml.py: Import simplexml updates from gajim, thanks + mainly to asterix, thorstenp and dwd + +2009-02-13 13:26 normanr + + * xmpp/protocol.py: Add extra XEP refs (this has been sitting in my + working copy for about a year) + +2008-09-14 01:45 normanr + + * xmpp/auth.py: Allow anonymous auth if username is None + +2008-08-09 21:00 normanr + + * doc/examples/xtalk.py, xmpp/auth.py: Fix for incorrect-encoding + during SASL PLAIN auth + +2008-02-08 12:00 normanr + + * xmpp/auth.py: Handle spaces in SASL DIGEST-MD5 responses + correctly + +2008-01-30 13:12 normanr + + * doc/examples/xtalk.py: Handle missing body, thanks to Brendan + Sleight + +2008-01-04 14:32 normanr + + * xmpp/transports.py: HTTPPROXYsocket will send data before we have + a Dispatcher + +2008-01-04 00:46 normanr + + * xmpp/jep0106.py: Move jep0106 test code into it's module + +2007-09-15 15:34 normanr + + * xmpp/transports.py: Clean up SSL errors a little bit + +2007-09-11 16:46 normanr + + * xmpp/simplexml.py: Fixes for children node fetching, still not + perfect, but much better + +2007-08-28 14:03 normanr + + * xmpp/: auth.py, client.py: [ 1529650 ] Bug in auth, can't retry + +2007-08-28 13:54 normanr + + * xmpp/commands.py: [ 1729857 ] typo in commands.py + +2007-08-04 02:35 normanr + + * xmpp/client.py: Fix socket namespace conflict + +2007-05-19 03:18 normanr + + * xmpp/: client.py, dispatcher.py, transports.py: More fixes - + reconnectAndReauth now works for Client too + +2007-05-19 02:43 normanr + + * xmpp/client.py: Fix PlugOut and reconnectAndReauth code execution + order + +2007-05-18 21:41 normanr + + * xmpp/simplexml.py: Ordering fix for when addChild and addData are + used on the same node (may increase memory usage, might need to + watch for that) + +2007-05-13 21:55 normanr + + * xmpp/: jep0106.py, protocol.py: change jep references into xep + +2007-05-13 21:55 normanr + + * xmpp/browser.py: Fix for non-ascii data in debug message + +2007-04-10 00:23 normanr + + * xmpp/protocol.py: moving admin namespace constants from jep0133 + to to protocol + +2007-04-09 23:22 normanr + + * xmpp/protocol.py: updating namespace constants + +2007-04-09 21:50 normanr + + * xmpp/protocol.py: add support for setting the DataField label in + it's constructor + +2007-03-20 09:00 snakeru + + * xmpp/simplexml.py: Fixed node attribute deletion with "del + node[attr]" syntax. + +2007-03-15 23:49 normanr + + * xmpp/client.py: Handle XCP component:accept namespace + +2006-10-06 16:30 normanr + + * doc/examples/: bot.py, xsend.py, xtalk.py: allow the bot to set a + connection resource + +2006-10-06 03:30 normanr + + * doc/index.html: missed a few links + +2006-10-06 03:14 normanr + + * doc/index.html: updating documentation links + +2006-10-06 02:25 normanr + + * setup.py: Debian updates for version 0.4 + +2006-10-04 00:03 normanr + + * xmpp/protocol.py: add some missing protocol namespaces + +2006-09-27 02:12 normanr + + * doc/examples/xtalk.py: initial check-in + +2006-09-27 02:12 normanr + + * doc/examples/xsend.py: tweaked login code to fail with reasons + +2006-08-21 12:12 normanr + + * doc/examples/xsend.py: Ignore comment lines in config file + +2006-08-21 12:11 normanr + + * xmpp/auth.py: Fixing auth splits + +2006-06-03 17:53 normanr + + * xmpp/: auth.py, client.py, dispatcher.py: added support for + wildfire component binding + +2006-06-03 16:54 normanr + + * doc/examples/commandsbot.py, xmpp/commands.py: fixed command + namespaces and basic circle area math + +2006-06-03 16:36 normanr + + * doc/examples/commandsbot.py: added example command bot from + Liorithiel + +2006-06-03 16:32 normanr + + * xmpp/commands.py: fixes from Liorithiel + +2006-06-03 16:22 normanr + + * xmpp/simplexml.py: added xmlns safety check + +2006-06-03 16:15 normanr + + * xmpp/commands.py: fixed command namespaces + +2006-05-30 23:55 normanr + + * doc/examples/xsend.py: General cleanups + +2006-05-22 12:58 normanr + + * xmpp/transports.py: Fix for non-int ports + +2006-05-18 09:53 snakeru + + * xmpp/simplexml.py: Added CDATA extracting method to xml node. + +2006-05-15 03:57 normanr + + * xmpp/jep0106.py: moved jep-0106 into xmpp + +2006-05-13 18:39 normanr + + * xmpp/commands.py: minor typos + +2006-03-25 09:11 snakeru + + * xmpp/debug.py: Re-enabled debugging. + +2006-03-25 09:01 snakeru + + * setup.py: Updated version stuff for xmpppy module. + +2006-03-25 08:47 snakeru + + * xmpp/features.py: Fixed bug in disco items discovery (thanks + Soren Roug). + +2006-02-11 18:45 snakeru + + * xmpp/debug.py: Made NoDebug class usable + +2006-02-11 18:37 snakeru + + * xmpp/client.py: fixed resources consumation in many places + +2006-02-11 18:36 snakeru + + * xmpp/features.py: fixed features.register + +2006-01-26 16:09 snakeru + + * xmpp/transports.py: Bugfix for previous commit + +2006-01-26 16:06 snakeru + + * xmpp/: debug.py, transports.py: Made xmpppy to print warnings to + stdout instead of stderr + +2006-01-19 00:38 normanr + + * xmpp/: client.py, commands.py, debug.py, protocol.py: xmlns + fixes, and minor tweaks for speed and safety + +2006-01-18 22:26 normanr + + * xmpp/: auth.py, dispatcher.py: Namespace fixes + +2006-01-10 01:08 normanr + + * xmpp/protocol.py: Message.buildReply fix for Gerard + +2006-01-07 21:41 normanr + + * xmpp/protocol.py: Added message events, and minor DataForm fix + +2006-01-02 22:40 normanr + + * xmpp/: auth.py, client.py, dispatcher.py, protocol.py: Jabberd2 + component protocol support + +2006-01-02 22:26 normanr + + * xmpp/debug.py: Enhanced debug output + +2005-12-19 17:30 snakeru + + * xmpp/debug.py: Disabled color output on non-un*x-like platforms. + +2005-12-11 21:54 normanr + + * xmpp/: client.py, transports.py: made failed connections slightly + more robust. + +2005-11-30 20:05 normanr + + * xmpp/auth.py: http://trac.gajim.org/ticket/1188 - fix for base64 + encoded strings ending with an equals sign + +2005-11-30 20:03 normanr + + * xmpp/commands.py: command nodes now return correct disco#info + values + +2005-11-22 08:20 snakeru + + * xmpp/auth.py: Fixed digest-uri parameter in SASL auth. Thanks to + Le Boulanger Yann and Norman Rasmussen. + +2005-10-31 09:15 snakeru + + * xmpp/protocol.py: Fixed timstamp detecting bug (thanks to Daryl + Herzmann). + +2005-10-26 12:45 snakeru + + * xmpp/auth.py: Fixed SASL bug on win32 platform. (Thanks to Martin + Thomas) + +2005-10-24 23:32 normanr + + * xmpp/: client.py, transports.py: [gajim]it is standarD not with + T; thanks dkm + +2005-10-23 01:47 normanr + + * xmpp/: client.py, debug.py, protocol.py: fixed whitespace + +2005-10-08 03:17 normanr + + * xmpp/: browser.py, commands.py: fixes for discovery replies that + gajim exposed + +2005-10-01 21:53 snakeru + + * xmpp/: client.py, transports.py: Made SRV resolution disableable + (Gajim patch 3658). + +2005-10-01 21:51 snakeru + + * xmpp/client.py: Added catchment for exception while tls handshake + (Gajim patch 3323). + +2005-10-01 21:41 snakeru + + * xmpp/transports.py: Added events for sent/received bytes (Gajim + patches 2789, 2979, 3254). + +2005-10-01 21:40 snakeru + + * xmpp/session.py: Removed useless #!/usr/bin/python header (Gajim + patch 2115) + +2005-10-01 21:39 snakeru + + * xmpp/transports.py: Typo and debug line text fixes (Gajim patch + 2113). + +2005-10-01 21:36 snakeru + + * xmpp/protocol.py: Added method for retrieve nick value in MUC + (Gajim patch 2089). + +2005-10-01 21:34 snakeru + + * xmpp/TODO: Added several lines to TODO. + +2005-10-01 19:03 snakeru + + * xmpp/client.py: Fixed binding process. Formatiing fixes. + +2005-10-01 18:58 snakeru + + * xmpp/protocol.py: Fixed first timestamp detection + +2005-10-01 01:34 mikealbon + + * xmpp/client.py: SASL Timeout, Gajim #2066 + +2005-10-01 01:28 mikealbon + + * xmpp/client.py: Enable SSL on non-standard port. Gajim #2065 + +2005-10-01 00:13 mikealbon + + * xmpp/features.py: Asynchronous In-band Registration. Gajim + patches #2035 #2318 + +2005-09-30 01:38 mikealbon + + * xmpp/protocol.py: Bumper pack of namespace definitions. Including + gajim #2637. + +2005-09-28 11:20 snakeru + + * xmpp/client.py: Docstring fixes. Fixed typo in Client.connect + method docstring (Thanks to Andrew Diederich). Evaluated more + descriptions of Component.__init__ and Component.__connect__ . + +2005-09-23 22:32 normanr + + * xmpp/: browser.py, client.py, commands.py: Fixes to make commands + work, when you're working with multiple jids and nodes. + +2005-09-21 00:56 normanr + + * xmpp/TODO: some todo items + +2005-09-17 19:15 normanr + + * xmpp/protocol.py: tidied disco and muc namespaces + +2005-09-17 19:13 normanr + + * xmpp/commands.py: fix for items being returned on non-items disco + +2005-09-16 18:28 snakeru + + * xmpp/TODO: Added TODO line about roster parsing traceback. + +2005-09-16 18:15 snakeru + + * xmpp/TODO: Added another todo line about input chunking. + +2005-09-16 17:59 snakeru + + * xmpp/TODO: Added keepalive feature in TODO list. + +2005-09-09 08:12 snakeru + + * doc/examples/xsend.py: Added help message to sample config file. + +2005-09-06 02:04 mikealbon + + * xmpp/transports.py: Added SRV record resolution for new client + connections. This is using gajim changesets (2036 2037 2039 2040 + 3184 3407 3408 3409 3410 3411 3412 3413) Debugging through debug + needs to be fixed. + +2005-08-29 01:50 normanr + + * xmpp/commands.py: fixes for error constants + +2005-08-19 00:03 normanr + + * xmpp/dispatcher.py: Added support for non-fatal exception + handling, exceptions can also be logged to file. + +2005-08-06 08:48 snakeru + + * xmpp/session.py: Fixed usage of .T. and .NT. notation according + to recent change. + +2005-08-06 08:44 snakeru + + * xmpp/simplexml.py: WARNING! Incompartible change! Now + newtag=n.T.newtag do not creates new tag but only returns + existing one (if possible). If you need to create tag use either + .NT. method or attribute set (i.e. n.T.newtag=something). + +2005-08-05 08:57 snakeru + + * xmpp/features.py: The setPrivacyList function used a nonexistent + payload variable where it should use the list parameter (thanks + to Michal Politowski). + +2005-07-13 17:22 snakeru + + * xmpp/roster.py: Docstring fix + +2005-07-08 01:34 normanr + + * xmpp/commands.py: Commands now work. Errors are also returned if + continuing an invalid session. + +2005-06-02 16:22 snakeru + + * xmpp/client.py: Changed cl.connected from 'tls' to 'ssl' in case + of port 5223/443. + +2005-06-02 16:19 snakeru + + * xmpp/client.py: List of default ssl ports is now [5223,443]. + +2005-05-28 13:30 mikealbon + + * xmpp/commands.py: Lots of bugfixes -- thanks Norman + +2005-05-26 09:04 snakeru + + * xmpp/: auth.py, client.py: Another SASL case was broken. Fix + applied, tested against variety of servers. + +2005-05-24 17:59 snakeru + + * xmpp/auth.py: Removed early FeaturesHandler call to not start + auth before credentials got passed. + +2005-05-24 17:28 snakeru + + * xmpp/client.py: Added parameter to auth() to disable SASL + +2005-05-12 13:20 snakeru + + * xmpp/TODO: Added note about TLS issue + +2005-05-12 13:00 snakeru + + * xmpp/client.py: Added return value description to connect() + docstring. + +2005-05-12 11:35 snakeru + + * xmpp/transports.py: Fixed TLS-not-disconnects bug + +2005-05-11 09:42 snakeru + + * xmpp/roster.py: Added comment about roster's NodeProcessed + behaivoir. + +2005-05-11 09:38 snakeru + + * xmpp/roster.py: Roster Iq handler must raise NodeProcessed. + Otherwise, iq's will hit default handler and <iq type='error'/> + will be sent back. + +2005-05-09 21:31 snakeru + + * doc/index.html: Made <a/> tags to not open new windows. + +2005-05-09 21:27 snakeru + + * doc/xmpppy_title.png: New design. Big thanks to Marek Kubica for + it. + +2005-05-09 21:09 snakeru + + * doc/: index.html, xmpppy.css: New design. Big thanks to Marek + Kubica for it. + +2005-05-09 18:51 snakeru + + * xmpp/dispatcher.py: Bugfix: RegisterHandler(...,makefirst=1) + didn't work. + +2005-05-08 12:03 snakeru + + * setup.py: Changed download url from whole project to xmpppy + module + +2005-05-08 11:54 snakeru + + * setup.py: Add reminder to fix source code release version string + while making release + +2005-05-08 08:51 snakeru + + * xmpp/: client.py, dispatcher.py: Added possibility to detect + broken servers that didn't restart stream after tls start and + disable tls for them. + +2005-05-07 20:24 snakeru + + * xmpp/transports.py: Fixed traceback while connecting via proxy + +2005-05-07 20:14 snakeru + + * xmpp/protocol.py: Fixed stupid typo in DataForm + +2005-05-07 07:26 snakeru + + * xmpp/dispatcher.py: Added non-locking SendAndCallForResponse + method to ease life of realtime clients. + +2005-05-07 06:42 snakeru + + * xmpp/auth.py: Auth was failing when server declares XMPP stream + (version="1.0") but not supports SASL. + +2005-05-06 21:33 snakeru + + * xmpp/protocol.py: Added missing MUC attributes helper. Added + 'instructions' field to XData.asDict() + +2005-05-02 12:38 snakeru + + * xmpp/roster.py: Formatting fix. + +2005-05-02 12:36 snakeru + + * xmpp/: dispatcher.py, protocol.py: Added stream errors classes + along with default handler + +2005-04-30 14:17 snakeru + + * xmpp/: client.py, auth.py: Fixed Non-SASL auth brocken with one + of today's commits. + +2005-04-30 12:56 snakeru + + * xmpp/: dispatcher.py, transports.py: Bugfix: TLS mode was unable + to handle big (>1024 bytes) chunks of data. Was forced to + change TCPsocket.receive() return values logic. + +2005-04-30 12:53 snakeru + + * xmpp/client.py: Fixed auth logic: if SASL failed - then auth + definitely failed too. + +2005-04-30 12:14 snakeru + + * xmpp/transports.py: Minor changes in receive() code in + preparation to fix TLS bug. + +2005-04-30 12:10 snakeru + + * xmpp/dispatcher.py: Added two docstrings + +2005-04-30 11:43 snakeru + + * xmpp/features.py: Fixed getRegInfo to not crash on query's CDATA + +2005-04-30 11:33 snakeru + + * xmpp/commands.py: Formatteed/added several docstrings + +2005-04-30 11:20 snakeru + + * xmpp/simplexml.py: Cosmetic docstrings changes + +2005-04-30 11:17 snakeru + + * xmpp/protocol.py: Added NS_COMMANDS, NS_ENCRYPTED, NS_SIGNED + namespaces. Added MUC iq attributes functions from jabberpy. + +2005-04-30 11:13 snakeru + + * xmpp/browser.py: Fixed RegisterHandler calls to catch only 'get' + iqs. Fixed DiscoHandler to raise NodeProcessed (or we should + just return instead?) + +2005-04-30 11:01 snakeru + + * xmpp/: auth.py, transports.py: Fixed plugout methods to not take + parameter + +2005-04-10 12:25 snakeru + + * xmpp/TODO: TODO for 0.2 release + +2005-04-10 12:21 snakeru + + * xmpp/simplexml.py: Reduced overload caused by extensive usage of + T/NT classes. + +2005-04-10 12:09 snakeru + + * xmpp/client.py: Added back possibility of manual specification of + server type (for Component) for case if ejabberd team (or + others) will add features to component streams. Changed default + port for component connection to 5347 (jabberd2 router). + +2005-04-10 11:55 snakeru + + * xmpp/client.py: Replaced manual server type specification with + autodetect + +2005-03-09 17:18 snakeru + + * doc/examples/logger.py: Added presences tracking + +2005-03-09 11:32 snakeru + + * doc/examples/logger.py: Bugfix: proxy was specified incorrectly + +2005-03-09 00:18 snakeru + + * doc/index.html: index.html + +2005-03-09 00:14 snakeru + + * doc/examples/README.py: Moved to "examples". + +2005-03-08 23:57 snakeru + + * doc/examples/logger.py: Conference logging bot example + +2005-03-08 23:48 snakeru + + * Makefile: Installer Makefile + +2005-03-08 22:50 snakeru + + * xmpp/commands.py: Tuned "import"s stuff to be more in-line with + library + +2005-03-08 22:36 snakeru + + * xmpp/: auth.py, client.py, dispatcher.py, protocol.py: Tweaked + library to make it play nice as jabberd2 legacy component. + +2005-03-08 19:15 snakeru + + * xmpp/simplexml.py: Some tweaks about determining if node needs + 'xmlns' attribute. + +2005-03-07 12:34 snakeru + + * xmpp/__init__.py: Added commands module import + +2005-03-07 12:07 snakeru + + * xmpp/auth.py: Preserved handlers during auth process to allow + early handlers registration. + +2005-02-25 08:49 snakeru + + * xmpp/roster.py: Changed (c) date range + +2005-02-25 08:48 snakeru + + * xmpp/roster.py: Fixed Iq callback brocken last commit + +2005-02-25 08:35 snakeru + + * xmpp/roster.py: "raise NodeProcessed" removed to allow userspace + catch roster changes too + +2005-02-21 14:52 snakeru + + * xmpp/dispatcher.py: Added etherx namespace to the default set to + allow stream errors handling. + +2005-02-17 11:16 snakeru + + * xmpp/roster.py: Bugfix: presences should not really inherit + meta-info (like <show/> etc) + +2005-02-16 19:33 snakeru + + * xmpp/roster.py: Bugfix: UNbroke accidentally brocken code. Shame + on me. + +2005-02-16 19:29 snakeru + + * xmpp/auth.py: Bugfix: (NonSASL) Added removal of empty + <password/> node to achieve JiveMessenger compartibility (Tnx + Brian Tipton) + +2005-02-07 21:38 snakeru + + * xmpp/roster.py: BugFix: Roster.PresenceHandler should not raise + NodeProcessed exception. + +2005-01-31 16:10 mikealbon + + * xmpp/commands.py: Modified the handlers used. Result messages are + not required for command processor use. + +2005-01-21 14:16 mikealbon + + * xmpp/commands.py: Initial version of commands processor + +2005-01-16 23:18 snakeru + + * doc/examples/bot.py: Xmpppy-based bot example + +2005-01-16 21:32 snakeru + + * doc/examples/xsend.py: Old servers compartibility stuff added. + Tnx google, randomthoughts. + +2005-01-04 08:17 snakeru + + * xmpp/session.py: Session class added + +2004-12-26 11:54 snakeru + + * setup.py: python distutils install tool + +2004-12-26 11:40 snakeru + + * README: README rewrited + +2004-12-26 11:34 snakeru + + * doc/index.html: Some more updates + +2004-12-26 11:12 snakeru + + * doc/: advanced.html, basic.html, index.html: Documentation + updated: expert docs written, advanced started. + +2004-12-26 10:39 snakeru + + * doc/expert.html: Removed since api documentation is maintained + via docstrings. + +2004-12-25 23:06 snakeru + + * xmpp/: __init__.py, dispatcher.py, features.py, filetransfer.py, + roster.py, transports.py: Added and/or modifyed docstrings. Now + every method in library is documented\! Hurray\! + +2004-12-24 22:56 snakeru + + * xmpp/: browser.py, protocol.py: Pydoc strings added + +2004-12-23 23:11 snakeru + + * xmpp/: __init__.py, auth.py, client.py, dispatcher.py, + simplexml.py: Docstrings merged. Most of them were ready already + in (shame!) july. + +2004-12-09 18:08 snakeru + + * xmpp/dispatcher.py: Bugfix: complete autodetection of default + handler's namespace + +2004-12-09 18:06 snakeru + + * xmpp/dispatcher.py: More wisdom for default handler's namespace + determining + +2004-12-09 17:34 snakeru + + * xmpp/dispatcher.py: Added some wisdom to determining of default + handler's namespace. + +2004-12-09 17:21 snakeru + + * xmpp/: auth.py, client.py, protocol.py: Fixed component auth that + was brocken by dispatcher's changes. + +2004-12-06 17:49 snakeru + + * xmpp/browser.py: Bugfix: typo in _DiscoveryHandler (thanks 2 Mike + Albon) + +2004-10-23 11:58 snakeru + + * xmpp/protocol.py: NS_DIALBACK added JID's node and domain now + stored only lowercase Bugfix: don't allow empty type to go into + error node Do not serialise error reply to error stanza (prevent + error bouncing) + +2004-10-23 11:53 snakeru + + * xmpp/dispatcher.py: "chained" handlers killed changing type of + incomed stanza only if it is a simple Node (to allow pickling) + +2004-10-23 11:51 snakeru + + * xmpp/auth.py: Bugfix: auth details should go into self._owner + +2004-10-08 23:10 snakeru + + * xmpp/auth.py: Ensure that username and resourcename got from + server's responce. + +2004-10-08 23:06 snakeru + + * xmpp/dispatcher.py: Now stanza properties stored in it's + attribute "props". + +2004-10-08 23:01 snakeru + + * xmpp/features.py: Bugfix: don't traceback if DISCO/Browse timed + out. + +2004-09-25 23:05 snakeru + + * xmpp/simplexml.py: Rolled back ns vocabularies. They were + potentially messing namespaces. + +2004-09-25 22:52 snakeru + + * xmpp/simplexml.py: Allowed attribute values to be objects + +2004-09-25 22:50 snakeru + + * xmpp/protocol.py: XMPP streams namespace added. One more XMPP + stanza error condition added: <not-authorized/> Error conditions + now differs by name: STREAM_, ERR_ and SASL_. Attribute 'to' and + 'from' in protocol instances now JIDs - not strings. Added + namespace to Protocol element's __init__s. Changed error forming + process to involve correct namespace setting. + +2004-09-25 22:46 snakeru + + * xmpp/dispatcher.py: Namespace handler now comes under the name + "default". + +2004-09-20 00:05 snakeru + + * xmpp/client.py: Plugging in now available only once. Now using + SASL.auth() method instead of SASL() + +2004-09-19 16:34 snakeru + + * xmpp/dispatcher.py: Added plugout method for proper destuction of + Stream instance. Added RegisterNamespace method for registering + default handlers for ns. Made UnregisterHandler immune to + unregistering absent handler. + +2004-09-19 16:31 snakeru + + * xmpp/simplexml.py: added destroy method to NodeBuilder to prevent + memory leak + +2004-09-19 16:28 snakeru + + * xmpp/transports.py: Added plugout method to TLS class for + unregistering handlers. Added raising NodeProcessed in TLS + handler according to dispatcher's architecture. + +2004-09-19 16:19 snakeru + + * xmpp/protocol.py: Added SASL error conditions + +2004-09-19 15:49 snakeru + + * xmpp/auth.py: SASL.auth method added. Removed credentials passing + from PlugIn. plugout methods added for unregistering handlers. + NodeProcessed exceptions now raised according to dispatcher + architecture. + +2004-09-17 23:28 snakeru + + * xmpp/: auth.py, dispatcher.py, transports.py: Added stanzas + namespace support in dispatcher. + +2004-09-17 23:22 snakeru + + * xmpp/protocol.py: Added xmpp streams namespace. Made + stream-level error conditions render in proper xmlns. Removed + error text backward compartibility positioning. + +2004-09-17 23:19 snakeru + + * xmpp/simplexml.py: XML namespaces vocabulary introduced. Quick + node access methods introduced. + +2004-09-15 22:35 snakeru + + * xmpp/simplexml.py: Bugfix: more delicate namespaces processing. + Slow (again) but sure. Bugfix: stream-level CDATA processing. + Feature: stream open/close events system. + +2004-09-15 22:30 snakeru + + * xmpp/transports.py: Fixed comment. + +2004-09-15 22:22 snakeru + + * xmpp/roster.py: Bugfix: handle roster item deletion properly. + Translated comment into english. + +2004-09-15 22:19 snakeru + + * xmpp/protocol.py: Added 'jabber:client' and 'jabber:server' + namespaces. + +2004-09-15 18:57 snakeru + + * xmpp/filetransfer.py: Comments translated to english. + +2004-09-15 18:51 snakeru + + * xmpp/dispatcher.py: Added sessions support. + +2004-09-15 07:18 snakeru + + * xmpp/ietf-docs/draft-ietf-xmpp-core.html: Update to revision 24. + +2004-09-13 14:32 snakeru + + * doc/examples/xsend.py: Fixed typo: SendInitialPresence => + SendInitPresence. + +2004-08-04 22:41 snakeru + + * xmpp/__init__.py: IBB is fixed and worth inclusion. + +2004-08-04 22:39 snakeru + + * xmpp/__init__.py: IBB stuff is fixed and worth inclusion. + +2004-08-04 22:30 snakeru + + * xmpp/: __init__.py, features.py, protocol.py: Stable 0.1 will not + include browser and new DataForm class. + +2004-08-04 21:35 snakeru + + * xmpp/: client.py, filetransfer.py: Fixed and tested IBB. Added + usual debugging stuff to it. + +2004-08-04 21:32 snakeru + + * xmpp/dispatcher.py: Bugfix: typeless stanzas were processed + several times sometimes. + +2004-08-04 21:26 snakeru + + * doc/basic.html: Corrections to text donated by Mike Albon. + +2004-07-26 17:00 snakeru + + * xmpp/simplexml.py: Bugfix: nodebuilder was tracing on the first + node. + +2004-07-23 16:34 snakeru + + * xmpp/protocol.py: Added getQueryChildren method. WARNING: it + behaves gust like getQueryPayload before. And the getQueryPayload + is now different! + +2004-07-23 16:31 snakeru + + * xmpp/simplexml.py: Made getPayload to return both CDATA and child + nodes just like setPayload uses. + +2004-07-23 16:27 snakeru + + * xmpp/simplexml.py: Fixed bug in CDATA handling code. The data + will not be shifted between tags anymore. + +2004-07-12 23:20 snakeru + + * xmpp/: features.py, protocol.py: DataForm class re-implemented to + conform JEP-0004 more closely. + +2004-07-11 23:32 snakeru + + * xmpp/protocol.py: Added support for multiple values. Bugfix: + label is an option's property - not DataField's. + +2004-07-11 23:01 snakeru + + * xmpp/protocol.py: Added import of ustr function from simplexml + module. Bugfix: jid comparsion made less vulnerable to type + mismatches. Added JID.__hash__ method. + +2004-07-11 23:00 snakeru + + * xmpp/browser.py: Added support for several hosts on one + connection. Argument Handler in setDiscoHandler converted to + lowercase. + +2004-06-30 07:30 snakeru + + * xmpp/browser.py: Added support for nodes like + "http://jabber.org/protocol/commands". + +2004-06-28 22:58 snakeru + + * xmpp/protocol.py: Added DataField class in preparation to + DataForm rewrite. + +2004-06-28 15:55 snakeru + + * xmpp/roster.py: Added raising NodeProcessed exception to mark + already processed iq and presences. + +2004-06-28 09:38 snakeru + + * xmpp/simplexml.py: Added Node.has_attr + +2004-06-27 23:10 snakeru + + * xmpp/dispatcher.py: Bugfix: the returnStanzaHandler must not + return error stanzas. + +2004-06-27 22:24 snakeru + + * xmpp/: __init__.py, browser.py: Browser module tested, fixed and + included into library structure. + +2004-06-27 20:00 snakeru + + * xmpp/browser.py: Hand-crafted and logically debugged the heart - + _traversePath. Now need to check other methods. + +2004-06-27 17:30 snakeru + + * xmpp/: dispatcher.py, features.py, filetransfer.py, protocol.py: + NodeProcessed mechaniks fixed: class moved to protocol + module. try: except: block fixed to catch all needed + exceptions. Default handler mechanics fixed. + returnStanzaHandler moved from features to dispatcher. It will be + default handler in 0.2. Dispatcher.UnregisterHandler fixed. + +2004-06-27 17:03 snakeru + + * xmpp/protocol.py: Iq.buildReply made to appropriate set the + queryNS value. Error text message now included in error body tag + for compartibility with older protocol. + +2004-06-26 12:26 snakeru + + * xmpp/dispatcher.py: Bugfix: already dispatched node must not be + changed anymore by NodeBuilder. + +2004-06-26 12:24 snakeru + + * xmpp/client.py: Bugfix: TLS failed to restart after disconnect. + +2004-06-26 12:20 snakeru + + * xmpp/roster.py: Namespace declarations moved to protocol module. + Protocol module imported to the local namespace. Bugfix: item + deletion required Node class in local namespace. + +2004-06-26 08:42 snakeru + + * xmpp/: auth.py, client.py, dispatcher.py, features.py, + filetransfer.py, protocol.py, transports.py: All namespaces + declarations moved to protocol module. Introduced politics of + direct protocol module importing. + +2004-06-26 08:14 snakeru + + * xmpp/__init__.py: Added direct import from protocol module. + +2004-06-23 16:48 snakeru + + * xmpp/protocol.py: Added getQuerynode and setQuerynode methods. + +2004-06-20 22:36 snakeru + + * doc/index.html: Hope that Mike will never see it. I am so + ashamed... + +2004-06-20 22:30 snakeru + + * doc/: advanced.html, expert.html: Empty file. + +2004-06-20 22:29 snakeru + + * doc/basic.html: Unformatted but finished at the first look. + +2004-06-20 22:28 snakeru + + * doc/index.html: Added links to documentation and Mike Albon's IRC + transport. + +2004-06-20 22:16 snakeru + + * xmpp/simplexml.py: Bugfix: use " to not corrupt XML on some + attribute values. + +2004-06-20 13:45 snakeru + + * doc/examples/xsend.py: Example script that is used in "simple" + doc. + +2004-06-20 12:39 snakeru + + * README: First documentation appeared. Added notice about + official Debian's packages. + +2004-06-18 07:40 snakeru + + * doc/basic.html: Some more bits. + +2004-06-18 07:02 snakeru + + * doc/basic.html: Started attempt to write a docs for library. + +2004-06-17 19:57 snakeru + + * xmpp/protocol.py: Bugfix: tag.getError() will not issue a + traceback anymore if there is no error (thanks to sneakin). + +2004-06-17 19:13 snakeru + + * xmpp/simplexml.py: Bugfix: bits like xml:lang='en' was processed + incorrectly. ** This is a very crude hack. I must think more + on this. + +2004-06-03 17:25 snakeru + + * xmpp/: protocol.py, roster.py: Fixed bug with "@" and "/" + characters in the resource string. + +2004-06-03 16:38 snakeru + + * xmpp/simplexml.py: Bugfix: addChild now set's child.parent + properly. + +2004-05-28 22:08 snakeru + + * README: Some unimportant corrections. + +2004-05-28 22:02 snakeru + + * README: Added "normal" README. + +2004-05-25 14:46 snakeru + + * xmpp/roster.py: getRoster , getItem methods added + +2004-05-25 14:46 snakeru + + * xmpp/client.py: getRoster method added. + +2004-05-20 15:56 snakeru + + * xmpp/protocol.py: Fixed backtrace on unhandled condition case. + +2004-05-20 11:09 snakeru + + * xmpp/: client.py, debug.py: Bugfix: debug_flags was in "debug" + module namespace instead of being Debug class attribute. + +2004-05-17 23:25 snakeru + + * xmpp/protocol.py: JID.__ne__ method added. + +2004-05-17 17:30 snakeru + + * xmpp/client.py: DeregisterDisconnectHandler renamed to + UnregisterDisconnectHandler. + +2004-05-17 13:32 snakeru + + * xmpp/__init__.py: Added NS_XXX importing into module's namespace. + +2004-05-14 09:40 snakeru + + * xmpp/roster.py: Added getItems, keys and __getitem__ methods + (limited mapping interface). Bugfix: setItem used incorrect XML. + Self contact corrected to have all (though dummy) parameters. + +2004-05-05 10:34 snakeru + + * xmpp/: auth.py, client.py: Removed "#!/usr/bin/python" headers to + please lintian. + +2004-05-04 12:28 snakeru + + * xmpp/: auth.py, client.py, dispatcher.py, features.py, + protocol.py: Changed all "type" in functions arguments to "typ" . + WARNING: VERY INCOMPARTIBLE CHANGES! This is one more step + away from jabberpy API. + +2004-05-04 11:58 snakeru + + * xmpp/filetransfer.py: Very preliminary. It worked recently but + poorly and may be broken already. + +2004-05-04 11:54 snakeru + + * xmpp/: features.py, protocol.py: Protocol.Error syntax changed. + WARNING: incompartible changes. + +2004-05-03 20:57 snakeru + + * xmpp/: features.py, protocol.py: Error nodes creating and setting + made more (I hope) intuitive. WARNING: uncompartible changes. + +2004-05-03 09:33 snakeru + + * xmpp/protocol.py: Python 2.1 compartibility in Protocol.__init__. + Bugfix: Protocol element properties is child's namespaces - w/o + dependency of parent node namespace. + +2004-05-03 09:29 snakeru + + * xmpp/simplexml.py: Bugfix: preserve namespace when cloning node. + Bugfix: fixed traceback in NodeBuilder on non-Node object + upgrading. + +2004-05-02 22:23 snakeru + + * xmpp/: auth.py, client.py, dispatcher.py, roster.py, + transports.py: Implemented common plugins framework. + +2004-04-29 23:21 snakeru + + * xmpp/simplexml.py: Node cloning improved. Full cloning mode + introduced. NodeBuilder can now take initial node as the base + for building. + +2004-04-29 23:14 snakeru + + * xmpp/protocol.py: Message.buildReply and Iq.buildReply + introduced. + +2004-04-29 23:10 snakeru + + * xmpp/dispatcher.py: Events introduced. Cycle (null) handlers + introduced. + +2004-04-29 23:06 snakeru + + * xmpp/client.py: Default resource name "xmpppy" now used only when + auth with non-xmpp compliant jabberd server. In the other + cases it uses server-provided resource name. + +2004-04-29 22:53 snakeru + + * xmpp/protocol.py: DataForm now can use prototype node for + initialisation (as other protocol elements). DataForm: + workaround for broken jabberd1.4 added to handle double + <password/> tag. DataForm mapping methods added. + +2004-04-29 22:46 snakeru + + * xmpp/features.py: 'jid' replaced by 'host' in registration + methods. Documentation added to registration methods. BugTypo: + 'res' instead of 'resp' Bugfix: DataForm doesn't take nodename as + parameter. + +2004-04-26 23:27 snakeru + + * xmpp/ietf-docs/: draft-ietf-xmpp-core.html, + draft-ietf-xmpp-im.html: Update to current upstream version. + +2004-04-25 09:12 snakeru + + * xmpp/: auth.py, client.py, dispatcher.py, features.py, + simplexml.py, transports.py: Date extended in license text. + +2004-04-25 09:07 snakeru + + * xmpp/protocol.py: Date extended in license text. Empty <text/> + node removed from error stanza. + +2004-04-18 13:12 snakeru + + * xmpp/dispatcher.py: Added "default handler" mechanizm. "Process" + function documented. + +2004-04-18 13:09 snakeru + + * xmpp/features.py: returnStanzaHandler added. + +2004-04-18 13:06 snakeru + + * xmpp/protocol.py: Added translation of error codes to error + conditions. Default error condition changed to + <undefined-condition/> + +2004-04-18 10:24 snakeru + + * xmpp/protocol.py: XMPP-Core stanza and stream level errors + support added. + +2004-04-18 08:36 snakeru + + * xmpp/: auth.py, features.py, protocol.py: Function "resultNode" + replaced by "isResultNode". Function "errorNode" replaced by + "isErrorNode". + +2004-03-25 19:25 snakeru + + * xmpp/: dispatcher.py, protocol.py: Changed dispatching policy: + check for ALL child namespaces - not for only first <query/> in + Iq stanza. + +2004-03-25 19:24 snakeru + + * xmpp/client.py: Cleanup: import of features no more needed. + +2004-03-11 08:49 snakeru + + * xmpp/simplexml.py: All character data is now *STORED* in utf-8 + not only printed. Bugfix: fancy output was incorrect on data + output of child nodes. + +2004-03-03 19:03 snakeru + + * doc/index.html: Web page xmpppy.sf.net + +2004-02-24 09:20 snakeru + + * xmpp/dispatcher.py: Added "NodeProcessed" mechanism to allow + handlers stop further stanza processing. + +2004-02-20 15:16 snakeru + + * xmpp/__init__.py: Added revision control comment line. + +2004-02-14 13:16 snakeru + + * xmpp/: dispatcher.py, transports.py: Added experimental support + for live reconnection. + +2004-02-14 13:11 snakeru + + * xmpp/client.py: Bugfix: Client.connect doesn't always returned + true when connection estabilished. Added experimental support + for live reconnection. + +2004-02-11 22:27 snakeru + + * xmpp/client.py: Made tests like isConnected()[4:] possible. + +2004-02-11 22:24 snakeru + + * xmpp/: client.py, transports.py: Made isConnected return more + meningful result (tcp|tls+old_auth|sasl) + +2004-02-11 22:02 snakeru + + * xmpp/client.py: Added isConnected method. + +2004-02-11 16:53 snakeru + + * xmpp/: auth.py, client.py, dispatcher.py, transports.py: Fix: + Previous client.py commit broke jabberd2-compartible mechanisms. + +2004-02-10 20:25 snakeru + + * xmpp/client.py: Bugfix: Component used 'client' string in debug + output. Common stuff abstracted from Client and Component to + CommonClient class. Client.connect and Component.connect methods + merged to new method: CommonClient.connect. + +2004-02-10 20:16 snakeru + + * xmpp/auth.py: Fixed error text saying that we can do only PLAIN + authentication. + +2004-02-09 11:05 snakeru + + * xmpp/: auth.py, client.py: connect() and auth() methods now + returns result of operation. + +2004-02-09 11:03 snakeru + + * xmpp/transports.py: PlugIn methods now returns results of + connection. connect() method doesn't call 'disconnected' on + failed connect. + +2004-02-09 11:01 snakeru + + * xmpp/simplexml.py: Changes in "fancy" node output. Even more + CDATA corruption ;) + +2004-01-26 09:30 snakeru + + * xmpp/__init__.py: Simple import of all modules. + +2004-01-26 09:25 snakeru + + * xmpp/client.py: Bugfix: non-sasl auth was not recognized. + +2004-01-20 11:51 snakeru + + * xmpp/protocol.py: JID.getStripped now returns lower case JID + (questionable change). DataForm now represents itself as <x + xmlns='jabber:x:data'/> tag. + +2004-01-20 11:49 snakeru + + * xmpp/client.py: Added (again) default port for component class. + Disconnect method copied from client to component class. + +2004-01-20 11:46 snakeru + + * xmpp/features.py: Bugfixes in privacy lists mangling stuff. + +2004-01-15 12:58 snakeru + + * xmpp/transports.py: Maked early start of TLS when connecting to + port 5223 possible. + +2004-01-15 12:57 snakeru + + * xmpp/client.py: Bugfix: SASL authentication must be completed + before resource binding. Added TLS early start when connecting + to port 5223. + +2004-01-12 11:35 snakeru + + * xmpp/client.py: Made TLS and SASL use more flexible to work with + ejabberd server. + +2004-01-12 11:32 snakeru + + * xmpp/auth.py: Added "any time" SASL auth status. + +2004-01-12 11:31 snakeru + + * xmpp/simplexml.py: Added fancy XML formatting (indents and + newlines). + +2004-01-12 09:40 snakeru + + * xmpp/auth.py: Maked use of Dispatcher's features tag caching. + Fixed issues with values quoting in ejabberd's challenge + response. + +2004-01-12 09:34 snakeru + + * xmpp/: dispatcher.py, transports.py: Maked dispatcher to cache + features tag. + +2004-01-10 10:35 snakeru + + * xmpp/protocol.py: Fixed case-handling in JIDs comparsions + +2004-01-09 10:14 snakeru + + * xmpp/protocol.py: Added comparsion methods. + +2004-01-08 21:10 snakeru + + * xmpp/auth.py: Maked use of resultNode and errorNode service + functions. Added component:accept authentication. + +2004-01-08 21:08 snakeru + + * xmpp/client.py: Bugfixes: replaced "m" with "self" in many cases + in Client code. Bugfix: Fixed client TLS init to start only if + server supports. Added Component code. Cleaned up tetsing stuff + in the end of file. + +2004-01-08 20:58 snakeru + + * xmpp/dispatcher.py: Changed WaitForResponse to always return + received Node if it were really received. Maked send to stamp + not only "ID" field on outgoung stanzas nor also a "from" filed. + +2004-01-08 20:56 snakeru + + * xmpp/features.py: Maked use of errorNode and resultNode + conditional functions. + +2004-01-08 20:54 snakeru + + * xmpp/protocol.py: Two conditional service functions added: + errorNode and resultNode. + +2004-01-08 09:17 snakeru + + * xmpp/simplexml.py: payload again can be of non-[] non-() type. + Optimised adding child node when child already ready. + +2004-01-07 12:41 snakeru + + * xmpp/: features.py, protocol.py: Syntactic changes and bugfixes + in protocol.DataForm. + +2004-01-07 12:40 snakeru + + * xmpp/simplexml.py: Bugfix: tag.getTags were broken. + +2003-12-15 18:04 snakeru + + * xmpp/auth.py: Fixed incompartibilityes with jabberd2 in + MD5-DIGEST algorythm. + +2003-12-14 22:32 snakeru + + * xmpp/: auth.py, browser.py, client.py, dispatcher.py, + features.py, protocol.py, roster.py, simplexml.py, transports.py: + Tuned SASL (though it still not working), maked it to restart + Dispatcher after auth. Added bind (though it also not working on + my server). Added features import. Added jabber:x:data handling + into protocol. Added roster control methods into roster. + +2003-12-14 22:13 snakeru + + * xmpp/features.py: Service/agents discovery, [un]registration and + password change, privacy lists handling. + +2003-12-14 22:11 snakeru + + * xmpp/features.py: This file prevents main branch from adding the + same file. + +2003-12-14 21:41 snakeru + + * xmpp/features.py: Initial revision + +2003-12-14 21:41 snakeru + + * xmpp/: auth.py, client.py, dispatcher.py, features.py, + protocol.py, roster.py, simplexml.py: Added service discovery, + [un]registration, privacy lists handling. + +2003-12-13 11:30 snakeru + + * xmpp/: auth.py, client.py, simplexml.py: Added and tested SASL + PLAIN. Added and tested SASL DIGEST-MD5. Though it works only on + test example from RFC2831 :( Added SASL test code to client. + +2003-12-12 22:28 snakeru + + * xmpp/: auth.py, browser.py, client.py, debug.py, dispatcher.py, + protocol.py, roster.py, simplexml.py, transports.py, + ietf-docs/draft-ietf-xmpp-core.html, + ietf-docs/draft-ietf-xmpp-im.html: Initial revision + +2003-12-12 22:28 snakeru + + * xmpp/: auth.py, browser.py, client.py, debug.py, dispatcher.py, + protocol.py, roster.py, simplexml.py, transports.py, + ietf-docs/draft-ietf-xmpp-core.html, + ietf-docs/draft-ietf-xmpp-im.html: Working items: roster, events + mechanism, starttls, Non-SASL authorization. + diff --git a/src/xmpppy-0.5.0rc1/MANIFEST b/src/xmpppy-0.5.0rc1/MANIFEST new file mode 100644 index 00000000..01d0e387 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/MANIFEST @@ -0,0 +1,19 @@ +README +setup.py +Makefile +ChangeLog +xmpp/__init__.py +xmpp/auth.py +xmpp/browser.py +xmpp/client.py +xmpp/commands.py +xmpp/debug.py +xmpp/dispatcher.py +xmpp/features.py +xmpp/filetransfer.py +xmpp/jep0106.py +xmpp/protocol.py +xmpp/roster.py +xmpp/session.py +xmpp/simplexml.py +xmpp/transports.py diff --git a/src/xmpppy-0.5.0rc1/Makefile b/src/xmpppy-0.5.0rc1/Makefile new file mode 100644 index 00000000..b2d7c3f5 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/Makefile @@ -0,0 +1,7 @@ + +#MODULESDIR=/usr/lib/($PYTHONVERSION)/site-packages + +install: + # Add here commands to install the package into debian/python-xmpp + [ -d $(MODULESDIR)/xmpp ] || mkdir $(MODULESDIR)/xmpp + install -m 0644 xmpp/*py $(MODULESDIR)/xmpp diff --git a/src/xmpppy-0.5.0rc1/README b/src/xmpppy-0.5.0rc1/README new file mode 100644 index 00000000..1042740d --- /dev/null +++ b/src/xmpppy-0.5.0rc1/README @@ -0,0 +1,43 @@ + Introduction to xmpppy. + http://xmpppy.sf.net/ + + This is my work to replace the jabberpy with the current and maintained +project. + Now the project nears feature freese of 0.2 branch. Almost all goals are +achieved already. Though the main goal was to write a documentation - at least +a line for every feature of library. Yesterday I have checked in last docstrings +for all yet undocumented modules and now I can say that this issue is resolved +(at least for 0.2 release level). + Documentation exists in three formats. + - The first is the examples that I wrote to show xmpppy in action. This is + two simple scripts - README.py and xsend.py. + - The second is the html pages where I try to describe the idea of library + and the ways the goals are achieved. + - Third is the docstrings. I am currently using epydoc but other tools + should work too (at least the pydoc works) + + Installation +If you are using Debian (sarge or above) you can simply run +apt-get install python-xmpp +and you will get the current stable release of xmpppy installed. After installation +you can do 'import xmpp'. Though currently debian contains 0.1 release of xmpppy so +if you want to use 0.2 branch you should install it manually (python2.3 required). +Here you have several options: + - run 'python setup.py install' from xmpppy distribution as root. + All should work nice. + - if you don't like python installator - just copy xmpp directory into python's + site-packages directory (this is what setup.py does). + - If you have no intention to install library system-wide (or just have no + privileges to do it) you can copy xmpp directory just in your application's + directory. Example: + myxmpppytry/ + xmpp/ + ...xmpppy modules + test.py + +If you have any questions about xmpppy usage or you have find a bug or want +to share some ideas - you are welcome in xmpppy-devel maillist - see +http://lists.sourceforge.net/lists/listinfo/xmpppy-devel +for details of subscription. + +2004.12.26 Alexey Nezhdanov diff --git a/src/xmpppy-0.5.0rc1/build/lib/xmpp/__init__.py b/src/xmpppy-0.5.0rc1/build/lib/xmpp/__init__.py new file mode 100644 index 00000000..ad03b288 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/build/lib/xmpp/__init__.py @@ -0,0 +1,31 @@ +# $Id: __init__.py,v 1.9 2005/03/07 09:34:51 snakeru Exp $ + +""" +All features of xmpppy library contained within separate modules. +At present there are modules: +simplexml - XML handling routines +protocol - jabber-objects (I.e. JID and different stanzas and sub-stanzas) handling routines. +debug - Jacob Lundquist's debugging module. Very handy if you like colored debug. +auth - Non-SASL and SASL stuff. You will need it to auth as a client or transport. +transports - low level connection handling. TCP and TLS currently. HTTP support planned. +roster - simple roster for use in clients. +dispatcher - decision-making logic. Handles all hooks. The first who takes control over fresh stanzas. +features - different stuff that didn't worths separating into modules +browser - DISCO server framework. Allows to build dynamic disco tree. +filetransfer - Currently contains only IBB stuff. Can be used for bot-to-bot transfers. + +Most of the classes that is defined in all these modules is an ancestors of +class PlugIn so they share a single set of methods allowing you to compile +a featured XMPP client. For every instance of PlugIn class the 'owner' is the class +in what the plug was plugged. While plugging in such instance usually sets some +methods of owner to it's own ones for easy access. All session specific info stored +either in instance of PlugIn or in owner's instance. This is considered unhandy +and there are plans to port 'Session' class from xmppd.py project for storing all +session-related info. Though if you are not accessing instances variables directly +and use only methods for access all values you should not have any problems. + +""" + +import simplexml,protocol,debug,auth,transports,roster,dispatcher,features,browser,filetransfer,commands +from client import * +from protocol import * diff --git a/src/xmpppy-0.5.0rc1/build/lib/xmpp/auth.py b/src/xmpppy-0.5.0rc1/build/lib/xmpp/auth.py new file mode 100644 index 00000000..6e51d72b --- /dev/null +++ b/src/xmpppy-0.5.0rc1/build/lib/xmpp/auth.py @@ -0,0 +1,326 @@ +## auth.py +## +## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: auth.py,v 1.41 2008/09/13 21:45:21 normanr Exp $ + +""" +Provides library with all Non-SASL and SASL authentication mechanisms. +Can be used both for client and transport authentication. +""" + +from protocol import * +from client import PlugIn +import sha,base64,random,dispatcher,re + +import md5 +def HH(some): return md5.new(some).hexdigest() +def H(some): return md5.new(some).digest() +def C(some): return ':'.join(some) + +class NonSASL(PlugIn): + """ Implements old Non-SASL (JEP-0078) authentication used in jabberd1.4 and transport authentication.""" + def __init__(self,user,password,resource): + """ Caches username, password and resource for auth. """ + PlugIn.__init__(self) + self.DBG_LINE='gen_auth' + self.user=user + self.password=password + self.resource=resource + + def plugin(self,owner): + """ Determine the best auth method (digest/0k/plain) and use it for auth. + Returns used method name on success. Used internally. """ + if not self.resource: return self.authComponent(owner) + self.DEBUG('Querying server about possible auth methods','start') + resp=owner.Dispatcher.SendAndWaitForResponse(Iq('get',NS_AUTH,payload=[Node('username',payload=[self.user])])) + if not isResultNode(resp): + self.DEBUG('No result node arrived! Aborting...','error') + return + iq=Iq(typ='set',node=resp) + query=iq.getTag('query') + query.setTagData('username',self.user) + query.setTagData('resource',self.resource) + + if query.getTag('digest'): + self.DEBUG("Performing digest authentication",'ok') + query.setTagData('digest',sha.new(owner.Dispatcher.Stream._document_attrs['id']+self.password).hexdigest()) + if query.getTag('password'): query.delChild('password') + method='digest' + elif query.getTag('token'): + token=query.getTagData('token') + seq=query.getTagData('sequence') + self.DEBUG("Performing zero-k authentication",'ok') + hash = sha.new(sha.new(self.password).hexdigest()+token).hexdigest() + for foo in xrange(int(seq)): hash = sha.new(hash).hexdigest() + query.setTagData('hash',hash) + method='0k' + else: + self.DEBUG("Sequre methods unsupported, performing plain text authentication",'warn') + query.setTagData('password',self.password) + method='plain' + resp=owner.Dispatcher.SendAndWaitForResponse(iq) + if isResultNode(resp): + self.DEBUG('Sucessfully authenticated with remove host.','ok') + owner.User=self.user + owner.Resource=self.resource + owner._registered_name=owner.User+'@'+owner.Server+'/'+owner.Resource + return method + self.DEBUG('Authentication failed!','error') + + def authComponent(self,owner): + """ Authenticate component. Send handshake stanza and wait for result. Returns "ok" on success. """ + self.handshake=0 + owner.send(Node(NS_COMPONENT_ACCEPT+' handshake',payload=[sha.new(owner.Dispatcher.Stream._document_attrs['id']+self.password).hexdigest()])) + owner.RegisterHandler('handshake',self.handshakeHandler,xmlns=NS_COMPONENT_ACCEPT) + while not self.handshake: + self.DEBUG("waiting on handshake",'notify') + owner.Process(1) + owner._registered_name=self.user + if self.handshake+1: return 'ok' + + def handshakeHandler(self,disp,stanza): + """ Handler for registering in dispatcher for accepting transport authentication. """ + if stanza.getName()=='handshake': self.handshake=1 + else: self.handshake=-1 + +class SASL(PlugIn): + """ Implements SASL authentication. """ + def __init__(self,username,password): + PlugIn.__init__(self) + self.username=username + self.password=password + + def plugin(self,owner): + if not self._owner.Dispatcher.Stream._document_attrs.has_key('version'): self.startsasl='not-supported' + elif self._owner.Dispatcher.Stream.features: + try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) + except NodeProcessed: pass + else: self.startsasl=None + + def auth(self): + """ Start authentication. Result can be obtained via "SASL.startsasl" attribute and will be + either "success" or "failure". Note that successfull auth will take at least + two Dispatcher.Process() calls. """ + if self.startsasl: pass + elif self._owner.Dispatcher.Stream.features: + try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) + except NodeProcessed: pass + else: self._owner.RegisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) + + def plugout(self): + """ Remove SASL handlers from owner's dispatcher. Used internally. """ + if self._owner.__dict__.has_key('features'): self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) + if self._owner.__dict__.has_key('challenge'): self._owner.UnregisterHandler('challenge',self.SASLHandler,xmlns=NS_SASL) + if self._owner.__dict__.has_key('failure'): self._owner.UnregisterHandler('failure',self.SASLHandler,xmlns=NS_SASL) + if self._owner.__dict__.has_key('success'): self._owner.UnregisterHandler('success',self.SASLHandler,xmlns=NS_SASL) + + def FeaturesHandler(self,conn,feats): + """ Used to determine if server supports SASL auth. Used internally. """ + if not feats.getTag('mechanisms',namespace=NS_SASL): + self.startsasl='not-supported' + self.DEBUG('SASL not supported by server','error') + return + mecs=[] + for mec in feats.getTag('mechanisms',namespace=NS_SASL).getTags('mechanism'): + mecs.append(mec.getData()) + self._owner.RegisterHandler('challenge',self.SASLHandler,xmlns=NS_SASL) + self._owner.RegisterHandler('failure',self.SASLHandler,xmlns=NS_SASL) + self._owner.RegisterHandler('success',self.SASLHandler,xmlns=NS_SASL) + if "ANONYMOUS" in mecs and self.username == None: + node=Node('auth',attrs={'xmlns':NS_SASL,'mechanism':'ANONYMOUS'}) + elif "DIGEST-MD5" in mecs: + node=Node('auth',attrs={'xmlns':NS_SASL,'mechanism':'DIGEST-MD5'}) + elif "PLAIN" in mecs: + sasl_data='%s\x00%s\x00%s'%(self.username+'@'+self._owner.Server,self.username,self.password) + node=Node('auth',attrs={'xmlns':NS_SASL,'mechanism':'PLAIN'},payload=[base64.encodestring(sasl_data).replace('\r','').replace('\n','')]) + else: + self.startsasl='failure' + self.DEBUG('I can only use DIGEST-MD5 and PLAIN mecanisms.','error') + return + self.startsasl='in-process' + self._owner.send(node.__str__()) + raise NodeProcessed + + def SASLHandler(self,conn,challenge): + """ Perform next SASL auth step. Used internally. """ + if challenge.getNamespace()<>NS_SASL: return + if challenge.getName()=='failure': + self.startsasl='failure' + try: reason=challenge.getChildren()[0] + except: reason=challenge + self.DEBUG('Failed SASL authentification: %s'%reason,'error') + raise NodeProcessed + elif challenge.getName()=='success': + self.startsasl='success' + self.DEBUG('Successfully authenticated with remote server.','ok') + handlers=self._owner.Dispatcher.dumpHandlers() + self._owner.Dispatcher.PlugOut() + dispatcher.Dispatcher().PlugIn(self._owner) + self._owner.Dispatcher.restoreHandlers(handlers) + self._owner.User=self.username + raise NodeProcessed +########################################3333 + incoming_data=challenge.getData() + chal={} + data=base64.decodestring(incoming_data) + self.DEBUG('Got challenge:'+data,'ok') + for pair in re.findall('(\w+\s*=\s*(?:(?:"[^"]+")|(?:[^,]+)))',data): + key,value=[x.strip() for x in pair.split('=', 1)] + if value[:1]=='"' and value[-1:]=='"': value=value[1:-1] + chal[key]=value + if chal.has_key('qop') and 'auth' in [x.strip() for x in chal['qop'].split(',')]: + resp={} + resp['username']=self.username + resp['realm']=self._owner.Server + resp['nonce']=chal['nonce'] + cnonce='' + for i in range(7): + cnonce+=hex(int(random.random()*65536*4096))[2:] + resp['cnonce']=cnonce + resp['nc']=('00000001') + resp['qop']='auth' + resp['digest-uri']='xmpp/'+self._owner.Server + A1=C([H(C([resp['username'],resp['realm'],self.password])),resp['nonce'],resp['cnonce']]) + A2=C(['AUTHENTICATE',resp['digest-uri']]) + response= HH(C([HH(A1),resp['nonce'],resp['nc'],resp['cnonce'],resp['qop'],HH(A2)])) + resp['response']=response + resp['charset']='utf-8' + sasl_data='' + for key in ['charset','username','realm','nonce','nc','cnonce','digest-uri','response','qop']: + if key in ['nc','qop','response','charset']: sasl_data+="%s=%s,"%(key,resp[key]) + else: sasl_data+='%s="%s",'%(key,resp[key]) +########################################3333 + node=Node('response',attrs={'xmlns':NS_SASL},payload=[base64.encodestring(sasl_data[:-1]).replace('\r','').replace('\n','')]) + self._owner.send(node.__str__()) + elif chal.has_key('rspauth'): self._owner.send(Node('response',attrs={'xmlns':NS_SASL}).__str__()) + else: + self.startsasl='failure' + self.DEBUG('Failed SASL authentification: unknown challenge','error') + raise NodeProcessed + +class Bind(PlugIn): + """ Bind some JID to the current connection to allow router know of our location.""" + def __init__(self): + PlugIn.__init__(self) + self.DBG_LINE='bind' + self.bound=None + + def plugin(self,owner): + """ Start resource binding, if allowed at this time. Used internally. """ + if self._owner.Dispatcher.Stream.features: + try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) + except NodeProcessed: pass + else: self._owner.RegisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) + + def plugout(self): + """ Remove Bind handler from owner's dispatcher. Used internally. """ + self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) + + def FeaturesHandler(self,conn,feats): + """ Determine if server supports resource binding and set some internal attributes accordingly. """ + if not feats.getTag('bind',namespace=NS_BIND): + self.bound='failure' + self.DEBUG('Server does not requested binding.','error') + return + if feats.getTag('session',namespace=NS_SESSION): self.session=1 + else: self.session=-1 + self.bound=[] + + def Bind(self,resource=None): + """ Perform binding. Use provided resource name or random (if not provided). """ + while self.bound is None and self._owner.Process(1): pass + if resource: resource=[Node('resource',payload=[resource])] + else: resource=[] + resp=self._owner.SendAndWaitForResponse(Protocol('iq',typ='set',payload=[Node('bind',attrs={'xmlns':NS_BIND},payload=resource)])) + if isResultNode(resp): + self.bound.append(resp.getTag('bind').getTagData('jid')) + self.DEBUG('Successfully bound %s.'%self.bound[-1],'ok') + jid=JID(resp.getTag('bind').getTagData('jid')) + self._owner.User=jid.getNode() + self._owner.Resource=jid.getResource() + resp=self._owner.SendAndWaitForResponse(Protocol('iq',typ='set',payload=[Node('session',attrs={'xmlns':NS_SESSION})])) + if isResultNode(resp): + self.DEBUG('Successfully opened session.','ok') + self.session=1 + return 'ok' + else: + self.DEBUG('Session open failed.','error') + self.session=0 + elif resp: self.DEBUG('Binding failed: %s.'%resp.getTag('error'),'error') + else: + self.DEBUG('Binding failed: timeout expired.','error') + return '' + +class ComponentBind(PlugIn): + """ ComponentBind some JID to the current connection to allow router know of our location.""" + def __init__(self, sasl): + PlugIn.__init__(self) + self.DBG_LINE='bind' + self.bound=None + self.needsUnregister=None + self.sasl = sasl + + def plugin(self,owner): + """ Start resource binding, if allowed at this time. Used internally. """ + if not self.sasl: + self.bound=[] + return + if self._owner.Dispatcher.Stream.features: + try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) + except NodeProcessed: pass + else: + self._owner.RegisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) + self.needsUnregister=1 + + def plugout(self): + """ Remove ComponentBind handler from owner's dispatcher. Used internally. """ + if self.needsUnregister: + self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) + + def FeaturesHandler(self,conn,feats): + """ Determine if server supports resource binding and set some internal attributes accordingly. """ + if not feats.getTag('bind',namespace=NS_BIND): + self.bound='failure' + self.DEBUG('Server does not requested binding.','error') + return + if feats.getTag('session',namespace=NS_SESSION): self.session=1 + else: self.session=-1 + self.bound=[] + + def Bind(self,domain=None): + """ Perform binding. Use provided domain name (if not provided). """ + while self.bound is None and self._owner.Process(1): pass + if self.sasl: + xmlns = NS_COMPONENT_1 + else: + xmlns = None + self.bindresponse = None + ttl = dispatcher.DefaultTimeout + self._owner.RegisterHandler('bind',self.BindHandler,xmlns=xmlns) + self._owner.send(Protocol('bind',attrs={'name':domain},xmlns=NS_COMPONENT_1)) + while self.bindresponse is None and self._owner.Process(1) and ttl > 0: ttl-=1 + self._owner.UnregisterHandler('bind',self.BindHandler,xmlns=xmlns) + resp=self.bindresponse + if resp and resp.getAttr('error'): + self.DEBUG('Binding failed: %s.'%resp.getAttr('error'),'error') + elif resp: + self.DEBUG('Successfully bound.','ok') + return 'ok' + else: + self.DEBUG('Binding failed: timeout expired.','error') + return '' + + def BindHandler(self,conn,bind): + self.bindresponse = bind + pass diff --git a/src/xmpppy-0.5.0rc1/build/lib/xmpp/browser.py b/src/xmpppy-0.5.0rc1/build/lib/xmpp/browser.py new file mode 100644 index 00000000..8848ea4e --- /dev/null +++ b/src/xmpppy-0.5.0rc1/build/lib/xmpp/browser.py @@ -0,0 +1,221 @@ +## browser.py +## +## Copyright (C) 2004 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: browser.py,v 1.12 2007/05/13 17:55:14 normanr Exp $ + +"""Browser module provides DISCO server framework for your application. +This functionality can be used for very different purposes - from publishing +software version and supported features to building of "jabber site" that users +can navigate with their disco browsers and interact with active content. + +Such functionality is achieved via registering "DISCO handlers" that are +automatically called when user requests some node of your disco tree. +""" + +from dispatcher import * +from client import PlugIn + +class Browser(PlugIn): + """ WARNING! This class is for components only. It will not work in client mode! + + Standart xmpppy class that is ancestor of PlugIn and can be attached + to your application. + All processing will be performed in the handlers registered in the browser + instance. You can register any number of handlers ensuring that for each + node/jid combination only one (or none) handler registered. + You can register static information or the fully-blown function that will + calculate the answer dynamically. + Example of static info (see JEP-0030, examples 13-14): + # cl - your xmpppy connection instance. + b=xmpp.browser.Browser() + b.PlugIn(cl) + items=[] + item={} + item['jid']='catalog.shakespeare.lit' + item['node']='books' + item['name']='Books by and about Shakespeare' + items.append(item) + item={} + item['jid']='catalog.shakespeare.lit' + item['node']='clothing' + item['name']='Wear your literary taste with pride' + items.append(item) + item={} + item['jid']='catalog.shakespeare.lit' + item['node']='music' + item['name']='Music from the time of Shakespeare' + items.append(item) + info={'ids':[], 'features':[]} + b.setDiscoHandler({'items':items,'info':info}) + + items should be a list of item elements. + every item element can have any of these four keys: 'jid', 'node', 'name', 'action' + info should be a dicionary and must have keys 'ids' and 'features'. + Both of them should be lists: + ids is a list of dictionaries and features is a list of text strings. + Example (see JEP-0030, examples 1-2) + # cl - your xmpppy connection instance. + b=xmpp.browser.Browser() + b.PlugIn(cl) + items=[] + ids=[] + ids.append({'category':'conference','type':'text','name':'Play-Specific Chatrooms'}) + ids.append({'category':'directory','type':'chatroom','name':'Play-Specific Chatrooms'}) + features=[NS_DISCO_INFO,NS_DISCO_ITEMS,NS_MUC,NS_REGISTER,NS_SEARCH,NS_TIME,NS_VERSION] + info={'ids':ids,'features':features} + # info['xdata']=xmpp.protocol.DataForm() # JEP-0128 + b.setDiscoHandler({'items':[],'info':info}) + """ + def __init__(self): + """Initialises internal variables. Used internally.""" + PlugIn.__init__(self) + DBG_LINE='browser' + self._exported_methods=[] + self._handlers={'':{}} + + def plugin(self, owner): + """ Registers it's own iq handlers in your application dispatcher instance. + Used internally.""" + owner.RegisterHandler('iq',self._DiscoveryHandler,typ='get',ns=NS_DISCO_INFO) + owner.RegisterHandler('iq',self._DiscoveryHandler,typ='get',ns=NS_DISCO_ITEMS) + + def plugout(self): + """ Unregisters browser's iq handlers from your application dispatcher instance. + Used internally.""" + self._owner.UnregisterHandler('iq',self._DiscoveryHandler,typ='get',ns=NS_DISCO_INFO) + self._owner.UnregisterHandler('iq',self._DiscoveryHandler,typ='get',ns=NS_DISCO_ITEMS) + + def _traversePath(self,node,jid,set=0): + """ Returns dictionary and key or None,None + None - root node (w/o "node" attribute) + /a/b/c - node + /a/b/ - branch + Set returns '' or None as the key + get returns '' or None as the key or None as the dict. + Used internally.""" + if self._handlers.has_key(jid): cur=self._handlers[jid] + elif set: + self._handlers[jid]={} + cur=self._handlers[jid] + else: cur=self._handlers[''] + if node is None: node=[None] + else: node=node.replace('/',' /').split('/') + for i in node: + if i<>'' and cur.has_key(i): cur=cur[i] + elif set and i<>'': cur[i]={dict:cur,str:i}; cur=cur[i] + elif set or cur.has_key(''): return cur,'' + else: return None,None + if cur.has_key(1) or set: return cur,1 + raise "Corrupted data" + + def setDiscoHandler(self,handler,node='',jid=''): + """ This is the main method that you will use in this class. + It is used to register supplied DISCO handler (or dictionary with static info) + as handler of some disco tree branch. + If you do not specify the node this handler will be used for all queried nodes. + If you do not specify the jid this handler will be used for all queried JIDs. + + Usage: + cl.Browser.setDiscoHandler(someDict,node,jid) + or + cl.Browser.setDiscoHandler(someDISCOHandler,node,jid) + where + + someDict={ + 'items':[ + {'jid':'jid1','action':'action1','node':'node1','name':'name1'}, + {'jid':'jid2','action':'action2','node':'node2','name':'name2'}, + {'jid':'jid3','node':'node3','name':'name3'}, + {'jid':'jid4','node':'node4'} + ], + 'info' :{ + 'ids':[ + {'category':'category1','type':'type1','name':'name1'}, + {'category':'category2','type':'type2','name':'name2'}, + {'category':'category3','type':'type3','name':'name3'}, + ], + 'features':['feature1','feature2','feature3','feature4'], + 'xdata':DataForm + } + } + + and/or + + def someDISCOHandler(session,request,TYR): + # if TYR=='items': # returns items list of the same format as shown above + # elif TYR=='info': # returns info dictionary of the same format as shown above + # else: # this case is impossible for now. + """ + self.DEBUG('Registering handler %s for "%s" node->%s'%(handler,jid,node), 'info') + node,key=self._traversePath(node,jid,1) + node[key]=handler + + def getDiscoHandler(self,node='',jid=''): + """ Returns the previously registered DISCO handler + that is resonsible for this node/jid combination. + Used internally.""" + node,key=self._traversePath(node,jid) + if node: return node[key] + + def delDiscoHandler(self,node='',jid=''): + """ Unregisters DISCO handler that is resonsible for this + node/jid combination. When handler is unregistered the branch + is handled in the same way that it's parent branch from this moment. + """ + node,key=self._traversePath(node,jid) + if node: + handler=node[key] + del node[dict][node[str]] + return handler + + def _DiscoveryHandler(self,conn,request): + """ Servers DISCO iq request from the remote client. + Automatically determines the best handler to use and calls it + to handle the request. Used internally. + """ + node=request.getQuerynode() + if node: + nodestr=node + else: + nodestr='None' + handler=self.getDiscoHandler(node,request.getTo()) + if not handler: + self.DEBUG("No Handler for request with jid->%s node->%s ns->%s"%(request.getTo().__str__().encode('utf8'),nodestr.encode('utf8'),request.getQueryNS().encode('utf8')),'error') + conn.send(Error(request,ERR_ITEM_NOT_FOUND)) + raise NodeProcessed + self.DEBUG("Handling request with jid->%s node->%s ns->%s"%(request.getTo().__str__().encode('utf8'),nodestr.encode('utf8'),request.getQueryNS().encode('utf8')),'ok') + rep=request.buildReply('result') + if node: rep.setQuerynode(node) + q=rep.getTag('query') + if request.getQueryNS()==NS_DISCO_ITEMS: + # handler must return list: [{jid,action,node,name}] + if type(handler)==dict: lst=handler['items'] + else: lst=handler(conn,request,'items') + if lst==None: + conn.send(Error(request,ERR_ITEM_NOT_FOUND)) + raise NodeProcessed + for item in lst: q.addChild('item',item) + elif request.getQueryNS()==NS_DISCO_INFO: + if type(handler)==dict: dt=handler['info'] + else: dt=handler(conn,request,'info') + if dt==None: + conn.send(Error(request,ERR_ITEM_NOT_FOUND)) + raise NodeProcessed + # handler must return dictionary: + # {'ids':[{},{},{},{}], 'features':[fe,at,ur,es], 'xdata':DataForm} + for id in dt['ids']: q.addChild('identity',id) + for feature in dt['features']: q.addChild('feature',{'var':feature}) + if dt.has_key('xdata'): q.addChild(node=dt['xdata']) + conn.send(rep) + raise NodeProcessed diff --git a/src/xmpppy-0.5.0rc1/build/lib/xmpp/client.py b/src/xmpppy-0.5.0rc1/build/lib/xmpp/client.py new file mode 100644 index 00000000..4d932119 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/build/lib/xmpp/client.py @@ -0,0 +1,325 @@ +## client.py +## +## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: client.py,v 1.61 2009/04/07 06:19:42 snakeru Exp $ + +""" +Provides PlugIn class functionality to develop extentions for xmpppy. +Also provides Client and Component classes implementations as the +examples of xmpppy structures usage. +These classes can be used for simple applications "AS IS" though. +""" + +import socket +import debug +Debug=debug +Debug.DEBUGGING_IS_ON=1 +Debug.Debug.colors['socket']=debug.color_dark_gray +Debug.Debug.colors['CONNECTproxy']=debug.color_dark_gray +Debug.Debug.colors['nodebuilder']=debug.color_brown +Debug.Debug.colors['client']=debug.color_cyan +Debug.Debug.colors['component']=debug.color_cyan +Debug.Debug.colors['dispatcher']=debug.color_green +Debug.Debug.colors['browser']=debug.color_blue +Debug.Debug.colors['auth']=debug.color_yellow +Debug.Debug.colors['roster']=debug.color_magenta +Debug.Debug.colors['ibb']=debug.color_yellow + +Debug.Debug.colors['down']=debug.color_brown +Debug.Debug.colors['up']=debug.color_brown +Debug.Debug.colors['data']=debug.color_brown +Debug.Debug.colors['ok']=debug.color_green +Debug.Debug.colors['warn']=debug.color_yellow +Debug.Debug.colors['error']=debug.color_red +Debug.Debug.colors['start']=debug.color_dark_gray +Debug.Debug.colors['stop']=debug.color_dark_gray +Debug.Debug.colors['sent']=debug.color_yellow +Debug.Debug.colors['got']=debug.color_bright_cyan + +DBG_CLIENT='client' +DBG_COMPONENT='component' + +class PlugIn: + """ Common xmpppy plugins infrastructure: plugging in/out, debugging. """ + def __init__(self): + self._exported_methods=[] + self.DBG_LINE=self.__class__.__name__.lower() + + def PlugIn(self,owner): + """ Attach to main instance and register ourself and all our staff in it. """ + self._owner=owner + if self.DBG_LINE not in owner.debug_flags: + owner.debug_flags.append(self.DBG_LINE) + self.DEBUG('Plugging %s into %s'%(self,self._owner),'start') + if owner.__dict__.has_key(self.__class__.__name__): + return self.DEBUG('Plugging ignored: another instance already plugged.','error') + self._old_owners_methods=[] + for method in self._exported_methods: + if owner.__dict__.has_key(method.__name__): + self._old_owners_methods.append(owner.__dict__[method.__name__]) + owner.__dict__[method.__name__]=method + owner.__dict__[self.__class__.__name__]=self + if self.__class__.__dict__.has_key('plugin'): return self.plugin(owner) + + def PlugOut(self): + """ Unregister all our staff from main instance and detach from it. """ + self.DEBUG('Plugging %s out of %s.'%(self,self._owner),'stop') + ret = None + if self.__class__.__dict__.has_key('plugout'): ret = self.plugout() + self._owner.debug_flags.remove(self.DBG_LINE) + for method in self._exported_methods: del self._owner.__dict__[method.__name__] + for method in self._old_owners_methods: self._owner.__dict__[method.__name__]=method + del self._owner.__dict__[self.__class__.__name__] + return ret + + def DEBUG(self,text,severity='info'): + """ Feed a provided debug line to main instance's debug facility along with our ID string. """ + self._owner.DEBUG(self.DBG_LINE,text,severity) + +import transports,dispatcher,auth,roster +class CommonClient: + """ Base for Client and Component classes.""" + def __init__(self,server,port=5222,debug=['always', 'nodebuilder']): + """ Caches server name and (optionally) port to connect to. "debug" parameter specifies + the debug IDs that will go into debug output. You can either specifiy an "include" + or "exclude" list. The latter is done via adding "always" pseudo-ID to the list. + Full list: ['nodebuilder', 'dispatcher', 'gen_auth', 'SASL_auth', 'bind', 'socket', + 'CONNECTproxy', 'TLS', 'roster', 'browser', 'ibb'] . """ + if self.__class__.__name__=='Client': self.Namespace,self.DBG='jabber:client',DBG_CLIENT + elif self.__class__.__name__=='Component': self.Namespace,self.DBG=dispatcher.NS_COMPONENT_ACCEPT,DBG_COMPONENT + self.defaultNamespace=self.Namespace + self.disconnect_handlers=[] + self.Server=server + self.Port=port + if debug and type(debug)<>list: debug=['always', 'nodebuilder'] + self._DEBUG=Debug.Debug(debug) + self.DEBUG=self._DEBUG.Show + self.debug_flags=self._DEBUG.debug_flags + self.debug_flags.append(self.DBG) + self._owner=self + self._registered_name=None + self.RegisterDisconnectHandler(self.DisconnectHandler) + self.connected='' + self._route=0 + + def RegisterDisconnectHandler(self,handler): + """ Register handler that will be called on disconnect.""" + self.disconnect_handlers.append(handler) + + def UnregisterDisconnectHandler(self,handler): + """ Unregister handler that is called on disconnect.""" + self.disconnect_handlers.remove(handler) + + def disconnected(self): + """ Called on disconnection. Calls disconnect handlers and cleans things up. """ + self.connected='' + self.DEBUG(self.DBG,'Disconnect detected','stop') + self.disconnect_handlers.reverse() + for i in self.disconnect_handlers: i() + self.disconnect_handlers.reverse() + if self.__dict__.has_key('TLS'): self.TLS.PlugOut() + + def DisconnectHandler(self): + """ Default disconnect handler. Just raises an IOError. + If you choosed to use this class in your production client, + override this method or at least unregister it. """ + raise IOError('Disconnected from server.') + + def event(self,eventName,args={}): + """ Default event handler. To be overriden. """ + print "Event: ",(eventName,args) + + def isConnected(self): + """ Returns connection state. F.e.: None / 'tls' / 'tcp+non_sasl' . """ + return self.connected + + def reconnectAndReauth(self): + """ Example of reconnection method. In fact, it can be used to batch connection and auth as well. """ + handlerssave=self.Dispatcher.dumpHandlers() + if self.__dict__.has_key('ComponentBind'): self.ComponentBind.PlugOut() + if self.__dict__.has_key('Bind'): self.Bind.PlugOut() + self._route=0 + if self.__dict__.has_key('NonSASL'): self.NonSASL.PlugOut() + if self.__dict__.has_key('SASL'): self.SASL.PlugOut() + if self.__dict__.has_key('TLS'): self.TLS.PlugOut() + self.Dispatcher.PlugOut() + if self.__dict__.has_key('HTTPPROXYsocket'): self.HTTPPROXYsocket.PlugOut() + if self.__dict__.has_key('TCPsocket'): self.TCPsocket.PlugOut() + if not self.connect(server=self._Server,proxy=self._Proxy): return + if not self.auth(self._User,self._Password,self._Resource): return + self.Dispatcher.restoreHandlers(handlerssave) + return self.connected + + def connect(self,server=None,proxy=None,ssl=None,use_srv=None): + """ Make a tcp/ip connection, protect it with tls/ssl if possible and start XMPP stream. + Returns None or 'tcp' or 'tls', depending on the result.""" + if not server: server=(self.Server,self.Port) + if proxy: sock=transports.HTTPPROXYsocket(proxy,server,use_srv) + else: sock=transports.TCPsocket(server,use_srv) + connected=sock.PlugIn(self) + if not connected: + sock.PlugOut() + return + self._Server,self._Proxy=server,proxy + self.connected='tcp' + if (ssl is None and self.Connection.getPort() in (5223, 443)) or ssl: + try: # FIXME. This should be done in transports.py + transports.TLS().PlugIn(self,now=1) + self.connected='ssl' + except socket.sslerror: + return + dispatcher.Dispatcher().PlugIn(self) + while self.Dispatcher.Stream._document_attrs is None: + if not self.Process(1): return + if self.Dispatcher.Stream._document_attrs.has_key('version') and self.Dispatcher.Stream._document_attrs['version']=='1.0': + while not self.Dispatcher.Stream.features and self.Process(1): pass # If we get version 1.0 stream the features tag MUST BE presented + return self.connected + +class Client(CommonClient): + """ Example client class, based on CommonClient. """ + def connect(self,server=None,proxy=None,secure=None,use_srv=True): + """ Connect to jabber server. If you want to specify different ip/port to connect to you can + pass it as tuple as first parameter. If there is HTTP proxy between you and server + specify it's address and credentials (if needed) in the second argument. + If you want ssl/tls support to be discovered and enable automatically - leave third argument as None. (ssl will be autodetected only if port is 5223 or 443) + If you want to force SSL start (i.e. if port 5223 or 443 is remapped to some non-standard port) then set it to 1. + If you want to disable tls/ssl support completely, set it to 0. + Example: connect(('192.168.5.5',5222),{'host':'proxy.my.net','port':8080,'user':'me','password':'secret'}) + Returns '' or 'tcp' or 'tls', depending on the result.""" + if not CommonClient.connect(self,server,proxy,secure,use_srv) or secure<>None and not secure: return self.connected + transports.TLS().PlugIn(self) + if not self.Dispatcher.Stream._document_attrs.has_key('version') or not self.Dispatcher.Stream._document_attrs['version']=='1.0': return self.connected + while not self.Dispatcher.Stream.features and self.Process(1): pass # If we get version 1.0 stream the features tag MUST BE presented + if not self.Dispatcher.Stream.features.getTag('starttls'): return self.connected # TLS not supported by server + while not self.TLS.starttls and self.Process(1): pass + if not hasattr(self, 'TLS') or self.TLS.starttls!='success': self.event('tls_failed'); return self.connected + self.connected='tls' + return self.connected + + def auth(self,user,password,resource='',sasl=1): + """ Authenticate connnection and bind resource. If resource is not provided + random one or library name used. """ + self._User,self._Password,self._Resource=user,password,resource + while not self.Dispatcher.Stream._document_attrs and self.Process(1): pass + if self.Dispatcher.Stream._document_attrs.has_key('version') and self.Dispatcher.Stream._document_attrs['version']=='1.0': + while not self.Dispatcher.Stream.features and self.Process(1): pass # If we get version 1.0 stream the features tag MUST BE presented + if sasl: auth.SASL(user,password).PlugIn(self) + if not sasl or self.SASL.startsasl=='not-supported': + if not resource: resource='xmpppy' + if auth.NonSASL(user,password,resource).PlugIn(self): + self.connected+='+old_auth' + return 'old_auth' + return + self.SASL.auth() + while self.SASL.startsasl=='in-process' and self.Process(1): pass + if self.SASL.startsasl=='success': + auth.Bind().PlugIn(self) + while self.Bind.bound is None and self.Process(1): pass + if self.Bind.Bind(resource): + self.connected+='+sasl' + return 'sasl' + else: + if self.__dict__.has_key('SASL'): self.SASL.PlugOut() + + def getRoster(self): + """ Return the Roster instance, previously plugging it in and + requesting roster from server if needed. """ + if not self.__dict__.has_key('Roster'): roster.Roster().PlugIn(self) + return self.Roster.getRoster() + + def sendInitPresence(self,requestRoster=1): + """ Send roster request and initial <presence/>. + You can disable the first by setting requestRoster argument to 0. """ + self.sendPresence(requestRoster=requestRoster) + + def sendPresence(self,jid=None,typ=None,requestRoster=0): + """ Send some specific presence state. + Can also request roster from server if according agrument is set.""" + if requestRoster: roster.Roster().PlugIn(self) + self.send(dispatcher.Presence(to=jid, typ=typ)) + +class Component(CommonClient): + """ Component class. The only difference from CommonClient is ability to perform component authentication. """ + def __init__(self,transport,port=5347,typ=None,debug=['always', 'nodebuilder'],domains=None,sasl=0,bind=0,route=0,xcp=0): + """ Init function for Components. + As components use a different auth mechanism which includes the namespace of the component. + Jabberd1.4 and Ejabberd use the default namespace then for all client messages. + Jabberd2 uses jabber:client. + 'transport' argument is a transport name that you are going to serve (f.e. "irc.localhost"). + 'port' can be specified if 'transport' resolves to correct IP. If it is not then you'll have to specify IP + and port while calling "connect()". + If you are going to serve several different domains with single Component instance - you must list them ALL + in the 'domains' argument. + For jabberd2 servers you should set typ='jabberd2' argument. + """ + CommonClient.__init__(self,transport,port=port,debug=debug) + self.typ=typ + self.sasl=sasl + self.bind=bind + self.route=route + self.xcp=xcp + if domains: + self.domains=domains + else: + self.domains=[transport] + + def connect(self,server=None,proxy=None): + """ This will connect to the server, and if the features tag is found then set + the namespace to be jabber:client as that is required for jabberd2. + 'server' and 'proxy' arguments have the same meaning as in xmpp.Client.connect() """ + if self.sasl: + self.Namespace=auth.NS_COMPONENT_1 + self.Server=server[0] + CommonClient.connect(self,server=server,proxy=proxy) + if self.connected and (self.typ=='jabberd2' or not self.typ and self.Dispatcher.Stream.features != None) and (not self.xcp): + self.defaultNamespace=auth.NS_CLIENT + self.Dispatcher.RegisterNamespace(self.defaultNamespace) + self.Dispatcher.RegisterProtocol('iq',dispatcher.Iq) + self.Dispatcher.RegisterProtocol('message',dispatcher.Message) + self.Dispatcher.RegisterProtocol('presence',dispatcher.Presence) + return self.connected + + def dobind(self, sasl): + # This has to be done before binding, because we can receive a route stanza before binding finishes + self._route = self.route + if self.bind: + for domain in self.domains: + auth.ComponentBind(sasl).PlugIn(self) + while self.ComponentBind.bound is None: self.Process(1) + if (not self.ComponentBind.Bind(domain)): + self.ComponentBind.PlugOut() + return + self.ComponentBind.PlugOut() + + def auth(self,name,password,dup=None): + """ Authenticate component "name" with password "password".""" + self._User,self._Password,self._Resource=name,password,'' + try: + if self.sasl: auth.SASL(name,password).PlugIn(self) + if not self.sasl or self.SASL.startsasl=='not-supported': + if auth.NonSASL(name,password,'').PlugIn(self): + self.dobind(sasl=False) + self.connected+='+old_auth' + return 'old_auth' + return + self.SASL.auth() + while self.SASL.startsasl=='in-process' and self.Process(1): pass + if self.SASL.startsasl=='success': + self.dobind(sasl=True) + self.connected+='+sasl' + return 'sasl' + else: + raise auth.NotAuthorized(self.SASL.startsasl) + except: + self.DEBUG(self.DBG,"Failed to authenticate %s"%name,'error') diff --git a/src/xmpppy-0.5.0rc1/build/lib/xmpp/commands.py b/src/xmpppy-0.5.0rc1/build/lib/xmpp/commands.py new file mode 100644 index 00000000..cdebf8f2 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/build/lib/xmpp/commands.py @@ -0,0 +1,328 @@ +## $Id: commands.py,v 1.17 2007/08/28 09:54:15 normanr Exp $ + +## Ad-Hoc Command manager +## Mike Albon (c) 5th January 2005 + +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + + +"""This module is a ad-hoc command processor for xmpppy. It uses the plug-in mechanism like most of the core library. It depends on a DISCO browser manager. + +There are 3 classes here, a command processor Commands like the Browser, and a command template plugin Command, and an example command. + +To use this module: + + Instansiate the module with the parent transport and disco browser manager as parameters. + 'Plug in' commands using the command template. + The command feature must be added to existing disco replies where neccessary. + +What it supplies: + + Automatic command registration with the disco browser manager. + Automatic listing of commands in the public command list. + A means of handling requests, by redirection though the command manager. +""" + +from protocol import * +from client import PlugIn + +class Commands(PlugIn): + """Commands is an ancestor of PlugIn and can be attached to any session. + + The commands class provides a lookup and browse mechnism. It follows the same priciple of the Browser class, for Service Discovery to provide the list of commands, it adds the 'list' disco type to your existing disco handler function. + + How it works: + The commands are added into the existing Browser on the correct nodes. When the command list is built the supplied discovery handler function needs to have a 'list' option in type. This then gets enumerated, all results returned as None are ignored. + The command executed is then called using it's Execute method. All session management is handled by the command itself. + """ + def __init__(self, browser): + """Initialises class and sets up local variables""" + PlugIn.__init__(self) + DBG_LINE='commands' + self._exported_methods=[] + self._handlers={'':{}} + self._browser = browser + + def plugin(self, owner): + """Makes handlers within the session""" + # Plug into the session and the disco manager + # We only need get and set, results are not needed by a service provider, only a service user. + owner.RegisterHandler('iq',self._CommandHandler,typ='set',ns=NS_COMMANDS) + owner.RegisterHandler('iq',self._CommandHandler,typ='get',ns=NS_COMMANDS) + self._browser.setDiscoHandler(self._DiscoHandler,node=NS_COMMANDS,jid='') + + def plugout(self): + """Removes handlers from the session""" + # unPlug from the session and the disco manager + self._owner.UnregisterHandler('iq',self._CommandHandler,ns=NS_COMMANDS) + for jid in self._handlers: + self._browser.delDiscoHandler(self._DiscoHandler,node=NS_COMMANDS) + + def _CommandHandler(self,conn,request): + """The internal method to process the routing of command execution requests""" + # This is the command handler itself. + # We must: + # Pass on command execution to command handler + # (Do we need to keep session details here, or can that be done in the command?) + jid = str(request.getTo()) + try: + node = request.getTagAttr('command','node') + except: + conn.send(Error(request,ERR_BAD_REQUEST)) + raise NodeProcessed + if self._handlers.has_key(jid): + if self._handlers[jid].has_key(node): + self._handlers[jid][node]['execute'](conn,request) + else: + conn.send(Error(request,ERR_ITEM_NOT_FOUND)) + raise NodeProcessed + elif self._handlers[''].has_key(node): + self._handlers[''][node]['execute'](conn,request) + else: + conn.send(Error(request,ERR_ITEM_NOT_FOUND)) + raise NodeProcessed + + def _DiscoHandler(self,conn,request,typ): + """The internal method to process service discovery requests""" + # This is the disco manager handler. + if typ == 'items': + # We must: + # Generate a list of commands and return the list + # * This handler does not handle individual commands disco requests. + # Pseudo: + # Enumerate the 'item' disco of each command for the specified jid + # Build responce and send + # To make this code easy to write we add an 'list' disco type, it returns a tuple or 'none' if not advertised + list = [] + items = [] + jid = str(request.getTo()) + # Get specific jid based results + if self._handlers.has_key(jid): + for each in self._handlers[jid].keys(): + items.append((jid,each)) + else: + # Get generic results + for each in self._handlers[''].keys(): + items.append(('',each)) + if items != []: + for each in items: + i = self._handlers[each[0]][each[1]]['disco'](conn,request,'list') + if i != None: + list.append(Node(tag='item',attrs={'jid':i[0],'node':i[1],'name':i[2]})) + iq = request.buildReply('result') + if request.getQuerynode(): iq.setQuerynode(request.getQuerynode()) + iq.setQueryPayload(list) + conn.send(iq) + else: + conn.send(Error(request,ERR_ITEM_NOT_FOUND)) + raise NodeProcessed + elif typ == 'info': + return {'ids':[{'category':'automation','type':'command-list'}],'features':[]} + + def addCommand(self,name,cmddisco,cmdexecute,jid=''): + """The method to call if adding a new command to the session, the requred parameters of cmddisco and cmdexecute are the methods to enable that command to be executed""" + # This command takes a command object and the name of the command for registration + # We must: + # Add item into disco + # Add item into command list + if not self._handlers.has_key(jid): + self._handlers[jid]={} + self._browser.setDiscoHandler(self._DiscoHandler,node=NS_COMMANDS,jid=jid) + if self._handlers[jid].has_key(name): + raise NameError,'Command Exists' + else: + self._handlers[jid][name]={'disco':cmddisco,'execute':cmdexecute} + # Need to add disco stuff here + self._browser.setDiscoHandler(cmddisco,node=name,jid=jid) + + def delCommand(self,name,jid=''): + """Removed command from the session""" + # This command takes a command object and the name used for registration + # We must: + # Remove item from disco + # Remove item from command list + if not self._handlers.has_key(jid): + raise NameError,'Jid not found' + if not self._handlers[jid].has_key(name): + raise NameError, 'Command not found' + else: + #Do disco removal here + command = self.getCommand(name,jid)['disco'] + del self._handlers[jid][name] + self._browser.delDiscoHandler(command,node=name,jid=jid) + + def getCommand(self,name,jid=''): + """Returns the command tuple""" + # This gets the command object with name + # We must: + # Return item that matches this name + if not self._handlers.has_key(jid): + raise NameError,'Jid not found' + elif not self._handlers[jid].has_key(name): + raise NameError,'Command not found' + else: + return self._handlers[jid][name] + +class Command_Handler_Prototype(PlugIn): + """This is a prototype command handler, as each command uses a disco method + and execute method you can implement it any way you like, however this is + my first attempt at making a generic handler that you can hang process + stages on too. There is an example command below. + + The parameters are as follows: + name : the name of the command within the jabber environment + description : the natural language description + discofeatures : the features supported by the command + initial : the initial command in the from of {'execute':commandname} + + All stages set the 'actions' dictionary for each session to represent the possible options available. + """ + name = 'examplecommand' + count = 0 + description = 'an example command' + discofeatures = [NS_COMMANDS,NS_DATA] + # This is the command template + def __init__(self,jid=''): + """Set up the class""" + PlugIn.__init__(self) + DBG_LINE='command' + self.sessioncount = 0 + self.sessions = {} + # Disco information for command list pre-formatted as a tuple + self.discoinfo = {'ids':[{'category':'automation','type':'command-node','name':self.description}],'features': self.discofeatures} + self._jid = jid + + def plugin(self,owner): + """Plug command into the commands class""" + # The owner in this instance is the Command Processor + self._commands = owner + self._owner = owner._owner + self._commands.addCommand(self.name,self._DiscoHandler,self.Execute,jid=self._jid) + + def plugout(self): + """Remove command from the commands class""" + self._commands.delCommand(self.name,self._jid) + + def getSessionID(self): + """Returns an id for the command session""" + self.count = self.count+1 + return 'cmd-%s-%d'%(self.name,self.count) + + def Execute(self,conn,request): + """The method that handles all the commands, and routes them to the correct method for that stage.""" + # New request or old? + try: + session = request.getTagAttr('command','sessionid') + except: + session = None + try: + action = request.getTagAttr('command','action') + except: + action = None + if action == None: action = 'execute' + # Check session is in session list + if self.sessions.has_key(session): + if self.sessions[session]['jid']==request.getFrom(): + # Check action is vaild + if self.sessions[session]['actions'].has_key(action): + # Execute next action + self.sessions[session]['actions'][action](conn,request) + else: + # Stage not presented as an option + self._owner.send(Error(request,ERR_BAD_REQUEST)) + raise NodeProcessed + else: + # Jid and session don't match. Go away imposter + self._owner.send(Error(request,ERR_BAD_REQUEST)) + raise NodeProcessed + elif session != None: + # Not on this sessionid you won't. + self._owner.send(Error(request,ERR_BAD_REQUEST)) + raise NodeProcessed + else: + # New session + self.initial[action](conn,request) + + def _DiscoHandler(self,conn,request,type): + """The handler for discovery events""" + if type == 'list': + return (request.getTo(),self.name,self.description) + elif type == 'items': + return [] + elif type == 'info': + return self.discoinfo + +class TestCommand(Command_Handler_Prototype): + """ Example class. You should read source if you wish to understate how it works. + Generally, it presents a "master" that giudes user through to calculate something. + """ + name = 'testcommand' + description = 'a noddy example command' + def __init__(self,jid=''): + """ Init internal constants. """ + Command_Handler_Prototype.__init__(self,jid) + self.initial = {'execute':self.cmdFirstStage} + + def cmdFirstStage(self,conn,request): + """ Determine """ + # This is the only place this should be repeated as all other stages should have SessionIDs + try: + session = request.getTagAttr('command','sessionid') + except: + session = None + if session == None: + session = self.getSessionID() + self.sessions[session]={'jid':request.getFrom(),'actions':{'cancel':self.cmdCancel,'next':self.cmdSecondStage,'execute':self.cmdSecondStage},'data':{'type':None}} + # As this is the first stage we only send a form + reply = request.buildReply('result') + form = DataForm(title='Select type of operation',data=['Use the combobox to select the type of calculation you would like to do, then click Next',DataField(name='calctype',desc='Calculation Type',value=self.sessions[session]['data']['type'],options=[['circlediameter','Calculate the Diameter of a circle'],['circlearea','Calculate the area of a circle']],typ='list-single',required=1)]) + replypayload = [Node('actions',attrs={'execute':'next'},payload=[Node('next')]),form] + reply.addChild(name='command',namespace=NS_COMMANDS,attrs={'node':request.getTagAttr('command','node'),'sessionid':session,'status':'executing'},payload=replypayload) + self._owner.send(reply) + raise NodeProcessed + + def cmdSecondStage(self,conn,request): + form = DataForm(node = request.getTag(name='command').getTag(name='x',namespace=NS_DATA)) + self.sessions[request.getTagAttr('command','sessionid')]['data']['type']=form.getField('calctype').getValue() + self.sessions[request.getTagAttr('command','sessionid')]['actions']={'cancel':self.cmdCancel,None:self.cmdThirdStage,'previous':self.cmdFirstStage,'execute':self.cmdThirdStage,'next':self.cmdThirdStage} + # The form generation is split out to another method as it may be called by cmdThirdStage + self.cmdSecondStageReply(conn,request) + + def cmdSecondStageReply(self,conn,request): + reply = request.buildReply('result') + form = DataForm(title = 'Enter the radius', data=['Enter the radius of the circle (numbers only)',DataField(desc='Radius',name='radius',typ='text-single')]) + replypayload = [Node('actions',attrs={'execute':'complete'},payload=[Node('complete'),Node('prev')]),form] + reply.addChild(name='command',namespace=NS_COMMANDS,attrs={'node':request.getTagAttr('command','node'),'sessionid':request.getTagAttr('command','sessionid'),'status':'executing'},payload=replypayload) + self._owner.send(reply) + raise NodeProcessed + + def cmdThirdStage(self,conn,request): + form = DataForm(node = request.getTag(name='command').getTag(name='x',namespace=NS_DATA)) + try: + num = float(form.getField('radius').getValue()) + except: + self.cmdSecondStageReply(conn,request) + from math import pi + if self.sessions[request.getTagAttr('command','sessionid')]['data']['type'] == 'circlearea': + result = (num**2)*pi + else: + result = num*2*pi + reply = request.buildReply('result') + form = DataForm(typ='result',data=[DataField(desc='result',name='result',value=result)]) + reply.addChild(name='command',namespace=NS_COMMANDS,attrs={'node':request.getTagAttr('command','node'),'sessionid':request.getTagAttr('command','sessionid'),'status':'completed'},payload=[form]) + self._owner.send(reply) + raise NodeProcessed + + def cmdCancel(self,conn,request): + reply = request.buildReply('result') + reply.addChild(name='command',namespace=NS_COMMANDS,attrs={'node':request.getTagAttr('command','node'),'sessionid':request.getTagAttr('command','sessionid'),'status':'cancelled'}) + self._owner.send(reply) + del self.sessions[request.getTagAttr('command','sessionid')] diff --git a/src/xmpppy-0.5.0rc1/build/lib/xmpp/debug.py b/src/xmpppy-0.5.0rc1/build/lib/xmpp/debug.py new file mode 100644 index 00000000..34ade88f --- /dev/null +++ b/src/xmpppy-0.5.0rc1/build/lib/xmpp/debug.py @@ -0,0 +1,423 @@ +## debug.py +## +## Copyright (C) 2003 Jacob Lundqvist +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU Lesser General Public License as published +## by the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU Lesser General Public License for more details. + +_version_ = '1.4.0' + +"""\ + +Generic debug class + +Other modules can always define extra debug flags for local usage, as long as +they make sure they append them to debug_flags + +Also its always a good thing to prefix local flags with something, to reduce risk +of coliding flags. Nothing breaks if two flags would be identical, but it might +activate unintended debugging. + +flags can be numeric, but that makes analysing harder, on creation its +not obvious what is activated, and when flag_show is given, output isnt +really meaningfull. + +This Debug class can either be initialized and used on app level, or used independantly +by the individual classes. + +For samples of usage, see samples subdir in distro source, and selftest +in this code + +""" + + + +import sys +import traceback +import time +import os + +import types + +if os.environ.has_key('TERM'): + colors_enabled=True +else: + colors_enabled=False + +color_none = chr(27) + "[0m" +color_black = chr(27) + "[30m" +color_red = chr(27) + "[31m" +color_green = chr(27) + "[32m" +color_brown = chr(27) + "[33m" +color_blue = chr(27) + "[34m" +color_magenta = chr(27) + "[35m" +color_cyan = chr(27) + "[36m" +color_light_gray = chr(27) + "[37m" +color_dark_gray = chr(27) + "[30;1m" +color_bright_red = chr(27) + "[31;1m" +color_bright_green = chr(27) + "[32;1m" +color_yellow = chr(27) + "[33;1m" +color_bright_blue = chr(27) + "[34;1m" +color_purple = chr(27) + "[35;1m" +color_bright_cyan = chr(27) + "[36;1m" +color_white = chr(27) + "[37;1m" + + +""" +Define your flags in yor modules like this: + +from debug import * + +DBG_INIT = 'init' ; debug_flags.append( DBG_INIT ) +DBG_CONNECTION = 'connection' ; debug_flags.append( DBG_CONNECTION ) + + The reason for having a double statement wis so we can validate params + and catch all undefined debug flags + + This gives us control over all used flags, and makes it easier to allow + global debugging in your code, just do something like + + foo = Debug( debug_flags ) + + group flags, that is a flag in it self containing multiple flags should be + defined without the debug_flags.append() sequence, since the parts are already + in the list, also they must of course be defined after the flags they depend on ;) + example: + +DBG_MULTI = [ DBG_INIT, DBG_CONNECTION ] + + + + NoDebug + ------- + To speed code up, typically for product releases or such + use this class instead if you globaly want to disable debugging +""" + + +class NoDebug: + def __init__( self, *args, **kwargs ): + self.debug_flags = [] + def show( self, *args, **kwargs): + pass + def Show( self, *args, **kwargs): + pass + def is_active( self, flag ): + pass + colors={} + def active_set( self, active_flags = None ): + return 0 + + +LINE_FEED = '\n' + + +class Debug: + def __init__( self, + # + # active_flags are those that will trigger output + # + active_flags = None, + # + # Log file should be file object or file namne + # + log_file = sys.stderr, + # + # prefix and sufix can either be set globaly or per call. + # personally I use this to color code debug statements + # with prefix = chr(27) + '[34m' + # sufix = chr(27) + '[37;1m\n' + # + prefix = 'DEBUG: ', + sufix = '\n', + # + # If you want unix style timestamps, + # 0 disables timestamps + # 1 before prefix, good when prefix is a string + # 2 after prefix, good when prefix is a color + # + time_stamp = 0, + # + # flag_show should normaly be of, but can be turned on to get a + # good view of what flags are actually used for calls, + # if it is not None, it should be a string + # flags for current call will be displayed + # with flag_show as separator + # recomended values vould be '-' or ':', but any string goes + # + flag_show = None, + # + # If you dont want to validate flags on each call to + # show(), set this to 0 + # + validate_flags = 1, + # + # If you dont want the welcome message, set to 0 + # default is to show welcome if any flags are active + welcome = -1 + ): + + self.debug_flags = [] + if welcome == -1: + if active_flags and len(active_flags): + welcome = 1 + else: + welcome = 0 + + self._remove_dupe_flags() + if log_file: + if type( log_file ) is type(''): + try: + self._fh = open(log_file,'w') + except: + print 'ERROR: can open %s for writing' + sys.exit(0) + else: ## assume its a stream type object + self._fh = log_file + else: + self._fh = sys.stdout + + if time_stamp not in (0,1,2): + msg2 = '%s' % time_stamp + raise 'Invalid time_stamp param', msg2 + self.prefix = prefix + self.sufix = sufix + self.time_stamp = time_stamp + self.flag_show = None # must be initialised after possible welcome + self.validate_flags = validate_flags + + self.active_set( active_flags ) + if welcome: + self.show('') + caller = sys._getframe(1) # used to get name of caller + try: + mod_name= ":%s" % caller.f_locals['__name__'] + except: + mod_name = "" + self.show('Debug created for %s%s' % (caller.f_code.co_filename, + mod_name )) + self.show(' flags defined: %s' % ','.join( self.active )) + + if type(flag_show) in (type(''), type(None)): + self.flag_show = flag_show + else: + msg2 = '%s' % type(flag_show ) + raise 'Invalid type for flag_show!', msg2 + + + + + + def show( self, msg, flag = None, prefix = None, sufix = None, + lf = 0 ): + """ + flag can be of folowing types: + None - this msg will always be shown if any debugging is on + flag - will be shown if flag is active + (flag1,flag2,,,) - will be shown if any of the given flags + are active + + if prefix / sufix are not given, default ones from init will be used + + lf = -1 means strip linefeed if pressent + lf = 1 means add linefeed if not pressent + """ + + if self.validate_flags: + self._validate_flag( flag ) + + if not self.is_active(flag): + return + if prefix: + pre = prefix + else: + pre = self.prefix + if sufix: + suf = sufix + else: + suf = self.sufix + + if self.time_stamp == 2: + output = '%s%s ' % ( pre, + time.strftime('%b %d %H:%M:%S', + time.localtime(time.time() )), + ) + elif self.time_stamp == 1: + output = '%s %s' % ( time.strftime('%b %d %H:%M:%S', + time.localtime(time.time() )), + pre, + ) + else: + output = pre + + if self.flag_show: + if flag: + output = '%s%s%s' % ( output, flag, self.flag_show ) + else: + # this call uses the global default, + # dont print "None", just show the separator + output = '%s %s' % ( output, self.flag_show ) + + output = '%s%s%s' % ( output, msg, suf ) + if lf: + # strip/add lf if needed + last_char = output[-1] + if lf == 1 and last_char != LINE_FEED: + output = output + LINE_FEED + elif lf == -1 and last_char == LINE_FEED: + output = output[:-1] + try: + self._fh.write( output ) + except: + # unicode strikes again ;) + s=u'' + for i in range(len(output)): + if ord(output[i]) < 128: + c = output[i] + else: + c = '?' + s=s+c + self._fh.write( '%s%s%s' % ( pre, s, suf )) + self._fh.flush() + + + def is_active( self, flag ): + 'If given flag(s) should generate output.' + + # try to abort early to quicken code + if not self.active: + return 0 + if not flag or flag in self.active: + return 1 + else: + # check for multi flag type: + if type( flag ) in ( type(()), type([]) ): + for s in flag: + if s in self.active: + return 1 + return 0 + + + def active_set( self, active_flags = None ): + "returns 1 if any flags where actually set, otherwise 0." + r = 0 + ok_flags = [] + if not active_flags: + #no debuging at all + self.active = [] + elif type( active_flags ) in ( types.TupleType, types.ListType ): + flags = self._as_one_list( active_flags ) + for t in flags: + if t not in self.debug_flags: + sys.stderr.write('Invalid debugflag given: %s\n' % t ) + ok_flags.append( t ) + + self.active = ok_flags + r = 1 + else: + # assume comma string + try: + flags = active_flags.split(',') + except: + self.show( '***' ) + self.show( '*** Invalid debug param given: %s' % active_flags ) + self.show( '*** please correct your param!' ) + self.show( '*** due to this, full debuging is enabled' ) + self.active = self.debug_flags + + for f in flags: + s = f.strip() + ok_flags.append( s ) + self.active = ok_flags + + self._remove_dupe_flags() + return r + + def active_get( self ): + "returns currently active flags." + return self.active + + + def _as_one_list( self, items ): + """ init param might contain nested lists, typically from group flags. + + This code organises lst and remves dupes + """ + if type( items ) <> type( [] ) and type( items ) <> type( () ): + return [ items ] + r = [] + for l in items: + if type( l ) == type([]): + lst2 = self._as_one_list( l ) + for l2 in lst2: + self._append_unique_str(r, l2 ) + elif l == None: + continue + else: + self._append_unique_str(r, l ) + return r + + + def _append_unique_str( self, lst, item ): + """filter out any dupes.""" + if type(item) <> type(''): + msg2 = '%s' % item + raise 'Invalid item type (should be string)',msg2 + if item not in lst: + lst.append( item ) + return lst + + + def _validate_flag( self, flags ): + 'verify that flag is defined.' + if flags: + for f in self._as_one_list( flags ): + if not f in self.debug_flags: + msg2 = '%s' % f + raise 'Invalid debugflag given', msg2 + + def _remove_dupe_flags( self ): + """ + if multiple instances of Debug is used in same app, + some flags might be created multiple time, filter out dupes + """ + unique_flags = [] + for f in self.debug_flags: + if f not in unique_flags: + unique_flags.append(f) + self.debug_flags = unique_flags + + colors={} + def Show(self, flag, msg, prefix=''): + msg=msg.replace('\r','\\r').replace('\n','\\n').replace('><','>\n <') + if not colors_enabled: pass + elif self.colors.has_key(prefix): msg=self.colors[prefix]+msg+color_none + else: msg=color_none+msg + if not colors_enabled: prefixcolor='' + elif self.colors.has_key(flag): prefixcolor=self.colors[flag] + else: prefixcolor=color_none + + if prefix=='error': + _exception = sys.exc_info() + if _exception[0]: + msg=msg+'\n'+''.join(traceback.format_exception(_exception[0], _exception[1], _exception[2])).rstrip() + + prefix= self.prefix+prefixcolor+(flag+' '*12)[:12]+' '+(prefix+' '*6)[:6] + self.show(msg, flag, prefix) + + def is_active( self, flag ): + if not self.active: return 0 + if not flag or flag in self.active and DBG_ALWAYS not in self.active or flag not in self.active and DBG_ALWAYS in self.active : return 1 + return 0 + +DBG_ALWAYS='always' + +##Uncomment this to effectively disable all debugging and all debugging overhead. +#Debug=NoDebug diff --git a/src/xmpppy-0.5.0rc1/build/lib/xmpp/dispatcher.py b/src/xmpppy-0.5.0rc1/build/lib/xmpp/dispatcher.py new file mode 100644 index 00000000..cc94ee04 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/build/lib/xmpp/dispatcher.py @@ -0,0 +1,373 @@ +## transports.py +## +## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: dispatcher.py,v 1.42 2007/05/18 23:18:36 normanr Exp $ + +""" +Main xmpppy mechanism. Provides library with methods to assign different handlers +to different XMPP stanzas. +Contains one tunable attribute: DefaultTimeout (25 seconds by default). It defines time that +Dispatcher.SendAndWaitForResponce method will wait for reply stanza before giving up. +""" + +import simplexml,time,sys +from protocol import * +from client import PlugIn + +DefaultTimeout=25 +ID=0 + +class Dispatcher(PlugIn): + """ Ancestor of PlugIn class. Handles XMPP stream, i.e. aware of stream headers. + Can be plugged out/in to restart these headers (used for SASL f.e.). """ + def __init__(self): + PlugIn.__init__(self) + DBG_LINE='dispatcher' + self.handlers={} + self._expected={} + self._defaultHandler=None + self._pendingExceptions=[] + self._eventHandler=None + self._cycleHandlers=[] + self._exported_methods=[self.Process,self.RegisterHandler,self.RegisterDefaultHandler,\ + self.RegisterEventHandler,self.UnregisterCycleHandler,self.RegisterCycleHandler,\ + self.RegisterHandlerOnce,self.UnregisterHandler,self.RegisterProtocol,\ + self.WaitForResponse,self.SendAndWaitForResponse,self.send,self.disconnect,\ + self.SendAndCallForResponse, ] + + def dumpHandlers(self): + """ Return set of user-registered callbacks in it's internal format. + Used within the library to carry user handlers set over Dispatcher replugins. """ + return self.handlers + def restoreHandlers(self,handlers): + """ Restores user-registered callbacks structure from dump previously obtained via dumpHandlers. + Used within the library to carry user handlers set over Dispatcher replugins. """ + self.handlers=handlers + + def _init(self): + """ Registers default namespaces/protocols/handlers. Used internally. """ + self.RegisterNamespace('unknown') + self.RegisterNamespace(NS_STREAMS) + self.RegisterNamespace(self._owner.defaultNamespace) + self.RegisterProtocol('iq',Iq) + self.RegisterProtocol('presence',Presence) + self.RegisterProtocol('message',Message) + self.RegisterDefaultHandler(self.returnStanzaHandler) + self.RegisterHandler('error',self.streamErrorHandler,xmlns=NS_STREAMS) + + def plugin(self, owner): + """ Plug the Dispatcher instance into Client class instance and send initial stream header. Used internally.""" + self._init() + for method in self._old_owners_methods: + if method.__name__=='send': self._owner_send=method; break + self._owner.lastErrNode=None + self._owner.lastErr=None + self._owner.lastErrCode=None + self.StreamInit() + + def plugout(self): + """ Prepares instance to be destructed. """ + self.Stream.dispatch=None + self.Stream.DEBUG=None + self.Stream.features=None + self.Stream.destroy() + + def StreamInit(self): + """ Send an initial stream header. """ + self.Stream=simplexml.NodeBuilder() + self.Stream._dispatch_depth=2 + self.Stream.dispatch=self.dispatch + self.Stream.stream_header_received=self._check_stream_start + self._owner.debug_flags.append(simplexml.DBG_NODEBUILDER) + self.Stream.DEBUG=self._owner.DEBUG + self.Stream.features=None + self._metastream=Node('stream:stream') + self._metastream.setNamespace(self._owner.Namespace) + self._metastream.setAttr('version','1.0') + self._metastream.setAttr('xmlns:stream',NS_STREAMS) + self._metastream.setAttr('to',self._owner.Server) + self._owner.send("<?xml version='1.0'?>%s>"%str(self._metastream)[:-2]) + + def _check_stream_start(self,ns,tag,attrs): + if ns<>NS_STREAMS or tag<>'stream': + raise ValueError('Incorrect stream start: (%s,%s). Terminating.'%(tag,ns)) + + def Process(self, timeout=0): + """ Check incoming stream for data waiting. If "timeout" is positive - block for as max. this time. + Returns: + 1) length of processed data if some data were processed; + 2) '0' string if no data were processed but link is alive; + 3) 0 (zero) if underlying connection is closed. + Take note that in case of disconnection detect during Process() call + disconnect handlers are called automatically. + """ + for handler in self._cycleHandlers: handler(self) + if len(self._pendingExceptions) > 0: + _pendingException = self._pendingExceptions.pop() + raise _pendingException[0], _pendingException[1], _pendingException[2] + if self._owner.Connection.pending_data(timeout): + try: data=self._owner.Connection.receive() + except IOError: return + self.Stream.Parse(data) + if len(self._pendingExceptions) > 0: + _pendingException = self._pendingExceptions.pop() + raise _pendingException[0], _pendingException[1], _pendingException[2] + if data: return len(data) + return '0' # It means that nothing is received but link is alive. + + def RegisterNamespace(self,xmlns,order='info'): + """ Creates internal structures for newly registered namespace. + You can register handlers for this namespace afterwards. By default one namespace + already registered (jabber:client or jabber:component:accept depending on context. """ + self.DEBUG('Registering namespace "%s"'%xmlns,order) + self.handlers[xmlns]={} + self.RegisterProtocol('unknown',Protocol,xmlns=xmlns) + self.RegisterProtocol('default',Protocol,xmlns=xmlns) + + def RegisterProtocol(self,tag_name,Proto,xmlns=None,order='info'): + """ Used to declare some top-level stanza name to dispatcher. + Needed to start registering handlers for such stanzas. + Iq, message and presence protocols are registered by default. """ + if not xmlns: xmlns=self._owner.defaultNamespace + self.DEBUG('Registering protocol "%s" as %s(%s)'%(tag_name,Proto,xmlns), order) + self.handlers[xmlns][tag_name]={type:Proto, 'default':[]} + + def RegisterNamespaceHandler(self,xmlns,handler,typ='',ns='', makefirst=0, system=0): + """ Register handler for processing all stanzas for specified namespace. """ + self.RegisterHandler('default', handler, typ, ns, xmlns, makefirst, system) + + def RegisterHandler(self,name,handler,typ='',ns='',xmlns=None, makefirst=0, system=0): + """Register user callback as stanzas handler of declared type. Callback must take + (if chained, see later) arguments: dispatcher instance (for replying), incomed + return of previous handlers. + The callback must raise xmpp.NodeProcessed just before return if it want preven + callbacks to be called with the same stanza as argument _and_, more importantly + library from returning stanza to sender with error set (to be enabled in 0.2 ve + Arguments: + "name" - name of stanza. F.e. "iq". + "handler" - user callback. + "typ" - value of stanza's "type" attribute. If not specified any value match + "ns" - namespace of child that stanza must contain. + "chained" - chain together output of several handlers. + "makefirst" - insert handler in the beginning of handlers list instead of + adding it to the end. Note that more common handlers (i.e. w/o "typ" and " + will be called first nevertheless. + "system" - call handler even if NodeProcessed Exception were raised already. + """ + if not xmlns: xmlns=self._owner.defaultNamespace + self.DEBUG('Registering handler %s for "%s" type->%s ns->%s(%s)'%(handler,name,typ,ns,xmlns), 'info') + if not typ and not ns: typ='default' + if not self.handlers.has_key(xmlns): self.RegisterNamespace(xmlns,'warn') + if not self.handlers[xmlns].has_key(name): self.RegisterProtocol(name,Protocol,xmlns,'warn') + if not self.handlers[xmlns][name].has_key(typ+ns): self.handlers[xmlns][name][typ+ns]=[] + if makefirst: self.handlers[xmlns][name][typ+ns].insert(0,{'func':handler,'system':system}) + else: self.handlers[xmlns][name][typ+ns].append({'func':handler,'system':system}) + + def RegisterHandlerOnce(self,name,handler,typ='',ns='',xmlns=None,makefirst=0, system=0): + """ Unregister handler after first call (not implemented yet). """ + if not xmlns: xmlns=self._owner.defaultNamespace + self.RegisterHandler(name, handler, typ, ns, xmlns, makefirst, system) + + def UnregisterHandler(self,name,handler,typ='',ns='',xmlns=None): + """ Unregister handler. "typ" and "ns" must be specified exactly the same as with registering.""" + if not xmlns: xmlns=self._owner.defaultNamespace + if not self.handlers.has_key(xmlns): return + if not typ and not ns: typ='default' + for pack in self.handlers[xmlns][name][typ+ns]: + if handler==pack['func']: break + else: pack=None + try: self.handlers[xmlns][name][typ+ns].remove(pack) + except ValueError: pass + + def RegisterDefaultHandler(self,handler): + """ Specify the handler that will be used if no NodeProcessed exception were raised. + This is returnStanzaHandler by default. """ + self._defaultHandler=handler + + def RegisterEventHandler(self,handler): + """ Register handler that will process events. F.e. "FILERECEIVED" event. """ + self._eventHandler=handler + + def returnStanzaHandler(self,conn,stanza): + """ Return stanza back to the sender with <feature-not-implemennted/> error set. """ + if stanza.getType() in ['get','set']: + conn.send(Error(stanza,ERR_FEATURE_NOT_IMPLEMENTED)) + + def streamErrorHandler(self,conn,error): + name,text='error',error.getData() + for tag in error.getChildren(): + if tag.getNamespace()==NS_XMPP_STREAMS: + if tag.getName()=='text': text=tag.getData() + else: name=tag.getName() + if name in stream_exceptions.keys(): exc=stream_exceptions[name] + else: exc=StreamError + raise exc((name,text)) + + def RegisterCycleHandler(self,handler): + """ Register handler that will be called on every Dispatcher.Process() call. """ + if handler not in self._cycleHandlers: self._cycleHandlers.append(handler) + + def UnregisterCycleHandler(self,handler): + """ Unregister handler that will is called on every Dispatcher.Process() call.""" + if handler in self._cycleHandlers: self._cycleHandlers.remove(handler) + + def Event(self,realm,event,data): + """ Raise some event. Takes three arguments: + 1) "realm" - scope of event. Usually a namespace. + 2) "event" - the event itself. F.e. "SUCESSFULL SEND". + 3) data that comes along with event. Depends on event.""" + if self._eventHandler: self._eventHandler(realm,event,data) + + def dispatch(self,stanza,session=None,direct=0): + """ Main procedure that performs XMPP stanza recognition and calling apppropriate handlers for it. + Called internally. """ + if not session: session=self + session.Stream._mini_dom=None + name=stanza.getName() + + if not direct and self._owner._route: + if name == 'route': + if stanza.getAttr('error') == None: + if len(stanza.getChildren()) == 1: + stanza = stanza.getChildren()[0] + name=stanza.getName() + else: + for each in stanza.getChildren(): + self.dispatch(each,session,direct=1) + return + elif name == 'presence': + return + elif name in ('features','bind'): + pass + else: + raise UnsupportedStanzaType(name) + + if name=='features': session.Stream.features=stanza + + xmlns=stanza.getNamespace() + if not self.handlers.has_key(xmlns): + self.DEBUG("Unknown namespace: " + xmlns,'warn') + xmlns='unknown' + if not self.handlers[xmlns].has_key(name): + self.DEBUG("Unknown stanza: " + name,'warn') + name='unknown' + else: + self.DEBUG("Got %s/%s stanza"%(xmlns,name), 'ok') + + if stanza.__class__.__name__=='Node': stanza=self.handlers[xmlns][name][type](node=stanza) + + typ=stanza.getType() + if not typ: typ='' + stanza.props=stanza.getProperties() + ID=stanza.getID() + + session.DEBUG("Dispatching %s stanza with type->%s props->%s id->%s"%(name,typ,stanza.props,ID),'ok') + + list=['default'] # we will use all handlers: + if self.handlers[xmlns][name].has_key(typ): list.append(typ) # from very common... + for prop in stanza.props: + if self.handlers[xmlns][name].has_key(prop): list.append(prop) + if typ and self.handlers[xmlns][name].has_key(typ+prop): list.append(typ+prop) # ...to very particular + + chain=self.handlers[xmlns]['default']['default'] + for key in list: + if key: chain = chain + self.handlers[xmlns][name][key] + + output='' + if session._expected.has_key(ID): + user=0 + if type(session._expected[ID])==type(()): + cb,args=session._expected[ID] + session.DEBUG("Expected stanza arrived. Callback %s(%s) found!"%(cb,args),'ok') + try: cb(session,stanza,**args) + except Exception, typ: + if typ.__class__.__name__<>'NodeProcessed': raise + else: + session.DEBUG("Expected stanza arrived!",'ok') + session._expected[ID]=stanza + else: user=1 + for handler in chain: + if user or handler['system']: + try: + handler['func'](session,stanza) + except Exception, typ: + if typ.__class__.__name__<>'NodeProcessed': + self._pendingExceptions.insert(0, sys.exc_info()) + return + user=0 + if user and self._defaultHandler: self._defaultHandler(session,stanza) + + def WaitForResponse(self, ID, timeout=DefaultTimeout): + """ Block and wait until stanza with specific "id" attribute will come. + If no such stanza is arrived within timeout, return None. + If operation failed for some reason then owner's attributes + lastErrNode, lastErr and lastErrCode are set accordingly. """ + self._expected[ID]=None + has_timed_out=0 + abort_time=time.time() + timeout + self.DEBUG("Waiting for ID:%s with timeout %s..." % (ID,timeout),'wait') + while not self._expected[ID]: + if not self.Process(0.04): + self._owner.lastErr="Disconnect" + return None + if time.time() > abort_time: + self._owner.lastErr="Timeout" + return None + response=self._expected[ID] + del self._expected[ID] + if response.getErrorCode(): + self._owner.lastErrNode=response + self._owner.lastErr=response.getError() + self._owner.lastErrCode=response.getErrorCode() + return response + + def SendAndWaitForResponse(self, stanza, timeout=DefaultTimeout): + """ Put stanza on the wire and wait for recipient's response to it. """ + return self.WaitForResponse(self.send(stanza),timeout) + + def SendAndCallForResponse(self, stanza, func, args={}): + """ Put stanza on the wire and call back when recipient replies. + Additional callback arguments can be specified in args. """ + self._expected[self.send(stanza)]=(func,args) + + def send(self,stanza): + """ Serialise stanza and put it on the wire. Assign an unique ID to it before send. + Returns assigned ID.""" + if type(stanza) in [type(''), type(u'')]: return self._owner_send(stanza) + if not isinstance(stanza,Protocol): _ID=None + elif not stanza.getID(): + global ID + ID+=1 + _ID=`ID` + stanza.setID(_ID) + else: _ID=stanza.getID() + if self._owner._registered_name and not stanza.getAttr('from'): stanza.setAttr('from',self._owner._registered_name) + if self._owner._route and stanza.getName()!='bind': + to=self._owner.Server + if stanza.getTo() and stanza.getTo().getDomain(): + to=stanza.getTo().getDomain() + frm=stanza.getFrom() + if frm.getDomain(): + frm=frm.getDomain() + route=Protocol('route',to=to,frm=frm,payload=[stanza]) + stanza=route + stanza.setNamespace(self._owner.Namespace) + stanza.setParent(self._metastream) + self._owner_send(stanza) + return _ID + + def disconnect(self): + """ Send a stream terminator and and handle all incoming stanzas before stream closure. """ + self._owner_send('</stream:stream>') + while self.Process(1): pass diff --git a/src/xmpppy-0.5.0rc1/build/lib/xmpp/features.py b/src/xmpppy-0.5.0rc1/build/lib/xmpp/features.py new file mode 100644 index 00000000..c7993c29 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/build/lib/xmpp/features.py @@ -0,0 +1,182 @@ +## features.py +## +## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: features.py,v 1.25 2009/04/07 07:11:48 snakeru Exp $ + +""" +This module contains variable stuff that is not worth splitting into separate modules. +Here is: + DISCO client and agents-to-DISCO and browse-to-DISCO emulators. + IBR and password manager. + jabber:iq:privacy methods +All these methods takes 'disp' first argument that should be already connected +(and in most cases already authorised) dispatcher instance. +""" + +from protocol import * + +REGISTER_DATA_RECEIVED='REGISTER DATA RECEIVED' + +### DISCO ### http://jabber.org/protocol/disco ### JEP-0030 #################### +### Browse ### jabber:iq:browse ### JEP-0030 ################################### +### Agents ### jabber:iq:agents ### JEP-0030 ################################### +def _discover(disp,ns,jid,node=None,fb2b=0,fb2a=1): + """ Try to obtain info from the remote object. + If remote object doesn't support disco fall back to browse (if fb2b is true) + and if it doesnt support browse (or fb2b is not true) fall back to agents protocol + (if gb2a is true). Returns obtained info. Used internally. """ + iq=Iq(to=jid,typ='get',queryNS=ns) + if node: iq.setQuerynode(node) + rep=disp.SendAndWaitForResponse(iq) + if fb2b and not isResultNode(rep): rep=disp.SendAndWaitForResponse(Iq(to=jid,typ='get',queryNS=NS_BROWSE)) # Fallback to browse + if fb2a and not isResultNode(rep): rep=disp.SendAndWaitForResponse(Iq(to=jid,typ='get',queryNS=NS_AGENTS)) # Fallback to agents + if isResultNode(rep): return [n for n in rep.getQueryPayload() if isinstance(n, Node)] + return [] + +def discoverItems(disp,jid,node=None): + """ Query remote object about any items that it contains. Return items list. """ + """ According to JEP-0030: + query MAY have node attribute + item: MUST HAVE jid attribute and MAY HAVE name, node, action attributes. + action attribute of item can be either of remove or update value.""" + ret=[] + for i in _discover(disp,NS_DISCO_ITEMS,jid,node): + if i.getName()=='agent' and i.getTag('name'): i.setAttr('name',i.getTagData('name')) + ret.append(i.attrs) + return ret + +def discoverInfo(disp,jid,node=None): + """ Query remote object about info that it publishes. Returns identities and features lists.""" + """ According to JEP-0030: + query MAY have node attribute + identity: MUST HAVE category and name attributes and MAY HAVE type attribute. + feature: MUST HAVE var attribute""" + identities , features = [] , [] + for i in _discover(disp,NS_DISCO_INFO,jid,node): + if i.getName()=='identity': identities.append(i.attrs) + elif i.getName()=='feature': features.append(i.getAttr('var')) + elif i.getName()=='agent': + if i.getTag('name'): i.setAttr('name',i.getTagData('name')) + if i.getTag('description'): i.setAttr('name',i.getTagData('description')) + identities.append(i.attrs) + if i.getTag('groupchat'): features.append(NS_GROUPCHAT) + if i.getTag('register'): features.append(NS_REGISTER) + if i.getTag('search'): features.append(NS_SEARCH) + return identities , features + +### Registration ### jabber:iq:register ### JEP-0077 ########################### +def getRegInfo(disp,host,info={},sync=True): + """ Gets registration form from remote host. + You can pre-fill the info dictionary. + F.e. if you are requesting info on registering user joey than specify + info as {'username':'joey'}. See JEP-0077 for details. + 'disp' must be connected dispatcher instance.""" + iq=Iq('get',NS_REGISTER,to=host) + for i in info.keys(): iq.setTagData(i,info[i]) + if sync: + resp=disp.SendAndWaitForResponse(iq) + _ReceivedRegInfo(disp.Dispatcher,resp, host) + return resp + else: disp.SendAndCallForResponse(iq,_ReceivedRegInfo, {'agent': host}) + +def _ReceivedRegInfo(con, resp, agent): + iq=Iq('get',NS_REGISTER,to=agent) + if not isResultNode(resp): return + df=resp.getTag('query',namespace=NS_REGISTER).getTag('x',namespace=NS_DATA) + if df: + con.Event(NS_REGISTER,REGISTER_DATA_RECEIVED,(agent, DataForm(node=df))) + return + df=DataForm(typ='form') + for i in resp.getQueryPayload(): + if type(i)<>type(iq): pass + elif i.getName()=='instructions': df.addInstructions(i.getData()) + else: df.setField(i.getName()).setValue(i.getData()) + con.Event(NS_REGISTER,REGISTER_DATA_RECEIVED,(agent, df)) + +def register(disp,host,info): + """ Perform registration on remote server with provided info. + disp must be connected dispatcher instance. + Returns true or false depending on registration result. + If registration fails you can get additional info from the dispatcher's owner + attributes lastErrNode, lastErr and lastErrCode. + """ + iq=Iq('set',NS_REGISTER,to=host) + if type(info)<>type({}): info=info.asDict() + for i in info.keys(): iq.setTag('query').setTagData(i,info[i]) + resp=disp.SendAndWaitForResponse(iq) + if isResultNode(resp): return 1 + +def unregister(disp,host): + """ Unregisters with host (permanently removes account). + disp must be connected and authorized dispatcher instance. + Returns true on success.""" + resp=disp.SendAndWaitForResponse(Iq('set',NS_REGISTER,to=host,payload=[Node('remove')])) + if isResultNode(resp): return 1 + +def changePasswordTo(disp,newpassword,host=None): + """ Changes password on specified or current (if not specified) server. + disp must be connected and authorized dispatcher instance. + Returns true on success.""" + if not host: host=disp._owner.Server + resp=disp.SendAndWaitForResponse(Iq('set',NS_REGISTER,to=host,payload=[Node('username',payload=[disp._owner.Server]),Node('password',payload=[newpassword])])) + if isResultNode(resp): return 1 + +### Privacy ### jabber:iq:privacy ### draft-ietf-xmpp-im-19 #################### +#type=[jid|group|subscription] +#action=[allow|deny] + +def getPrivacyLists(disp): + """ Requests privacy lists from connected server. + Returns dictionary of existing lists on success.""" + try: + dict={'lists':[]} + resp=disp.SendAndWaitForResponse(Iq('get',NS_PRIVACY)) + if not isResultNode(resp): return + for list in resp.getQueryPayload(): + if list.getName()=='list': dict['lists'].append(list.getAttr('name')) + else: dict[list.getName()]=list.getAttr('name') + return dict + except: pass + +def getPrivacyList(disp,listname): + """ Requests specific privacy list listname. Returns list of XML nodes (rules) + taken from the server responce.""" + try: + resp=disp.SendAndWaitForResponse(Iq('get',NS_PRIVACY,payload=[Node('list',{'name':listname})])) + if isResultNode(resp): return resp.getQueryPayload()[0] + except: pass + +def setActivePrivacyList(disp,listname=None,typ='active'): + """ Switches privacy list 'listname' to specified type. + By default the type is 'active'. Returns true on success.""" + if listname: attrs={'name':listname} + else: attrs={} + resp=disp.SendAndWaitForResponse(Iq('set',NS_PRIVACY,payload=[Node(typ,attrs)])) + if isResultNode(resp): return 1 + +def setDefaultPrivacyList(disp,listname=None): + """ Sets the default privacy list as 'listname'. Returns true on success.""" + return setActivePrivacyList(disp,listname,'default') + +def setPrivacyList(disp,list): + """ Set the ruleset. 'list' should be the simpleXML node formatted + according to RFC 3921 (XMPP-IM) (I.e. Node('list',{'name':listname},payload=[...]) ) + Returns true on success.""" + resp=disp.SendAndWaitForResponse(Iq('set',NS_PRIVACY,payload=[list])) + if isResultNode(resp): return 1 + +def delPrivacyList(disp,listname): + """ Deletes privacy list 'listname'. Returns true on success.""" + resp=disp.SendAndWaitForResponse(Iq('set',NS_PRIVACY,payload=[Node('list',{'name':listname})])) + if isResultNode(resp): return 1 diff --git a/src/xmpppy-0.5.0rc1/build/lib/xmpp/filetransfer.py b/src/xmpppy-0.5.0rc1/build/lib/xmpp/filetransfer.py new file mode 100644 index 00000000..87ddc219 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/build/lib/xmpp/filetransfer.py @@ -0,0 +1,199 @@ +## filetransfer.py +## +## Copyright (C) 2004 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: filetransfer.py,v 1.6 2004/12/25 20:06:59 snakeru Exp $ + +""" +This module contains IBB class that is the simple implementation of JEP-0047. +Note that this is just a transport for data. You have to negotiate data transfer before +(via StreamInitiation most probably). Unfortunately SI is not implemented yet. +""" + +from protocol import * +from dispatcher import PlugIn +import base64 + +class IBB(PlugIn): + """ IBB used to transfer small-sized data chunk over estabilished xmpp connection. + Data is split into small blocks (by default 3000 bytes each), encoded as base 64 + and sent to another entity that compiles these blocks back into the data chunk. + This is very inefficiend but should work under any circumstances. Note that + using IBB normally should be the last resort. + """ + def __init__(self): + """ Initialise internal variables. """ + PlugIn.__init__(self) + self.DBG_LINE='ibb' + self._exported_methods=[self.OpenStream] + self._streams={} + self._ampnode=Node(NS_AMP+' amp',payload=[Node('rule',{'condition':'deliver-at','value':'stored','action':'error'}),Node('rule',{'condition':'match-resource','value':'exact','action':'error'})]) + + def plugin(self,owner): + """ Register handlers for receiving incoming datastreams. Used internally. """ + self._owner.RegisterHandlerOnce('iq',self.StreamOpenReplyHandler) # Move to StreamOpen and specify stanza id + self._owner.RegisterHandler('iq',self.IqHandler,ns=NS_IBB) + self._owner.RegisterHandler('message',self.ReceiveHandler,ns=NS_IBB) + + def IqHandler(self,conn,stanza): + """ Handles streams state change. Used internally. """ + typ=stanza.getType() + self.DEBUG('IqHandler called typ->%s'%typ,'info') + if typ=='set' and stanza.getTag('open',namespace=NS_IBB): self.StreamOpenHandler(conn,stanza) + elif typ=='set' and stanza.getTag('close',namespace=NS_IBB): self.StreamCloseHandler(conn,stanza) + elif typ=='result': self.StreamCommitHandler(conn,stanza) + elif typ=='error': self.StreamOpenReplyHandler(conn,stanza) + else: conn.send(Error(stanza,ERR_BAD_REQUEST)) + raise NodeProcessed + + def StreamOpenHandler(self,conn,stanza): + """ Handles opening of new incoming stream. Used internally. """ + """ +<iq type='set' + from='romeo@montague.net/orchard' + to='juliet@capulet.com/balcony' + id='inband_1'> + <open sid='mySID' + block-size='4096' + xmlns='http://jabber.org/protocol/ibb'/> +</iq> +""" + err=None + sid,blocksize=stanza.getTagAttr('open','sid'),stanza.getTagAttr('open','block-size') + self.DEBUG('StreamOpenHandler called sid->%s blocksize->%s'%(sid,blocksize),'info') + try: blocksize=int(blocksize) + except: err=ERR_BAD_REQUEST + if not sid or not blocksize: err=ERR_BAD_REQUEST + elif sid in self._streams.keys(): err=ERR_UNEXPECTED_REQUEST + if err: rep=Error(stanza,err) + else: + self.DEBUG("Opening stream: id %s, block-size %s"%(sid,blocksize),'info') + rep=Protocol('iq',stanza.getFrom(),'result',stanza.getTo(),{'id':stanza.getID()}) + self._streams[sid]={'direction':'<'+str(stanza.getFrom()),'block-size':blocksize,'fp':open('/tmp/xmpp_file_'+sid,'w'),'seq':0,'syn_id':stanza.getID()} + conn.send(rep) + + def OpenStream(self,sid,to,fp,blocksize=3000): + """ Start new stream. You should provide stream id 'sid', the endpoind jid 'to', + the file object containing info for send 'fp'. Also the desired blocksize can be specified. + Take into account that recommended stanza size is 4k and IBB uses base64 encoding + that increases size of data by 1/3.""" + if sid in self._streams.keys(): return + if not JID(to).getResource(): return + self._streams[sid]={'direction':'|>'+to,'block-size':blocksize,'fp':fp,'seq':0} + self._owner.RegisterCycleHandler(self.SendHandler) + syn=Protocol('iq',to,'set',payload=[Node(NS_IBB+' open',{'sid':sid,'block-size':blocksize})]) + self._owner.send(syn) + self._streams[sid]['syn_id']=syn.getID() + return self._streams[sid] + + def SendHandler(self,conn): + """ Send next portion of data if it is time to do it. Used internally. """ + self.DEBUG('SendHandler called','info') + for sid in self._streams.keys(): + stream=self._streams[sid] + if stream['direction'][:2]=='|>': cont=1 + elif stream['direction'][0]=='>': + chunk=stream['fp'].read(stream['block-size']) + if chunk: + datanode=Node(NS_IBB+' data',{'sid':sid,'seq':stream['seq']},base64.encodestring(chunk)) + stream['seq']+=1 + if stream['seq']==65536: stream['seq']=0 + conn.send(Protocol('message',stream['direction'][1:],payload=[datanode,self._ampnode])) + else: + """ notify the other side about stream closing + notify the local user about sucessfull send + delete the local stream""" + conn.send(Protocol('iq',stream['direction'][1:],'set',payload=[Node(NS_IBB+' close',{'sid':sid})])) + conn.Event(self.DBG_LINE,'SUCCESSFULL SEND',stream) + del self._streams[sid] + self._owner.UnregisterCycleHandler(self.SendHandler) + + """ +<message from='romeo@montague.net/orchard' to='juliet@capulet.com/balcony' id='msg1'> + <data xmlns='http://jabber.org/protocol/ibb' sid='mySID' seq='0'> + qANQR1DBwU4DX7jmYZnncmUQB/9KuKBddzQH+tZ1ZywKK0yHKnq57kWq+RFtQdCJ + WpdWpR0uQsuJe7+vh3NWn59/gTc5MDlX8dS9p0ovStmNcyLhxVgmqS8ZKhsblVeu + IpQ0JgavABqibJolc3BKrVtVV1igKiX/N7Pi8RtY1K18toaMDhdEfhBRzO/XB0+P + AQhYlRjNacGcslkhXqNjK5Va4tuOAPy2n1Q8UUrHbUd0g+xJ9Bm0G0LZXyvCWyKH + kuNEHFQiLuCY6Iv0myq6iX6tjuHehZlFSh80b5BVV9tNLwNR5Eqz1klxMhoghJOA + </data> + <amp xmlns='http://jabber.org/protocol/amp'> + <rule condition='deliver-at' value='stored' action='error'/> + <rule condition='match-resource' value='exact' action='error'/> + </amp> +</message> +""" + + def ReceiveHandler(self,conn,stanza): + """ Receive next portion of incoming datastream and store it write + it to temporary file. Used internally. + """ + sid,seq,data=stanza.getTagAttr('data','sid'),stanza.getTagAttr('data','seq'),stanza.getTagData('data') + self.DEBUG('ReceiveHandler called sid->%s seq->%s'%(sid,seq),'info') + try: seq=int(seq); data=base64.decodestring(data) + except: seq=''; data='' + err=None + if not sid in self._streams.keys(): err=ERR_ITEM_NOT_FOUND + else: + stream=self._streams[sid] + if not data: err=ERR_BAD_REQUEST + elif seq<>stream['seq']: err=ERR_UNEXPECTED_REQUEST + else: + self.DEBUG('Successfull receive sid->%s %s+%s bytes'%(sid,stream['fp'].tell(),len(data)),'ok') + stream['seq']+=1 + stream['fp'].write(data) + if err: + self.DEBUG('Error on receive: %s'%err,'error') + conn.send(Error(Iq(to=stanza.getFrom(),frm=stanza.getTo(),payload=[Node(NS_IBB+' close')]),err,reply=0)) + + def StreamCloseHandler(self,conn,stanza): + """ Handle stream closure due to all data transmitted. + Raise xmpppy event specifying successfull data receive. """ + sid=stanza.getTagAttr('close','sid') + self.DEBUG('StreamCloseHandler called sid->%s'%sid,'info') + if sid in self._streams.keys(): + conn.send(stanza.buildReply('result')) + conn.Event(self.DBG_LINE,'SUCCESSFULL RECEIVE',self._streams[sid]) + del self._streams[sid] + else: conn.send(Error(stanza,ERR_ITEM_NOT_FOUND)) + + def StreamBrokenHandler(self,conn,stanza): + """ Handle stream closure due to all some error while receiving data. + Raise xmpppy event specifying unsuccessfull data receive. """ + syn_id=stanza.getID() + self.DEBUG('StreamBrokenHandler called syn_id->%s'%syn_id,'info') + for sid in self._streams.keys(): + stream=self._streams[sid] + if stream['syn_id']==syn_id: + if stream['direction'][0]=='<': conn.Event(self.DBG_LINE,'ERROR ON RECEIVE',stream) + else: conn.Event(self.DBG_LINE,'ERROR ON SEND',stream) + del self._streams[sid] + + def StreamOpenReplyHandler(self,conn,stanza): + """ Handle remote side reply about is it agree or not to receive our datastream. + Used internally. Raises xmpppy event specfiying if the data transfer + is agreed upon.""" + syn_id=stanza.getID() + self.DEBUG('StreamOpenReplyHandler called syn_id->%s'%syn_id,'info') + for sid in self._streams.keys(): + stream=self._streams[sid] + if stream['syn_id']==syn_id: + if stanza.getType()=='error': + if stream['direction'][0]=='<': conn.Event(self.DBG_LINE,'ERROR ON RECEIVE',stream) + else: conn.Event(self.DBG_LINE,'ERROR ON SEND',stream) + del self._streams[sid] + elif stanza.getType()=='result': + if stream['direction'][0]=='|': + stream['direction']=stream['direction'][1:] + conn.Event(self.DBG_LINE,'STREAM COMMITTED',stream) + else: conn.send(Error(stanza,ERR_UNEXPECTED_REQUEST)) diff --git a/src/xmpppy-0.5.0rc1/build/lib/xmpp/jep0106.py b/src/xmpppy-0.5.0rc1/build/lib/xmpp/jep0106.py new file mode 100644 index 00000000..fcf11145 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/build/lib/xmpp/jep0106.py @@ -0,0 +1,57 @@ + +# JID Escaping XEP-0106 for the xmpppy based transports written by Norman Rasmussen + +"""This file is the XEP-0106 commands. + +Implemented commands as follows: + +4.2 Encode : Encoding Transformation +4.3 Decode : Decoding Transformation + + +""" + +xep0106mapping = [ + [' ' ,'20'], + ['"' ,'22'], + ['&' ,'26'], + ['\'','27'], + ['/' ,'2f'], + [':' ,'3a'], + ['<' ,'3c'], + ['>' ,'3e'], + ['@' ,'40']] + +def JIDEncode(str): + str = str.replace('\\5c', '\\5c5c') + for each in xep0106mapping: + str = str.replace('\\' + each[1], '\\5c' + each[1]) + for each in xep0106mapping: + str = str.replace(each[0], '\\' + each[1]) + return str + +def JIDDecode(str): + for each in xep0106mapping: + str = str.replace('\\' + each[1], each[0]) + return str.replace('\\5c', '\\') + +if __name__ == "__main__": + def test(before,valid): + during = JIDEncode(before) + after = JIDDecode(during) + if during == valid and after == before: + print 'PASS Before: ' + before + print 'PASS During: ' + during + else: + print 'FAIL Before: ' + before + print 'FAIL During: ' + during + print 'FAIL After : ' + after + print + + test('jid escaping',r'jid\20escaping') + test(r'\3and\2is\5@example.com',r'\5c3and\2is\5\40example.com') + test(r'\3catsand\2catsis\5cats@example.com',r'\5c3catsand\2catsis\5c5cats\40example.com') + test(r'\2plus\2is\4',r'\2plus\2is\4') + test(r'foo\bar',r'foo\bar') + test(r'foob\41r',r'foob\41r') + test('here\'s_a wild_&_/cr%zy/_address@example.com',r'here\27s_a\20wild_\26_\2fcr%zy\2f_address\40example.com') diff --git a/src/xmpppy-0.5.0rc1/build/lib/xmpp/protocol.py b/src/xmpppy-0.5.0rc1/build/lib/xmpp/protocol.py new file mode 100644 index 00000000..3e49b8d2 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/build/lib/xmpp/protocol.py @@ -0,0 +1,860 @@ +## protocol.py +## +## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: protocol.py,v 1.60 2009/04/07 11:14:28 snakeru Exp $ + +""" +Protocol module contains tools that is needed for processing of +xmpp-related data structures. +""" + +from simplexml import Node,ustr +import time +NS_ACTIVITY ='http://jabber.org/protocol/activity' # XEP-0108 +NS_ADDRESS ='http://jabber.org/protocol/address' # XEP-0033 +NS_ADMIN ='http://jabber.org/protocol/admin' # XEP-0133 +NS_ADMIN_ADD_USER =NS_ADMIN+'#add-user' # XEP-0133 +NS_ADMIN_DELETE_USER =NS_ADMIN+'#delete-user' # XEP-0133 +NS_ADMIN_DISABLE_USER =NS_ADMIN+'#disable-user' # XEP-0133 +NS_ADMIN_REENABLE_USER =NS_ADMIN+'#reenable-user' # XEP-0133 +NS_ADMIN_END_USER_SESSION =NS_ADMIN+'#end-user-session' # XEP-0133 +NS_ADMIN_GET_USER_PASSWORD =NS_ADMIN+'#get-user-password' # XEP-0133 +NS_ADMIN_CHANGE_USER_PASSWORD =NS_ADMIN+'#change-user-password' # XEP-0133 +NS_ADMIN_GET_USER_ROSTER =NS_ADMIN+'#get-user-roster' # XEP-0133 +NS_ADMIN_GET_USER_LASTLOGIN =NS_ADMIN+'#get-user-lastlogin' # XEP-0133 +NS_ADMIN_USER_STATS =NS_ADMIN+'#user-stats' # XEP-0133 +NS_ADMIN_EDIT_BLACKLIST =NS_ADMIN+'#edit-blacklist' # XEP-0133 +NS_ADMIN_EDIT_WHITELIST =NS_ADMIN+'#edit-whitelist' # XEP-0133 +NS_ADMIN_REGISTERED_USERS_NUM =NS_ADMIN+'#get-registered-users-num' # XEP-0133 +NS_ADMIN_DISABLED_USERS_NUM =NS_ADMIN+'#get-disabled-users-num' # XEP-0133 +NS_ADMIN_ONLINE_USERS_NUM =NS_ADMIN+'#get-online-users-num' # XEP-0133 +NS_ADMIN_ACTIVE_USERS_NUM =NS_ADMIN+'#get-active-users-num' # XEP-0133 +NS_ADMIN_IDLE_USERS_NUM =NS_ADMIN+'#get-idle-users-num' # XEP-0133 +NS_ADMIN_REGISTERED_USERS_LIST =NS_ADMIN+'#get-registered-users-list' # XEP-0133 +NS_ADMIN_DISABLED_USERS_LIST =NS_ADMIN+'#get-disabled-users-list' # XEP-0133 +NS_ADMIN_ONLINE_USERS_LIST =NS_ADMIN+'#get-online-users-list' # XEP-0133 +NS_ADMIN_ACTIVE_USERS_LIST =NS_ADMIN+'#get-active-users-list' # XEP-0133 +NS_ADMIN_IDLE_USERS_LIST =NS_ADMIN+'#get-idle-users-list' # XEP-0133 +NS_ADMIN_ANNOUNCE =NS_ADMIN+'#announce' # XEP-0133 +NS_ADMIN_SET_MOTD =NS_ADMIN+'#set-motd' # XEP-0133 +NS_ADMIN_EDIT_MOTD =NS_ADMIN+'#edit-motd' # XEP-0133 +NS_ADMIN_DELETE_MOTD =NS_ADMIN+'#delete-motd' # XEP-0133 +NS_ADMIN_SET_WELCOME =NS_ADMIN+'#set-welcome' # XEP-0133 +NS_ADMIN_DELETE_WELCOME =NS_ADMIN+'#delete-welcome' # XEP-0133 +NS_ADMIN_EDIT_ADMIN =NS_ADMIN+'#edit-admin' # XEP-0133 +NS_ADMIN_RESTART =NS_ADMIN+'#restart' # XEP-0133 +NS_ADMIN_SHUTDOWN =NS_ADMIN+'#shutdown' # XEP-0133 +NS_AGENTS ='jabber:iq:agents' # XEP-0094 (historical) +NS_AMP ='http://jabber.org/protocol/amp' # XEP-0079 +NS_AMP_ERRORS =NS_AMP+'#errors' # XEP-0079 +NS_AUTH ='jabber:iq:auth' # XEP-0078 +NS_AVATAR ='jabber:iq:avatar' # XEP-0008 (historical) +NS_BIND ='urn:ietf:params:xml:ns:xmpp-bind' # RFC 3920 +NS_BROWSE ='jabber:iq:browse' # XEP-0011 (historical) +NS_BYTESTREAM ='http://jabber.org/protocol/bytestreams' # XEP-0065 +NS_CAPS ='http://jabber.org/protocol/caps' # XEP-0115 +NS_CHATSTATES ='http://jabber.org/protocol/chatstates' # XEP-0085 +NS_CLIENT ='jabber:client' # RFC 3921 +NS_COMMANDS ='http://jabber.org/protocol/commands' # XEP-0050 +NS_COMPONENT_ACCEPT ='jabber:component:accept' # XEP-0114 +NS_COMPONENT_1 ='http://jabberd.jabberstudio.org/ns/component/1.0' # Jabberd2 +NS_COMPRESS ='http://jabber.org/protocol/compress' # XEP-0138 +NS_DATA ='jabber:x:data' # XEP-0004 +NS_DATA_LAYOUT ='http://jabber.org/protocol/xdata-layout' # XEP-0141 +NS_DATA_VALIDATE ='http://jabber.org/protocol/xdata-validate' # XEP-0122 +NS_DELAY ='jabber:x:delay' # XEP-0091 (deprecated) +NS_DIALBACK ='jabber:server:dialback' # RFC 3921 +NS_DISCO ='http://jabber.org/protocol/disco' # XEP-0030 +NS_DISCO_INFO =NS_DISCO+'#info' # XEP-0030 +NS_DISCO_ITEMS =NS_DISCO+'#items' # XEP-0030 +NS_ENCRYPTED ='jabber:x:encrypted' # XEP-0027 +NS_EVENT ='jabber:x:event' # XEP-0022 (deprecated) +NS_FEATURE ='http://jabber.org/protocol/feature-neg' # XEP-0020 +NS_FILE ='http://jabber.org/protocol/si/profile/file-transfer' # XEP-0096 +NS_GATEWAY ='jabber:iq:gateway' # XEP-0100 +NS_GEOLOC ='http://jabber.org/protocol/geoloc' # XEP-0080 +NS_GROUPCHAT ='gc-1.0' # XEP-0045 +NS_HTTP_BIND ='http://jabber.org/protocol/httpbind' # XEP-0124 +NS_IBB ='http://jabber.org/protocol/ibb' # XEP-0047 +NS_INVISIBLE ='presence-invisible' # Jabberd2 +NS_IQ ='iq' # Jabberd2 +NS_LAST ='jabber:iq:last' # XEP-0012 +NS_MESSAGE ='message' # Jabberd2 +NS_MOOD ='http://jabber.org/protocol/mood' # XEP-0107 +NS_MUC ='http://jabber.org/protocol/muc' # XEP-0045 +NS_MUC_ADMIN =NS_MUC+'#admin' # XEP-0045 +NS_MUC_OWNER =NS_MUC+'#owner' # XEP-0045 +NS_MUC_UNIQUE =NS_MUC+'#unique' # XEP-0045 +NS_MUC_USER =NS_MUC+'#user' # XEP-0045 +NS_MUC_REGISTER =NS_MUC+'#register' # XEP-0045 +NS_MUC_REQUEST =NS_MUC+'#request' # XEP-0045 +NS_MUC_ROOMCONFIG =NS_MUC+'#roomconfig' # XEP-0045 +NS_MUC_ROOMINFO =NS_MUC+'#roominfo' # XEP-0045 +NS_MUC_ROOMS =NS_MUC+'#rooms' # XEP-0045 +NS_MUC_TRAFIC =NS_MUC+'#traffic' # XEP-0045 +NS_NICK ='http://jabber.org/protocol/nick' # XEP-0172 +NS_OFFLINE ='http://jabber.org/protocol/offline' # XEP-0013 +NS_PHYSLOC ='http://jabber.org/protocol/physloc' # XEP-0112 +NS_PRESENCE ='presence' # Jabberd2 +NS_PRIVACY ='jabber:iq:privacy' # RFC 3921 +NS_PRIVATE ='jabber:iq:private' # XEP-0049 +NS_PUBSUB ='http://jabber.org/protocol/pubsub' # XEP-0060 +NS_REGISTER ='jabber:iq:register' # XEP-0077 +NS_RC ='http://jabber.org/protocol/rc' # XEP-0146 +NS_ROSTER ='jabber:iq:roster' # RFC 3921 +NS_ROSTERX ='http://jabber.org/protocol/rosterx' # XEP-0144 +NS_RPC ='jabber:iq:rpc' # XEP-0009 +NS_SASL ='urn:ietf:params:xml:ns:xmpp-sasl' # RFC 3920 +NS_SEARCH ='jabber:iq:search' # XEP-0055 +NS_SERVER ='jabber:server' # RFC 3921 +NS_SESSION ='urn:ietf:params:xml:ns:xmpp-session' # RFC 3921 +NS_SI ='http://jabber.org/protocol/si' # XEP-0096 +NS_SI_PUB ='http://jabber.org/protocol/sipub' # XEP-0137 +NS_SIGNED ='jabber:x:signed' # XEP-0027 +NS_STANZAS ='urn:ietf:params:xml:ns:xmpp-stanzas' # RFC 3920 +NS_STREAMS ='http://etherx.jabber.org/streams' # RFC 3920 +NS_TIME ='jabber:iq:time' # XEP-0090 (deprecated) +NS_TLS ='urn:ietf:params:xml:ns:xmpp-tls' # RFC 3920 +NS_VACATION ='http://jabber.org/protocol/vacation' # XEP-0109 +NS_VCARD ='vcard-temp' # XEP-0054 +NS_VCARD_UPDATE ='vcard-temp:x:update' # XEP-0153 +NS_VERSION ='jabber:iq:version' # XEP-0092 +NS_WAITINGLIST ='http://jabber.org/protocol/waitinglist' # XEP-0130 +NS_XHTML_IM ='http://jabber.org/protocol/xhtml-im' # XEP-0071 +NS_XMPP_STREAMS ='urn:ietf:params:xml:ns:xmpp-streams' # RFC 3920 + +xmpp_stream_error_conditions=""" +bad-format -- -- -- The entity has sent XML that cannot be processed. +bad-namespace-prefix -- -- -- The entity has sent a namespace prefix that is unsupported, or has sent no namespace prefix on an element that requires such a prefix. +conflict -- -- -- The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the existing stream. +connection-timeout -- -- -- The entity has not generated any traffic over the stream for some period of time. +host-gone -- -- -- The value of the 'to' attribute provided by the initiating entity in the stream header corresponds to a hostname that is no longer hosted by the server. +host-unknown -- -- -- The value of the 'to' attribute provided by the initiating entity in the stream header does not correspond to a hostname that is hosted by the server. +improper-addressing -- -- -- A stanza sent between two servers lacks a 'to' or 'from' attribute (or the attribute has no value). +internal-server-error -- -- -- The server has experienced a misconfiguration or an otherwise-undefined internal error that prevents it from servicing the stream. +invalid-from -- cancel -- -- The JID or hostname provided in a 'from' address does not match an authorized JID or validated domain negotiated between servers via SASL or dialback, or between a client and a server via authentication and resource authorization. +invalid-id -- -- -- The stream ID or dialback ID is invalid or does not match an ID previously provided. +invalid-namespace -- -- -- The streams namespace name is something other than "http://etherx.jabber.org/streams" or the dialback namespace name is something other than "jabber:server:dialback". +invalid-xml -- -- -- The entity has sent invalid XML over the stream to a server that performs validation. +not-authorized -- -- -- The entity has attempted to send data before the stream has been authenticated, or otherwise is not authorized to perform an action related to stream negotiation. +policy-violation -- -- -- The entity has violated some local service policy. +remote-connection-failed -- -- -- The server is unable to properly connect to a remote resource that is required for authentication or authorization. +resource-constraint -- -- -- The server lacks the system resources necessary to service the stream. +restricted-xml -- -- -- The entity has attempted to send restricted XML features such as a comment, processing instruction, DTD, entity reference, or unescaped character. +see-other-host -- -- -- The server will not provide service to the initiating entity but is redirecting traffic to another host. +system-shutdown -- -- -- The server is being shut down and all active streams are being closed. +undefined-condition -- -- -- The error condition is not one of those defined by the other conditions in this list. +unsupported-encoding -- -- -- The initiating entity has encoded the stream in an encoding that is not supported by the server. +unsupported-stanza-type -- -- -- The initiating entity has sent a first-level child of the stream that is not supported by the server. +unsupported-version -- -- -- The value of the 'version' attribute provided by the initiating entity in the stream header specifies a version of XMPP that is not supported by the server. +xml-not-well-formed -- -- -- The initiating entity has sent XML that is not well-formed.""" +xmpp_stanza_error_conditions=""" +bad-request -- 400 -- modify -- The sender has sent XML that is malformed or that cannot be processed. +conflict -- 409 -- cancel -- Access cannot be granted because an existing resource or session exists with the same name or address. +feature-not-implemented -- 501 -- cancel -- The feature requested is not implemented by the recipient or server and therefore cannot be processed. +forbidden -- 403 -- auth -- The requesting entity does not possess the required permissions to perform the action. +gone -- 302 -- modify -- The recipient or server can no longer be contacted at this address. +internal-server-error -- 500 -- wait -- The server could not process the stanza because of a misconfiguration or an otherwise-undefined internal server error. +item-not-found -- 404 -- cancel -- The addressed JID or item requested cannot be found. +jid-malformed -- 400 -- modify -- The value of the 'to' attribute in the sender's stanza does not adhere to the syntax defined in Addressing Scheme. +not-acceptable -- 406 -- cancel -- The recipient or server understands the request but is refusing to process it because it does not meet criteria defined by the recipient or server. +not-allowed -- 405 -- cancel -- The recipient or server does not allow any entity to perform the action. +not-authorized -- 401 -- auth -- The sender must provide proper credentials before being allowed to perform the action, or has provided improper credentials. +payment-required -- 402 -- auth -- The requesting entity is not authorized to access the requested service because payment is required. +recipient-unavailable -- 404 -- wait -- The intended recipient is temporarily unavailable. +redirect -- 302 -- modify -- The recipient or server is redirecting requests for this information to another entity. +registration-required -- 407 -- auth -- The requesting entity is not authorized to access the requested service because registration is required. +remote-server-not-found -- 404 -- cancel -- A remote server or service specified as part or all of the JID of the intended recipient does not exist. +remote-server-timeout -- 504 -- wait -- A remote server or service specified as part or all of the JID of the intended recipient could not be contacted within a reasonable amount of time. +resource-constraint -- 500 -- wait -- The server or recipient lacks the system resources necessary to service the request. +service-unavailable -- 503 -- cancel -- The server or recipient does not currently provide the requested service. +subscription-required -- 407 -- auth -- The requesting entity is not authorized to access the requested service because a subscription is required. +undefined-condition -- 500 -- -- +unexpected-request -- 400 -- wait -- The recipient or server understood the request but was not expecting it at this time (e.g., the request was out of order).""" +sasl_error_conditions=""" +aborted -- -- -- The receiving entity acknowledges an <abort/> element sent by the initiating entity; sent in reply to the <abort/> element. +incorrect-encoding -- -- -- The data provided by the initiating entity could not be processed because the [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003. encoding is incorrect (e.g., because the encoding does not adhere to the definition in Section 3 of [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003.); sent in reply to a <response/> element or an <auth/> element with initial response data. +invalid-authzid -- -- -- The authzid provided by the initiating entity is invalid, either because it is incorrectly formatted or because the initiating entity does not have permissions to authorize that ID; sent in reply to a <response/> element or an <auth/> element with initial response data. +invalid-mechanism -- -- -- The initiating entity did not provide a mechanism or requested a mechanism that is not supported by the receiving entity; sent in reply to an <auth/> element. +mechanism-too-weak -- -- -- The mechanism requested by the initiating entity is weaker than server policy permits for that initiating entity; sent in reply to a <response/> element or an <auth/> element with initial response data. +not-authorized -- -- -- The authentication failed because the initiating entity did not provide valid credentials (this includes but is not limited to the case of an unknown username); sent in reply to a <response/> element or an <auth/> element with initial response data. +temporary-auth-failure -- -- -- The authentication failed because of a temporary error condition within the receiving entity; sent in reply to an <auth/> element or <response/> element.""" + +ERRORS,_errorcodes={},{} +for ns,errname,errpool in [(NS_XMPP_STREAMS,'STREAM',xmpp_stream_error_conditions), + (NS_STANZAS ,'ERR' ,xmpp_stanza_error_conditions), + (NS_SASL ,'SASL' ,sasl_error_conditions)]: + for err in errpool.split('\n')[1:]: + cond,code,typ,text=err.split(' -- ') + name=errname+'_'+cond.upper().replace('-','_') + locals()[name]=ns+' '+cond + ERRORS[ns+' '+cond]=[code,typ,text] + if code: _errorcodes[code]=cond +del ns,errname,errpool,err,cond,code,typ,text + +def isResultNode(node): + """ Returns true if the node is a positive reply. """ + return node and node.getType()=='result' +def isErrorNode(node): + """ Returns true if the node is a negative reply. """ + return node and node.getType()=='error' + +class NodeProcessed(Exception): + """ Exception that should be raised by handler when the handling should be stopped. """ +class StreamError(Exception): + """ Base exception class for stream errors.""" +class BadFormat(StreamError): pass +class BadNamespacePrefix(StreamError): pass +class Conflict(StreamError): pass +class ConnectionTimeout(StreamError): pass +class HostGone(StreamError): pass +class HostUnknown(StreamError): pass +class ImproperAddressing(StreamError): pass +class InternalServerError(StreamError): pass +class InvalidFrom(StreamError): pass +class InvalidID(StreamError): pass +class InvalidNamespace(StreamError): pass +class InvalidXML(StreamError): pass +class NotAuthorized(StreamError): pass +class PolicyViolation(StreamError): pass +class RemoteConnectionFailed(StreamError): pass +class ResourceConstraint(StreamError): pass +class RestrictedXML(StreamError): pass +class SeeOtherHost(StreamError): pass +class SystemShutdown(StreamError): pass +class UndefinedCondition(StreamError): pass +class UnsupportedEncoding(StreamError): pass +class UnsupportedStanzaType(StreamError): pass +class UnsupportedVersion(StreamError): pass +class XMLNotWellFormed(StreamError): pass + +stream_exceptions = {'bad-format': BadFormat, + 'bad-namespace-prefix': BadNamespacePrefix, + 'conflict': Conflict, + 'connection-timeout': ConnectionTimeout, + 'host-gone': HostGone, + 'host-unknown': HostUnknown, + 'improper-addressing': ImproperAddressing, + 'internal-server-error': InternalServerError, + 'invalid-from': InvalidFrom, + 'invalid-id': InvalidID, + 'invalid-namespace': InvalidNamespace, + 'invalid-xml': InvalidXML, + 'not-authorized': NotAuthorized, + 'policy-violation': PolicyViolation, + 'remote-connection-failed': RemoteConnectionFailed, + 'resource-constraint': ResourceConstraint, + 'restricted-xml': RestrictedXML, + 'see-other-host': SeeOtherHost, + 'system-shutdown': SystemShutdown, + 'undefined-condition': UndefinedCondition, + 'unsupported-encoding': UnsupportedEncoding, + 'unsupported-stanza-type': UnsupportedStanzaType, + 'unsupported-version': UnsupportedVersion, + 'xml-not-well-formed': XMLNotWellFormed} + +class JID: + """ JID object. JID can be built from string, modified, compared, serialised into string. """ + def __init__(self, jid=None, node='', domain='', resource=''): + """ Constructor. JID can be specified as string (jid argument) or as separate parts. + Examples: + JID('node@domain/resource') + JID(node='node',domain='domain.org') + """ + if not jid and not domain: raise ValueError('JID must contain at least domain name') + elif type(jid)==type(self): self.node,self.domain,self.resource=jid.node,jid.domain,jid.resource + elif domain: self.node,self.domain,self.resource=node,domain,resource + else: + if jid.find('@')+1: self.node,jid=jid.split('@',1) + else: self.node='' + if jid.find('/')+1: self.domain,self.resource=jid.split('/',1) + else: self.domain,self.resource=jid,'' + def getNode(self): + """ Return the node part of the JID """ + return self.node + def setNode(self,node): + """ Set the node part of the JID to new value. Specify None to remove the node part.""" + self.node=node.lower() + def getDomain(self): + """ Return the domain part of the JID """ + return self.domain + def setDomain(self,domain): + """ Set the domain part of the JID to new value.""" + self.domain=domain.lower() + def getResource(self): + """ Return the resource part of the JID """ + return self.resource + def setResource(self,resource): + """ Set the resource part of the JID to new value. Specify None to remove the resource part.""" + self.resource=resource + def getStripped(self): + """ Return the bare representation of JID. I.e. string value w/o resource. """ + return self.__str__(0) + def __eq__(self, other): + """ Compare the JID to another instance or to string for equality. """ + try: other=JID(other) + except ValueError: return 0 + return self.resource==other.resource and self.__str__(0) == other.__str__(0) + def __ne__(self, other): + """ Compare the JID to another instance or to string for non-equality. """ + return not self.__eq__(other) + def bareMatch(self, other): + """ Compare the node and domain parts of the JID's for equality. """ + return self.__str__(0) == JID(other).__str__(0) + def __str__(self,wresource=1): + """ Serialise JID into string. """ + if self.node: jid=self.node+'@'+self.domain + else: jid=self.domain + if wresource and self.resource: return jid+'/'+self.resource + return jid + def __hash__(self): + """ Produce hash of the JID, Allows to use JID objects as keys of the dictionary. """ + return hash(self.__str__()) + +class Protocol(Node): + """ A "stanza" object class. Contains methods that are common for presences, iqs and messages. """ + def __init__(self, name=None, to=None, typ=None, frm=None, attrs={}, payload=[], timestamp=None, xmlns=None, node=None): + """ Constructor, name is the name of the stanza i.e. 'message' or 'presence' or 'iq'. + to is the value of 'to' attribure, 'typ' - 'type' attribute + frn - from attribure, attrs - other attributes mapping, payload - same meaning as for simplexml payload definition + timestamp - the time value that needs to be stamped over stanza + xmlns - namespace of top stanza node + node - parsed or unparsed stana to be taken as prototype. + """ + if not attrs: attrs={} + if to: attrs['to']=to + if frm: attrs['from']=frm + if typ: attrs['type']=typ + Node.__init__(self, tag=name, attrs=attrs, payload=payload, node=node) + if not node and xmlns: self.setNamespace(xmlns) + if self['to']: self.setTo(self['to']) + if self['from']: self.setFrom(self['from']) + if node and type(self)==type(node) and self.__class__==node.__class__ and self.attrs.has_key('id'): del self.attrs['id'] + self.timestamp=None + for x in self.getTags('x',namespace=NS_DELAY): + try: + if not self.getTimestamp() or x.getAttr('stamp')<self.getTimestamp(): self.setTimestamp(x.getAttr('stamp')) + except: pass + if timestamp is not None: self.setTimestamp(timestamp) # To auto-timestamp stanza just pass timestamp='' + def getTo(self): + """ Return value of the 'to' attribute. """ + try: return self['to'] + except: return None + def getFrom(self): + """ Return value of the 'from' attribute. """ + try: return self['from'] + except: return None + def getTimestamp(self): + """ Return the timestamp in the 'yyyymmddThhmmss' format. """ + return self.timestamp + def getID(self): + """ Return the value of the 'id' attribute. """ + return self.getAttr('id') + def setTo(self,val): + """ Set the value of the 'to' attribute. """ + self.setAttr('to', JID(val)) + def getType(self): + """ Return the value of the 'type' attribute. """ + return self.getAttr('type') + def setFrom(self,val): + """ Set the value of the 'from' attribute. """ + self.setAttr('from', JID(val)) + def setType(self,val): + """ Set the value of the 'type' attribute. """ + self.setAttr('type', val) + def setID(self,val): + """ Set the value of the 'id' attribute. """ + self.setAttr('id', val) + def getError(self): + """ Return the error-condition (if present) or the textual description of the error (otherwise). """ + errtag=self.getTag('error') + if errtag: + for tag in errtag.getChildren(): + if tag.getName()<>'text': return tag.getName() + return errtag.getData() + def getErrorCode(self): + """ Return the error code. Obsolette. """ + return self.getTagAttr('error','code') + def setError(self,error,code=None): + """ Set the error code. Obsolette. Use error-conditions instead. """ + if code: + if str(code) in _errorcodes.keys(): error=ErrorNode(_errorcodes[str(code)],text=error) + else: error=ErrorNode(ERR_UNDEFINED_CONDITION,code=code,typ='cancel',text=error) + elif type(error) in [type(''),type(u'')]: error=ErrorNode(error) + self.setType('error') + self.addChild(node=error) + def setTimestamp(self,val=None): + """Set the timestamp. timestamp should be the yyyymmddThhmmss string.""" + if not val: val=time.strftime('%Y%m%dT%H:%M:%S', time.gmtime()) + self.timestamp=val + self.setTag('x',{'stamp':self.timestamp},namespace=NS_DELAY) + def getProperties(self): + """ Return the list of namespaces to which belongs the direct childs of element""" + props=[] + for child in self.getChildren(): + prop=child.getNamespace() + if prop not in props: props.append(prop) + return props + def __setitem__(self,item,val): + """ Set the item 'item' to the value 'val'.""" + if item in ['to','from']: val=JID(val) + return self.setAttr(item,val) + +class Message(Protocol): + """ XMPP Message stanza - "push" mechanism.""" + def __init__(self, to=None, body=None, typ=None, subject=None, attrs={}, frm=None, payload=[], timestamp=None, xmlns=NS_CLIENT, node=None): + """ Create message object. You can specify recipient, text of message, type of message + any additional attributes, sender of the message, any additional payload (f.e. jabber:x:delay element) and namespace in one go. + Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as message. """ + Protocol.__init__(self, 'message', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node) + if body: self.setBody(body) + if subject: self.setSubject(subject) + def getBody(self): + """ Returns text of the message. """ + return self.getTagData('body') + def getSubject(self): + """ Returns subject of the message. """ + return self.getTagData('subject') + def getThread(self): + """ Returns thread of the message. """ + return self.getTagData('thread') + def setBody(self,val): + """ Sets the text of the message. """ + self.setTagData('body',val) + def setSubject(self,val): + """ Sets the subject of the message. """ + self.setTagData('subject',val) + def setThread(self,val): + """ Sets the thread of the message. """ + self.setTagData('thread',val) + def buildReply(self,text=None): + """ Builds and returns another message object with specified text. + The to, from and thread properties of new message are pre-set as reply to this message. """ + m=Message(to=self.getFrom(),frm=self.getTo(),body=text) + th=self.getThread() + if th: m.setThread(th) + return m + +class Presence(Protocol): + """ XMPP Presence object.""" + def __init__(self, to=None, typ=None, priority=None, show=None, status=None, attrs={}, frm=None, timestamp=None, payload=[], xmlns=NS_CLIENT, node=None): + """ Create presence object. You can specify recipient, type of message, priority, show and status values + any additional attributes, sender of the presence, timestamp, any additional payload (f.e. jabber:x:delay element) and namespace in one go. + Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as presence. """ + Protocol.__init__(self, 'presence', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node) + if priority: self.setPriority(priority) + if show: self.setShow(show) + if status: self.setStatus(status) + def getPriority(self): + """ Returns the priority of the message. """ + return self.getTagData('priority') + def getShow(self): + """ Returns the show value of the message. """ + return self.getTagData('show') + def getStatus(self): + """ Returns the status string of the message. """ + return self.getTagData('status') + def setPriority(self,val): + """ Sets the priority of the message. """ + self.setTagData('priority',val) + def setShow(self,val): + """ Sets the show value of the message. """ + self.setTagData('show',val) + def setStatus(self,val): + """ Sets the status string of the message. """ + self.setTagData('status',val) + + def _muc_getItemAttr(self,tag,attr): + for xtag in self.getTags('x'): + for child in xtag.getTags(tag): + return child.getAttr(attr) + def _muc_getSubTagDataAttr(self,tag,attr): + for xtag in self.getTags('x'): + for child in xtag.getTags('item'): + for cchild in child.getTags(tag): + return cchild.getData(),cchild.getAttr(attr) + return None,None + def getRole(self): + """Returns the presence role (for groupchat)""" + return self._muc_getItemAttr('item','role') + def getAffiliation(self): + """Returns the presence affiliation (for groupchat)""" + return self._muc_getItemAttr('item','affiliation') + def getNick(self): + """Returns the nick value (for nick change in groupchat)""" + return self._muc_getItemAttr('item','nick') + def getJid(self): + """Returns the presence jid (for groupchat)""" + return self._muc_getItemAttr('item','jid') + def getReason(self): + """Returns the reason of the presence (for groupchat)""" + return self._muc_getSubTagDataAttr('reason','')[0] + def getActor(self): + """Returns the reason of the presence (for groupchat)""" + return self._muc_getSubTagDataAttr('actor','jid')[1] + def getStatusCode(self): + """Returns the status code of the presence (for groupchat)""" + return self._muc_getItemAttr('status','code') + +class Iq(Protocol): + """ XMPP Iq object - get/set dialog mechanism. """ + def __init__(self, typ=None, queryNS=None, attrs={}, to=None, frm=None, payload=[], xmlns=NS_CLIENT, node=None): + """ Create Iq object. You can specify type, query namespace + any additional attributes, recipient of the iq, sender of the iq, any additional payload (f.e. jabber:x:data node) and namespace in one go. + Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as an iq. """ + Protocol.__init__(self, 'iq', to=to, typ=typ, attrs=attrs, frm=frm, xmlns=xmlns, node=node) + if payload: self.setQueryPayload(payload) + if queryNS: self.setQueryNS(queryNS) + def getQueryNS(self): + """ Return the namespace of the 'query' child element.""" + tag=self.getTag('query') + if tag: return tag.getNamespace() + def getQuerynode(self): + """ Return the 'node' attribute value of the 'query' child element.""" + return self.getTagAttr('query','node') + def getQueryPayload(self): + """ Return the 'query' child element payload.""" + tag=self.getTag('query') + if tag: return tag.getPayload() + def getQueryChildren(self): + """ Return the 'query' child element child nodes.""" + tag=self.getTag('query') + if tag: return tag.getChildren() + def setQueryNS(self,namespace): + """ Set the namespace of the 'query' child element.""" + self.setTag('query').setNamespace(namespace) + def setQueryPayload(self,payload): + """ Set the 'query' child element payload.""" + self.setTag('query').setPayload(payload) + def setQuerynode(self,node): + """ Set the 'node' attribute value of the 'query' child element.""" + self.setTagAttr('query','node',node) + def buildReply(self,typ): + """ Builds and returns another Iq object of specified type. + The to, from and query child node of new Iq are pre-set as reply to this Iq. """ + iq=Iq(typ,to=self.getFrom(),frm=self.getTo(),attrs={'id':self.getID()}) + if self.getTag('query'): iq.setQueryNS(self.getQueryNS()) + return iq + +class ErrorNode(Node): + """ XMPP-style error element. + In the case of stanza error should be attached to XMPP stanza. + In the case of stream-level errors should be used separately. """ + def __init__(self,name,code=None,typ=None,text=None): + """ Create new error node object. + Mandatory parameter: name - name of error condition. + Optional parameters: code, typ, text. Used for backwards compartibility with older jabber protocol.""" + if ERRORS.has_key(name): + cod,type,txt=ERRORS[name] + ns=name.split()[0] + else: cod,ns,type,txt='500',NS_STANZAS,'cancel','' + if typ: type=typ + if code: cod=code + if text: txt=text + Node.__init__(self,'error',{},[Node(name)]) + if type: self.setAttr('type',type) + if not cod: self.setName('stream:error') + if txt: self.addChild(node=Node(ns+' text',{},[txt])) + if cod: self.setAttr('code',cod) + +class Error(Protocol): + """ Used to quickly transform received stanza into error reply.""" + def __init__(self,node,error,reply=1): + """ Create error reply basing on the received 'node' stanza and the 'error' error condition. + If the 'node' is not the received stanza but locally created ('to' and 'from' fields needs not swapping) + specify the 'reply' argument as false.""" + if reply: Protocol.__init__(self,to=node.getFrom(),frm=node.getTo(),node=node) + else: Protocol.__init__(self,node=node) + self.setError(error) + if node.getType()=='error': self.__str__=self.__dupstr__ + def __dupstr__(self,dup1=None,dup2=None): + """ Dummy function used as preventor of creating error node in reply to error node. + I.e. you will not be able to serialise "double" error into string. + """ + return '' + +class DataField(Node): + """ This class is used in the DataForm class to describe the single data item. + If you are working with jabber:x:data (XEP-0004, XEP-0068, XEP-0122) + then you will need to work with instances of this class. """ + def __init__(self,name=None,value=None,typ=None,required=0,label=None,desc=None,options=[],node=None): + """ Create new data field of specified name,value and type. + Also 'required','desc' and 'options' fields can be set. + Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new datafiled. + """ + Node.__init__(self,'field',node=node) + if name: self.setVar(name) + if type(value) in [list,tuple]: self.setValues(value) + elif value: self.setValue(value) + if typ: self.setType(typ) + elif not typ and not node: self.setType('text-single') + if required: self.setRequired(required) + if label: self.setLabel(label) + if desc: self.setDesc(desc) + if options: self.setOptions(options) + def setRequired(self,req=1): + """ Change the state of the 'required' flag. """ + if req: self.setTag('required') + else: + try: self.delChild('required') + except ValueError: return + def isRequired(self): + """ Returns in this field a required one. """ + return self.getTag('required') + def setLabel(self,label): + """ Set the label of this field. """ + self.setAttr('label',label) + def getLabel(self): + """ Return the label of this field. """ + return self.getAttr('label') + def setDesc(self,desc): + """ Set the description of this field. """ + self.setTagData('desc',desc) + def getDesc(self): + """ Return the description of this field. """ + return self.getTagData('desc') + def setValue(self,val): + """ Set the value of this field. """ + self.setTagData('value',val) + def getValue(self): + return self.getTagData('value') + def setValues(self,lst): + """ Set the values of this field as values-list. + Replaces all previous filed values! If you need to just add a value - use addValue method.""" + while self.getTag('value'): self.delChild('value') + for val in lst: self.addValue(val) + def addValue(self,val): + """ Add one more value to this field. Used in 'get' iq's or such.""" + self.addChild('value',{},[val]) + def getValues(self): + """ Return the list of values associated with this field.""" + ret=[] + for tag in self.getTags('value'): ret.append(tag.getData()) + return ret + def getOptions(self): + """ Return label-option pairs list associated with this field.""" + ret=[] + for tag in self.getTags('option'): ret.append([tag.getAttr('label'),tag.getTagData('value')]) + return ret + def setOptions(self,lst): + """ Set label-option pairs list associated with this field.""" + while self.getTag('option'): self.delChild('option') + for opt in lst: self.addOption(opt) + def addOption(self,opt): + """ Add one more label-option pair to this field.""" + if type(opt) in [str,unicode]: self.addChild('option').setTagData('value',opt) + else: self.addChild('option',{'label':opt[0]}).setTagData('value',opt[1]) + def getType(self): + """ Get type of this field. """ + return self.getAttr('type') + def setType(self,val): + """ Set type of this field. """ + return self.setAttr('type',val) + def getVar(self): + """ Get 'var' attribute value of this field. """ + return self.getAttr('var') + def setVar(self,val): + """ Set 'var' attribute value of this field. """ + return self.setAttr('var',val) + +class DataReported(Node): + """ This class is used in the DataForm class to describe the 'reported data field' data items which are used in + 'multiple item form results' (as described in XEP-0004). + Represents the fields that will be returned from a search. This information is useful when + you try to use the jabber:iq:search namespace to return dynamic form information. + """ + def __init__(self,node=None): + """ Create new empty 'reported data' field. However, note that, according XEP-0004: + * It MUST contain one or more DataFields. + * Contained DataFields SHOULD possess a 'type' and 'label' attribute in addition to 'var' attribute + * Contained DataFields SHOULD NOT contain a <value/> element. + Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new + dataitem. + """ + Node.__init__(self,'reported',node=node) + if node: + newkids=[] + for n in self.getChildren(): + if n.getName()=='field': newkids.append(DataField(node=n)) + else: newkids.append(n) + self.kids=newkids + def getField(self,name): + """ Return the datafield object with name 'name' (if exists). """ + return self.getTag('field',attrs={'var':name}) + def setField(self,name,typ=None,label=None): + """ Create if nessessary or get the existing datafield object with name 'name' and return it. + If created, attributes 'type' and 'label' are applied to new datafield.""" + f=self.getField(name) + if f: return f + return self.addChild(node=DataField(name,None,typ,0,label)) + def asDict(self): + """ Represent dataitem as simple dictionary mapping of datafield names to their values.""" + ret={} + for field in self.getTags('field'): + name=field.getAttr('var') + typ=field.getType() + if isinstance(typ,(str,unicode)) and typ[-6:]=='-multi': + val=[] + for i in field.getTags('value'): val.append(i.getData()) + else: val=field.getTagData('value') + ret[name]=val + if self.getTag('instructions'): ret['instructions']=self.getInstructions() + return ret + def __getitem__(self,name): + """ Simple dictionary interface for getting datafields values by their names.""" + item=self.getField(name) + if item: return item.getValue() + raise IndexError('No such field') + def __setitem__(self,name,val): + """ Simple dictionary interface for setting datafields values by their names.""" + return self.setField(name).setValue(val) + +class DataItem(Node): + """ This class is used in the DataForm class to describe data items which are used in 'multiple + item form results' (as described in XEP-0004). + """ + def __init__(self,node=None): + """ Create new empty data item. However, note that, according XEP-0004, DataItem MUST contain ALL + DataFields described in DataReported. + Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new + dataitem. + """ + Node.__init__(self,'item',node=node) + if node: + newkids=[] + for n in self.getChildren(): + if n.getName()=='field': newkids.append(DataField(node=n)) + else: newkids.append(n) + self.kids=newkids + def getField(self,name): + """ Return the datafield object with name 'name' (if exists). """ + return self.getTag('field',attrs={'var':name}) + def setField(self,name): + """ Create if nessessary or get the existing datafield object with name 'name' and return it. """ + f=self.getField(name) + if f: return f + return self.addChild(node=DataField(name)) + def asDict(self): + """ Represent dataitem as simple dictionary mapping of datafield names to their values.""" + ret={} + for field in self.getTags('field'): + name=field.getAttr('var') + typ=field.getType() + if isinstance(typ,(str,unicode)) and typ[-6:]=='-multi': + val=[] + for i in field.getTags('value'): val.append(i.getData()) + else: val=field.getTagData('value') + ret[name]=val + if self.getTag('instructions'): ret['instructions']=self.getInstructions() + return ret + def __getitem__(self,name): + """ Simple dictionary interface for getting datafields values by their names.""" + item=self.getField(name) + if item: return item.getValue() + raise IndexError('No such field') + def __setitem__(self,name,val): + """ Simple dictionary interface for setting datafields values by their names.""" + return self.setField(name).setValue(val) + +class DataForm(Node): + """ DataForm class. Used for manipulating dataforms in XMPP. + Relevant XEPs: 0004, 0068, 0122. + Can be used in disco, pub-sub and many other applications.""" + def __init__(self, typ=None, data=[], title=None, node=None): + """ + Create new dataform of type 'typ'; 'data' is the list of DataReported, + DataItem and DataField instances that this dataform contains; 'title' + is the title string. + You can specify the 'node' argument as the other node to be used as + base for constructing this dataform. + + title and instructions is optional and SHOULD NOT contain newlines. + Several instructions MAY be present. + 'typ' can be one of ('form' | 'submit' | 'cancel' | 'result' ) + 'typ' of reply iq can be ( 'result' | 'set' | 'set' | 'result' ) respectively. + 'cancel' form can not contain any fields. All other forms contains AT LEAST one field. + 'title' MAY be included in forms of type "form" and "result" + """ + Node.__init__(self,'x',node=node) + if node: + newkids=[] + for n in self.getChildren(): + if n.getName()=='field': newkids.append(DataField(node=n)) + elif n.getName()=='item': newkids.append(DataItem(node=n)) + elif n.getName()=='reported': newkids.append(DataReported(node=n)) + else: newkids.append(n) + self.kids=newkids + if typ: self.setType(typ) + self.setNamespace(NS_DATA) + if title: self.setTitle(title) + if type(data)==type({}): + newdata=[] + for name in data.keys(): newdata.append(DataField(name,data[name])) + data=newdata + for child in data: + if type(child) in [type(''),type(u'')]: self.addInstructions(child) + elif child.__class__.__name__=='DataField': self.kids.append(child) + elif child.__class__.__name__=='DataItem': self.kids.append(child) + elif child.__class__.__name__=='DataReported': self.kids.append(child) + else: self.kids.append(DataField(node=child)) + def getType(self): + """ Return the type of dataform. """ + return self.getAttr('type') + def setType(self,typ): + """ Set the type of dataform. """ + self.setAttr('type',typ) + def getTitle(self): + """ Return the title of dataform. """ + return self.getTagData('title') + def setTitle(self,text): + """ Set the title of dataform. """ + self.setTagData('title',text) + def getInstructions(self): + """ Return the instructions of dataform. """ + return self.getTagData('instructions') + def setInstructions(self,text): + """ Set the instructions of dataform. """ + self.setTagData('instructions',text) + def addInstructions(self,text): + """ Add one more instruction to the dataform. """ + self.addChild('instructions',{},[text]) + def getField(self,name): + """ Return the datafield object with name 'name' (if exists). """ + return self.getTag('field',attrs={'var':name}) + def setField(self,name): + """ Create if nessessary or get the existing datafield object with name 'name' and return it. """ + f=self.getField(name) + if f: return f + return self.addChild(node=DataField(name)) + def asDict(self): + """ Represent dataform as simple dictionary mapping of datafield names to their values.""" + ret={} + for field in self.getTags('field'): + name=field.getAttr('var') + typ=field.getType() + if isinstance(typ,(str,unicode)) and typ[-6:]=='-multi': + val=[] + for i in field.getTags('value'): val.append(i.getData()) + else: val=field.getTagData('value') + ret[name]=val + if self.getTag('instructions'): ret['instructions']=self.getInstructions() + return ret + def __getitem__(self,name): + """ Simple dictionary interface for getting datafields values by their names.""" + item=self.getField(name) + if item: return item.getValue() + raise IndexError('No such field') + def __setitem__(self,name,val): + """ Simple dictionary interface for setting datafields values by their names.""" + return self.setField(name).setValue(val) diff --git a/src/xmpppy-0.5.0rc1/build/lib/xmpp/roster.py b/src/xmpppy-0.5.0rc1/build/lib/xmpp/roster.py new file mode 100644 index 00000000..676a4c9a --- /dev/null +++ b/src/xmpppy-0.5.0rc1/build/lib/xmpp/roster.py @@ -0,0 +1,184 @@ +## roster.py +## +## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: roster.py,v 1.20 2005/07/13 13:22:52 snakeru Exp $ + +""" +Simple roster implementation. Can be used though for different tasks like +mass-renaming of contacts. +""" + +from protocol import * +from client import PlugIn + +class Roster(PlugIn): + """ Defines a plenty of methods that will allow you to manage roster. + Also automatically track presences from remote JIDs taking into + account that every JID can have multiple resources connected. Does not + currently support 'error' presences. + You can also use mapping interface for access to the internal representation of + contacts in roster. + """ + def __init__(self): + """ Init internal variables. """ + PlugIn.__init__(self) + self.DBG_LINE='roster' + self._data = {} + self.set=None + self._exported_methods=[self.getRoster] + + def plugin(self,owner,request=1): + """ Register presence and subscription trackers in the owner's dispatcher. + Also request roster from server if the 'request' argument is set. + Used internally.""" + self._owner.RegisterHandler('iq',self.RosterIqHandler,'result',NS_ROSTER) + self._owner.RegisterHandler('iq',self.RosterIqHandler,'set',NS_ROSTER) + self._owner.RegisterHandler('presence',self.PresenceHandler) + if request: self.Request() + + def Request(self,force=0): + """ Request roster from server if it were not yet requested + (or if the 'force' argument is set). """ + if self.set is None: self.set=0 + elif not force: return + self._owner.send(Iq('get',NS_ROSTER)) + self.DEBUG('Roster requested from server','start') + + def getRoster(self): + """ Requests roster from server if neccessary and returns self.""" + if not self.set: self.Request() + while not self.set: self._owner.Process(10) + return self + + def RosterIqHandler(self,dis,stanza): + """ Subscription tracker. Used internally for setting items state in + internal roster representation. """ + for item in stanza.getTag('query').getTags('item'): + jid=item.getAttr('jid') + if item.getAttr('subscription')=='remove': + if self._data.has_key(jid): del self._data[jid] + raise NodeProcessed # a MUST + self.DEBUG('Setting roster item %s...'%jid,'ok') + if not self._data.has_key(jid): self._data[jid]={} + self._data[jid]['name']=item.getAttr('name') + self._data[jid]['ask']=item.getAttr('ask') + self._data[jid]['subscription']=item.getAttr('subscription') + self._data[jid]['groups']=[] + if not self._data[jid].has_key('resources'): self._data[jid]['resources']={} + for group in item.getTags('group'): self._data[jid]['groups'].append(group.getData()) + self._data[self._owner.User+'@'+self._owner.Server]={'resources':{},'name':None,'ask':None,'subscription':None,'groups':None,} + self.set=1 + raise NodeProcessed # a MUST. Otherwise you'll get back an <iq type='error'/> + + def PresenceHandler(self,dis,pres): + """ Presence tracker. Used internally for setting items' resources state in + internal roster representation. """ + jid=JID(pres.getFrom()) + if not self._data.has_key(jid.getStripped()): self._data[jid.getStripped()]={'name':None,'ask':None,'subscription':'none','groups':['Not in roster'],'resources':{}} + + item=self._data[jid.getStripped()] + typ=pres.getType() + + if not typ: + self.DEBUG('Setting roster item %s for resource %s...'%(jid.getStripped(),jid.getResource()),'ok') + item['resources'][jid.getResource()]=res={'show':None,'status':None,'priority':'0','timestamp':None} + if pres.getTag('show'): res['show']=pres.getShow() + if pres.getTag('status'): res['status']=pres.getStatus() + if pres.getTag('priority'): res['priority']=pres.getPriority() + if not pres.getTimestamp(): pres.setTimestamp() + res['timestamp']=pres.getTimestamp() + elif typ=='unavailable' and item['resources'].has_key(jid.getResource()): del item['resources'][jid.getResource()] + # Need to handle type='error' also + + def _getItemData(self,jid,dataname): + """ Return specific jid's representation in internal format. Used internally. """ + jid=jid[:(jid+'/').find('/')] + return self._data[jid][dataname] + def _getResourceData(self,jid,dataname): + """ Return specific jid's resource representation in internal format. Used internally. """ + if jid.find('/')+1: + jid,resource=jid.split('/',1) + if self._data[jid]['resources'].has_key(resource): return self._data[jid]['resources'][resource][dataname] + elif self._data[jid]['resources'].keys(): + lastpri=-129 + for r in self._data[jid]['resources'].keys(): + if int(self._data[jid]['resources'][r]['priority'])>lastpri: resource,lastpri=r,int(self._data[jid]['resources'][r]['priority']) + return self._data[jid]['resources'][resource][dataname] + def delItem(self,jid): + """ Delete contact 'jid' from roster.""" + self._owner.send(Iq('set',NS_ROSTER,payload=[Node('item',{'jid':jid,'subscription':'remove'})])) + def getAsk(self,jid): + """ Returns 'ask' value of contact 'jid'.""" + return self._getItemData(jid,'ask') + def getGroups(self,jid): + """ Returns groups list that contact 'jid' belongs to.""" + return self._getItemData(jid,'groups') + def getName(self,jid): + """ Returns name of contact 'jid'.""" + return self._getItemData(jid,'name') + def getPriority(self,jid): + """ Returns priority of contact 'jid'. 'jid' should be a full (not bare) JID.""" + return self._getResourceData(jid,'priority') + def getRawRoster(self): + """ Returns roster representation in internal format. """ + return self._data + def getRawItem(self,jid): + """ Returns roster item 'jid' representation in internal format. """ + return self._data[jid[:(jid+'/').find('/')]] + def getShow(self, jid): + """ Returns 'show' value of contact 'jid'. 'jid' should be a full (not bare) JID.""" + return self._getResourceData(jid,'show') + def getStatus(self, jid): + """ Returns 'status' value of contact 'jid'. 'jid' should be a full (not bare) JID.""" + return self._getResourceData(jid,'status') + def getSubscription(self,jid): + """ Returns 'subscription' value of contact 'jid'.""" + return self._getItemData(jid,'subscription') + def getResources(self,jid): + """ Returns list of connected resources of contact 'jid'.""" + return self._data[jid[:(jid+'/').find('/')]]['resources'].keys() + def setItem(self,jid,name=None,groups=[]): + """ Creates/renames contact 'jid' and sets the groups list that it now belongs to.""" + iq=Iq('set',NS_ROSTER) + query=iq.getTag('query') + attrs={'jid':jid} + if name: attrs['name']=name + item=query.setTag('item',attrs) + for group in groups: item.addChild(node=Node('group',payload=[group])) + self._owner.send(iq) + def getItems(self): + """ Return list of all [bare] JIDs that the roster is currently tracks.""" + return self._data.keys() + def keys(self): + """ Same as getItems. Provided for the sake of dictionary interface.""" + return self._data.keys() + def __getitem__(self,item): + """ Get the contact in the internal format. Raises KeyError if JID 'item' is not in roster.""" + return self._data[item] + def getItem(self,item): + """ Get the contact in the internal format (or None if JID 'item' is not in roster).""" + if self._data.has_key(item): return self._data[item] + def Subscribe(self,jid): + """ Send subscription request to JID 'jid'.""" + self._owner.send(Presence(jid,'subscribe')) + def Unsubscribe(self,jid): + """ Ask for removing our subscription for JID 'jid'.""" + self._owner.send(Presence(jid,'unsubscribe')) + def Authorize(self,jid): + """ Authorise JID 'jid'. Works only if these JID requested auth previously. """ + self._owner.send(Presence(jid,'subscribed')) + def Unauthorize(self,jid): + """ Unauthorise JID 'jid'. Use for declining authorisation request + or for removing existing authorization. """ + self._owner.send(Presence(jid,'unsubscribed')) diff --git a/src/xmpppy-0.5.0rc1/build/lib/xmpp/session.py b/src/xmpppy-0.5.0rc1/build/lib/xmpp/session.py new file mode 100644 index 00000000..24066b32 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/build/lib/xmpp/session.py @@ -0,0 +1,349 @@ +## +## XMPP server +## +## Copyright (C) 2004 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +__version__="$Id" + +""" +When your handler is called it is getting the session instance as the first argument. +This is the difference from xmpppy 0.1 where you got the "Client" instance. +With Session class you can have "multi-session" client instead of having +one client for each connection. Is is specifically important when you are +writing the server. +""" + +from protocol import * + +# Transport-level flags +SOCKET_UNCONNECTED =0 +SOCKET_ALIVE =1 +SOCKET_DEAD =2 +# XML-level flags +STREAM__NOT_OPENED =1 +STREAM__OPENED =2 +STREAM__CLOSING =3 +STREAM__CLOSED =4 +# XMPP-session flags +SESSION_NOT_AUTHED =1 +SESSION_AUTHED =2 +SESSION_BOUND =3 +SESSION_OPENED =4 +SESSION_CLOSED =5 + +class Session: + """ + The Session class instance is used for storing all session-related info like + credentials, socket/xml stream/session state flags, roster items (in case of + client type connection) etc. + Session object have no means of discovering is any info is ready to be read. + Instead you should use poll() (recomended) or select() methods for this purpose. + Session can be one of two types: 'server' and 'client'. 'server' session handles + inbound connection and 'client' one used to create an outbound one. + Session instance have multitude of internal attributes. The most imporant is the 'peer' one. + It is set once the peer is authenticated (client). + """ + def __init__(self,socket,owner,xmlns=None,peer=None): + """ When the session is created it's type (client/server) is determined from the beginning. + socket argument is the pre-created socket-like object. + It must have the following methods: send, recv, fileno, close. + owner is the 'master' instance that have Dispatcher plugged into it and generally + will take care about all session events. + xmlns is the stream namespace that will be used. Client must set this argument + If server sets this argument than stream will be dropped if opened with some another namespace. + peer is the name of peer instance. This is the flag that differentiates client session from + server session. Client must set it to the name of the server that will be connected, server must + leave this argument alone. + """ + self.xmlns=xmlns + if peer: + self.TYP='client' + self.peer=peer + self._socket_state=SOCKET_UNCONNECTED + else: + self.TYP='server' + self.peer=None + self._socket_state=SOCKET_ALIVE + self._sock=socket + self._send=socket.send + self._recv=socket.recv + self.fileno=socket.fileno + self._registered=0 + + self.Dispatcher=owner.Dispatcher + self.DBG_LINE='session' + self.DEBUG=owner.Dispatcher.DEBUG + self._expected={} + self._owner=owner + if self.TYP=='server': self.ID=`random.random()`[2:] + else: self.ID=None + + self.sendbuffer='' + self._stream_pos_queued=None + self._stream_pos_sent=0 + self.deliver_key_queue=[] + self.deliver_queue_map={} + self.stanza_queue=[] + + self._session_state=SESSION_NOT_AUTHED + self.waiting_features=[] + for feature in [NS_TLS,NS_SASL,NS_BIND,NS_SESSION]: + if feature in owner.features: self.waiting_features.append(feature) + self.features=[] + self.feature_in_process=None + self.slave_session=None + self.StartStream() + + def StartStream(self): + """ This method is used to initialise the internal xml expat parser + and to send initial stream header (in case of client connection). + Should be used after initial connection and after every stream restart.""" + self._stream_state=STREAM__NOT_OPENED + self.Stream=simplexml.NodeBuilder() + self.Stream._dispatch_depth=2 + self.Stream.dispatch=self._dispatch + self.Parse=self.Stream.Parse + self.Stream.stream_footer_received=self._stream_close + if self.TYP=='client': + self.Stream.stream_header_received=self._catch_stream_id + self._stream_open() + else: + self.Stream.stream_header_received=self._stream_open + + def receive(self): + """ Reads all pending incoming data. + Raises IOError on disconnection. + Blocks until at least one byte is read.""" + try: received = self._recv(10240) + except: received = '' + + if len(received): # length of 0 means disconnect + self.DEBUG(`self.fileno()`+' '+received,'got') + else: + self.DEBUG('Socket error while receiving data','error') + self.set_socket_state(SOCKET_DEAD) + raise IOError("Peer disconnected") + return received + + def sendnow(self,chunk): + """ Put chunk into "immidiatedly send" queue. + Should only be used for auth/TLS stuff and like. + If you just want to shedule regular stanza for delivery use enqueue method. + """ + if isinstance(chunk,Node): chunk = chunk.__str__().encode('utf-8') + elif type(chunk)==type(u''): chunk = chunk.encode('utf-8') + self.enqueue(chunk) + + def enqueue(self,stanza): + """ Takes Protocol instance as argument. + Puts stanza into "send" fifo queue. Items into the send queue are hold until + stream authenticated. After that this method is effectively the same as "sendnow" method.""" + if isinstance(stanza,Protocol): + self.stanza_queue.append(stanza) + else: self.sendbuffer+=stanza + if self._socket_state>=SOCKET_ALIVE: self.push_queue() + + def push_queue(self,failreason=ERR_RECIPIENT_UNAVAILABLE): + """ If stream is authenticated than move items from "send" queue to "immidiatedly send" queue. + Else if the stream is failed then return all queued stanzas with error passed as argument. + Otherwise do nothing.""" + # If the stream authed - convert stanza_queue into sendbuffer and set the checkpoints + + if self._stream_state>=STREAM__CLOSED or self._socket_state>=SOCKET_DEAD: # the stream failed. Return all stanzas that are still waiting for delivery. + self._owner.deactivatesession(self) + for key in self.deliver_key_queue: # Not sure. May be I + self._dispatch(Error(self.deliver_queue_map[key],failreason),trusted=1) # should simply re-dispatch it? + for stanza in self.stanza_queue: # But such action can invoke + self._dispatch(Error(stanza,failreason),trusted=1) # Infinite loops in case of S2S connection... + self.deliver_queue_map,self.deliver_key_queue,self.stanza_queue={},[],[] + return + elif self._session_state>=SESSION_AUTHED: # FIXME! äÏÌÖÅÎ ÂÙÔØ ËÁËÏÊ-ÔÏ ÄÒÕÇÏÊ ÆÌÁÇ. + #### LOCK_QUEUE + for stanza in self.stanza_queue: + txt=stanza.__str__().encode('utf-8') + self.sendbuffer+=txt + self._stream_pos_queued+=len(txt) # should be re-evaluated for SSL connection. + self.deliver_queue_map[self._stream_pos_queued]=stanza # position of the stream when stanza will be successfully and fully sent + self.deliver_key_queue.append(self._stream_pos_queued) + self.stanza_queue=[] + #### UNLOCK_QUEUE + + def flush_queue(self): + """ Put the "immidiatedly send" queue content on the wire. Blocks until at least one byte sent.""" + if self.sendbuffer: + try: + # LOCK_QUEUE + sent=self._send(self.sendbuffer) # âÌÏËÉÒÕÀÝÁÑ ÛÔÕÞËÁ! + except: + # UNLOCK_QUEUE + self.set_socket_state(SOCKET_DEAD) + self.DEBUG("Socket error while sending data",'error') + return self.terminate_stream() + self.DEBUG(`self.fileno()`+' '+self.sendbuffer[:sent],'sent') + self._stream_pos_sent+=sent + self.sendbuffer=self.sendbuffer[sent:] + self._stream_pos_delivered=self._stream_pos_sent # Should be acquired from socket somehow. Take SSL into account. + while self.deliver_key_queue and self._stream_pos_delivered>self.deliver_key_queue[0]: + del self.deliver_queue_map[self.deliver_key_queue[0]] + self.deliver_key_queue.remove(self.deliver_key_queue[0]) + # UNLOCK_QUEUE + + def _dispatch(self,stanza,trusted=0): + """ This is callback that is used to pass the received stanza forth to owner's dispatcher + _if_ the stream is authorised. Otherwise the stanza is just dropped. + The 'trusted' argument is used to emulate stanza receive. + This method is used internally. + """ + self._owner.packets+=1 + if self._stream_state==STREAM__OPENED or trusted: # if the server really should reject all stanzas after he is closed stream (himeself)? + self.DEBUG(stanza.__str__(),'dispatch') + stanza.trusted=trusted + return self.Dispatcher.dispatch(stanza,self) + + def _catch_stream_id(self,ns=None,tag='stream',attrs={}): + """ This callback is used to detect the stream namespace of incoming stream. Used internally. """ + if not attrs.has_key('id') or not attrs['id']: + return self.terminate_stream(STREAM_INVALID_XML) + self.ID=attrs['id'] + if not attrs.has_key('version'): self._owner.Dialback(self) + + def _stream_open(self,ns=None,tag='stream',attrs={}): + """ This callback is used to handle opening stream tag of the incoming stream. + In the case of client session it just make some validation. + Server session also sends server headers and if the stream valid the features node. + Used internally. """ + text='<?xml version="1.0" encoding="utf-8"?>\n<stream:stream' + if self.TYP=='client': + text+=' to="%s"'%self.peer + else: + text+=' id="%s"'%self.ID + if not attrs.has_key('to'): text+=' from="%s"'%self._owner.servernames[0] + else: text+=' from="%s"'%attrs['to'] + if attrs.has_key('xml:lang'): text+=' xml:lang="%s"'%attrs['xml:lang'] + if self.xmlns: xmlns=self.xmlns + else: xmlns=NS_SERVER + text+=' xmlns:db="%s" xmlns:stream="%s" xmlns="%s"'%(NS_DIALBACK,NS_STREAMS,xmlns) + if attrs.has_key('version') or self.TYP=='client': text+=' version="1.0"' + self.sendnow(text+'>') + self.set_stream_state(STREAM__OPENED) + if self.TYP=='client': return + if tag<>'stream': return self.terminate_stream(STREAM_INVALID_XML) + if ns<>NS_STREAMS: return self.terminate_stream(STREAM_INVALID_NAMESPACE) + if self.Stream.xmlns<>self.xmlns: return self.terminate_stream(STREAM_BAD_NAMESPACE_PREFIX) + if not attrs.has_key('to'): return self.terminate_stream(STREAM_IMPROPER_ADDRESSING) + if attrs['to'] not in self._owner.servernames: return self.terminate_stream(STREAM_HOST_UNKNOWN) + self.ourname=attrs['to'].lower() + if self.TYP=='server' and attrs.has_key('version'): + # send features + features=Node('stream:features') + if NS_TLS in self.waiting_features: + features.NT.starttls.setNamespace(NS_TLS) + features.T.starttls.NT.required + if NS_SASL in self.waiting_features: + features.NT.mechanisms.setNamespace(NS_SASL) + for mec in self._owner.SASL.mechanisms: + features.T.mechanisms.NT.mechanism=mec + else: + if NS_BIND in self.waiting_features: features.NT.bind.setNamespace(NS_BIND) + if NS_SESSION in self.waiting_features: features.NT.session.setNamespace(NS_SESSION) + self.sendnow(features) + + def feature(self,feature): + """ Declare some stream feature as activated one. """ + if feature not in self.features: self.features.append(feature) + self.unfeature(feature) + + def unfeature(self,feature): + """ Declare some feature as illegal. Illegal features can not be used. + Example: BIND feature becomes illegal after Non-SASL auth. """ + if feature in self.waiting_features: self.waiting_features.remove(feature) + + def _stream_close(self,unregister=1): + """ Write the closing stream tag and destroy the underlaying socket. Used internally. """ + if self._stream_state>=STREAM__CLOSED: return + self.set_stream_state(STREAM__CLOSING) + self.sendnow('</stream:stream>') + self.set_stream_state(STREAM__CLOSED) + self.push_queue() # decompose queue really since STREAM__CLOSED + self._owner.flush_queues() + if unregister: self._owner.unregistersession(self) + self._destroy_socket() + + def terminate_stream(self,error=None,unregister=1): + """ Notify the peer about stream closure. + Ensure that xmlstream is not brokes - i.e. if the stream isn't opened yet - + open it before closure. + If the error condition is specified than create a stream error and send it along with + closing stream tag. + Emulate receiving 'unavailable' type presence just before stream closure. + """ + if self._stream_state>=STREAM__CLOSING: return + if self._stream_state<STREAM__OPENED: + self.set_stream_state(STREAM__CLOSING) + self._stream_open() + else: + self.set_stream_state(STREAM__CLOSING) + p=Presence(typ='unavailable') + p.setNamespace(NS_CLIENT) + self._dispatch(p,trusted=1) + if error: + if isinstance(error,Node): self.sendnow(error) + else: self.sendnow(ErrorNode(error)) + self._stream_close(unregister=unregister) + if self.slave_session: + self.slave_session.terminate_stream(STREAM_REMOTE_CONNECTION_FAILED) + + def _destroy_socket(self): + """ Break cyclic dependancies to let python's GC free memory right now.""" + self.Stream.dispatch=None + self.Stream.stream_footer_received=None + self.Stream.stream_header_received=None + self.Stream.destroy() + self._sock.close() + self.set_socket_state(SOCKET_DEAD) + + def start_feature(self,f): + """ Declare some feature as "negotiating now" to prevent other features from start negotiating. """ + if self.feature_in_process: raise "Starting feature %s over %s !"%(f,self.feature_in_process) + self.feature_in_process=f + + def stop_feature(self,f): + """ Declare some feature as "negotiated" to allow other features start negotiating. """ + if self.feature_in_process<>f: raise "Stopping feature %s instead of %s !"%(f,self.feature_in_process) + self.feature_in_process=None + + def set_socket_state(self,newstate): + """ Change the underlaying socket state. + Socket starts with SOCKET_UNCONNECTED state + and then proceeds (possibly) to SOCKET_ALIVE + and then to SOCKET_DEAD """ + if self._socket_state<newstate: self._socket_state=newstate + + def set_session_state(self,newstate): + """ Change the session state. + Session starts with SESSION_NOT_AUTHED state + and then comes through + SESSION_AUTHED, SESSION_BOUND, SESSION_OPENED and SESSION_CLOSED states. + """ + if self._session_state<newstate: + if self._session_state<SESSION_AUTHED and \ + newstate>=SESSION_AUTHED: self._stream_pos_queued=self._stream_pos_sent + self._session_state=newstate + + def set_stream_state(self,newstate): + """ Change the underlaying XML stream state + Stream starts with STREAM__NOT_OPENED and then proceeds with + STREAM__OPENED, STREAM__CLOSING and STREAM__CLOSED states. + Note that some features (like TLS and SASL) + requires stream re-start so this state can have non-linear changes. """ + if self._stream_state<newstate: self._stream_state=newstate diff --git a/src/xmpppy-0.5.0rc1/build/lib/xmpp/simplexml.py b/src/xmpppy-0.5.0rc1/build/lib/xmpp/simplexml.py new file mode 100644 index 00000000..99383dea --- /dev/null +++ b/src/xmpppy-0.5.0rc1/build/lib/xmpp/simplexml.py @@ -0,0 +1,485 @@ +## simplexml.py based on Mattew Allum's xmlstream.py +## +## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: simplexml.py,v 1.34 2009/03/03 10:24:02 normanr Exp $ + +"""Simplexml module provides xmpppy library with all needed tools to handle XML nodes and XML streams. +I'm personally using it in many other separate projects. It is designed to be as standalone as possible.""" + +import xml.parsers.expat + +def XMLescape(txt): + """Returns provided string with symbols & < > " replaced by their respective XML entities.""" + # replace also FORM FEED and ESC, because they are not valid XML chars + return txt.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """).replace(u'\x0C', "").replace(u'\x1B', "") + +ENCODING='utf-8' +def ustr(what): + """Converts object "what" to unicode string using it's own __str__ method if accessible or unicode method otherwise.""" + if isinstance(what, unicode): return what + try: r=what.__str__() + except AttributeError: r=str(what) + if not isinstance(r, unicode): return unicode(r,ENCODING) + return r + +class Node(object): + """ Node class describes syntax of separate XML Node. It have a constructor that permits node creation + from set of "namespace name", attributes and payload of text strings and other nodes. + It does not natively support building node from text string and uses NodeBuilder class for that purpose. + After creation node can be mangled in many ways so it can be completely changed. + Also node can be serialised into string in one of two modes: default (where the textual representation + of node describes it exactly) and "fancy" - with whitespace added to make indentation and thus make + result more readable by human. + + Node class have attribute FORCE_NODE_RECREATION that is defaults to False thus enabling fast node + replication from the some other node. The drawback of the fast way is that new node shares some + info with the "original" node that is changing the one node may influence the other. Though it is + rarely needed (in xmpppy it is never needed at all since I'm usually never using original node after + replication (and using replication only to move upwards on the classes tree). + """ + FORCE_NODE_RECREATION=0 + def __init__(self, tag=None, attrs={}, payload=[], parent=None, nsp=None, node_built=False, node=None): + """ Takes "tag" argument as the name of node (prepended by namespace, if needed and separated from it + by a space), attrs dictionary as the set of arguments, payload list as the set of textual strings + and child nodes that this node carries within itself and "parent" argument that is another node + that this one will be the child of. Also the __init__ can be provided with "node" argument that is + either a text string containing exactly one node or another Node instance to begin with. If both + "node" and other arguments is provided then the node initially created as replica of "node" + provided and then modified to be compliant with other arguments.""" + if node: + if self.FORCE_NODE_RECREATION and isinstance(node, Node): + node=str(node) + if not isinstance(node, Node): + node=NodeBuilder(node,self) + node_built = True + else: + self.name,self.namespace,self.attrs,self.data,self.kids,self.parent,self.nsd = node.name,node.namespace,{},[],[],node.parent,{} + for key in node.attrs.keys(): self.attrs[key]=node.attrs[key] + for data in node.data: self.data.append(data) + for kid in node.kids: self.kids.append(kid) + for k,v in node.nsd.items(): self.nsd[k] = v + else: self.name,self.namespace,self.attrs,self.data,self.kids,self.parent,self.nsd = 'tag','',{},[],[],None,{} + if parent: + self.parent = parent + self.nsp_cache = {} + if nsp: + for k,v in nsp.items(): self.nsp_cache[k] = v + for attr,val in attrs.items(): + if attr == 'xmlns': + self.nsd[u''] = val + elif attr.startswith('xmlns:'): + self.nsd[attr[6:]] = val + self.attrs[attr]=attrs[attr] + if tag: + if node_built: + pfx,self.name = (['']+tag.split(':'))[-2:] + self.namespace = self.lookup_nsp(pfx) + else: + if ' ' in tag: + self.namespace,self.name = tag.split() + else: + self.name = tag + if isinstance(payload, basestring): payload=[payload] + for i in payload: + if isinstance(i, Node): self.addChild(node=i) + else: self.data.append(ustr(i)) + + def lookup_nsp(self,pfx=''): + ns = self.nsd.get(pfx,None) + if ns is None: + ns = self.nsp_cache.get(pfx,None) + if ns is None: + if self.parent: + ns = self.parent.lookup_nsp(pfx) + self.nsp_cache[pfx] = ns + else: + return 'http://www.gajim.org/xmlns/undeclared' + return ns + + def __str__(self,fancy=0): + """ Method used to dump node into textual representation. + if "fancy" argument is set to True produces indented output for readability.""" + s = (fancy-1) * 2 * ' ' + "<" + self.name + if self.namespace: + if not self.parent or self.parent.namespace!=self.namespace: + if 'xmlns' not in self.attrs: + s = s + ' xmlns="%s"'%self.namespace + for key in self.attrs.keys(): + val = ustr(self.attrs[key]) + s = s + ' %s="%s"' % ( key, XMLescape(val) ) + s = s + ">" + cnt = 0 + if self.kids: + if fancy: s = s + "\n" + for a in self.kids: + if not fancy and (len(self.data)-1)>=cnt: s=s+XMLescape(self.data[cnt]) + elif (len(self.data)-1)>=cnt: s=s+XMLescape(self.data[cnt].strip()) + if isinstance(a, Node): + s = s + a.__str__(fancy and fancy+1) + elif a: + s = s + a.__str__() + cnt=cnt+1 + if not fancy and (len(self.data)-1) >= cnt: s = s + XMLescape(self.data[cnt]) + elif (len(self.data)-1) >= cnt: s = s + XMLescape(self.data[cnt].strip()) + if not self.kids and s.endswith('>'): + s=s[:-1]+' />' + if fancy: s = s + "\n" + else: + if fancy and not self.data: s = s + (fancy-1) * 2 * ' ' + s = s + "</" + self.name + ">" + if fancy: s = s + "\n" + return s + def getCDATA(self): + """ Serialise node, dropping all tags and leaving CDATA intact. + That is effectively kills all formatiing, leaving only text were contained in XML. + """ + s = "" + cnt = 0 + if self.kids: + for a in self.kids: + s=s+self.data[cnt] + if a: s = s + a.getCDATA() + cnt=cnt+1 + if (len(self.data)-1) >= cnt: s = s + self.data[cnt] + return s + def addChild(self, name=None, attrs={}, payload=[], namespace=None, node=None): + """ If "node" argument is provided, adds it as child node. Else creates new node from + the other arguments' values and adds it as well.""" + if 'xmlns' in attrs: + raise AttributeError("Use namespace=x instead of attrs={'xmlns':x}") + if node: + newnode=node + node.parent = self + else: newnode=Node(tag=name, parent=self, attrs=attrs, payload=payload) + if namespace: + newnode.setNamespace(namespace) + self.kids.append(newnode) + self.data.append(u'') + return newnode + def addData(self, data): + """ Adds some CDATA to node. """ + self.data.append(ustr(data)) + self.kids.append(None) + def clearData(self): + """ Removes all CDATA from the node. """ + self.data=[] + def delAttr(self, key): + """ Deletes an attribute "key" """ + del self.attrs[key] + def delChild(self, node, attrs={}): + """ Deletes the "node" from the node's childs list, if "node" is an instance. + Else deletes the first node that have specified name and (optionally) attributes. """ + if not isinstance(node, Node): node=self.getTag(node,attrs) + self.kids[self.kids.index(node)]=None + return node + def getAttrs(self): + """ Returns all node's attributes as dictionary. """ + return self.attrs + def getAttr(self, key): + """ Returns value of specified attribute. """ + try: return self.attrs[key] + except: return None + def getChildren(self): + """ Returns all node's child nodes as list. """ + return self.kids + def getData(self): + """ Returns all node CDATA as string (concatenated). """ + return ''.join(self.data) + def getName(self): + """ Returns the name of node """ + return self.name + def getNamespace(self): + """ Returns the namespace of node """ + return self.namespace + def getParent(self): + """ Returns the parent of node (if present). """ + return self.parent + def getPayload(self): + """ Return the payload of node i.e. list of child nodes and CDATA entries. + F.e. for "<node>text1<nodea/><nodeb/> text2</node>" will be returned list: + ['text1', <nodea instance>, <nodeb instance>, ' text2']. """ + ret=[] + for i in range(max(len(self.data),len(self.kids))): + if i < len(self.data) and self.data[i]: ret.append(self.data[i]) + if i < len(self.kids) and self.kids[i]: ret.append(self.kids[i]) + return ret + def getTag(self, name, attrs={}, namespace=None): + """ Filters all child nodes using specified arguments as filter. + Returns the first found or None if not found. """ + return self.getTags(name, attrs, namespace, one=1) + def getTagAttr(self,tag,attr): + """ Returns attribute value of the child with specified name (or None if no such attribute).""" + try: return self.getTag(tag).attrs[attr] + except: return None + def getTagData(self,tag): + """ Returns cocatenated CDATA of the child with specified name.""" + try: return self.getTag(tag).getData() + except: return None + def getTags(self, name, attrs={}, namespace=None, one=0): + """ Filters all child nodes using specified arguments as filter. + Returns the list of nodes found. """ + nodes=[] + for node in self.kids: + if not node: continue + if namespace and namespace!=node.getNamespace(): continue + if node.getName() == name: + for key in attrs.keys(): + if key not in node.attrs or node.attrs[key]!=attrs[key]: break + else: nodes.append(node) + if one and nodes: return nodes[0] + if not one: return nodes + + def iterTags(self, name, attrs={}, namespace=None): + """ Iterate over all children using specified arguments as filter. """ + for node in self.kids: + if not node: continue + if namespace is not None and namespace!=node.getNamespace(): continue + if node.getName() == name: + for key in attrs.keys(): + if key not in node.attrs or \ + node.attrs[key]!=attrs[key]: break + else: + yield node + + def setAttr(self, key, val): + """ Sets attribute "key" with the value "val". """ + self.attrs[key]=val + def setData(self, data): + """ Sets node's CDATA to provided string. Resets all previous CDATA!""" + self.data=[ustr(data)] + def setName(self,val): + """ Changes the node name. """ + self.name = val + def setNamespace(self, namespace): + """ Changes the node namespace. """ + self.namespace=namespace + def setParent(self, node): + """ Sets node's parent to "node". WARNING: do not checks if the parent already present + and not removes the node from the list of childs of previous parent. """ + self.parent = node + def setPayload(self,payload,add=0): + """ Sets node payload according to the list specified. WARNING: completely replaces all node's + previous content. If you wish just to add child or CDATA - use addData or addChild methods. """ + if isinstance(payload, basestring): payload=[payload] + if add: self.kids+=payload + else: self.kids=payload + def setTag(self, name, attrs={}, namespace=None): + """ Same as getTag but if the node with specified namespace/attributes not found, creates such + node and returns it. """ + node=self.getTags(name, attrs, namespace=namespace, one=1) + if node: return node + else: return self.addChild(name, attrs, namespace=namespace) + def setTagAttr(self,tag,attr,val): + """ Creates new node (if not already present) with name "tag" + and sets it's attribute "attr" to value "val". """ + try: self.getTag(tag).attrs[attr]=val + except: self.addChild(tag,attrs={attr:val}) + def setTagData(self,tag,val,attrs={}): + """ Creates new node (if not already present) with name "tag" and (optionally) attributes "attrs" + and sets it's CDATA to string "val". """ + try: self.getTag(tag,attrs).setData(ustr(val)) + except: self.addChild(tag,attrs,payload=[ustr(val)]) + def has_attr(self,key): + """ Checks if node have attribute "key".""" + return key in self.attrs + def __getitem__(self,item): + """ Returns node's attribute "item" value. """ + return self.getAttr(item) + def __setitem__(self,item,val): + """ Sets node's attribute "item" value. """ + return self.setAttr(item,val) + def __delitem__(self,item): + """ Deletes node's attribute "item". """ + return self.delAttr(item) + def __getattr__(self,attr): + """ Reduce memory usage caused by T/NT classes - use memory only when needed. """ + if attr=='T': + self.T=T(self) + return self.T + if attr=='NT': + self.NT=NT(self) + return self.NT + raise AttributeError + +class T: + """ Auxiliary class used to quick access to node's child nodes. """ + def __init__(self,node): self.__dict__['node']=node + def __getattr__(self,attr): return self.node.getTag(attr) + def __setattr__(self,attr,val): + if isinstance(val,Node): Node.__init__(self.node.setTag(attr),node=val) + else: return self.node.setTagData(attr,val) + def __delattr__(self,attr): return self.node.delChild(attr) + +class NT(T): + """ Auxiliary class used to quick create node's child nodes. """ + def __getattr__(self,attr): return self.node.addChild(attr) + def __setattr__(self,attr,val): + if isinstance(val,Node): self.node.addChild(attr,node=val) + else: return self.node.addChild(attr,payload=[val]) + +DBG_NODEBUILDER = 'nodebuilder' +class NodeBuilder: + """ Builds a Node class minidom from data parsed to it. This class used for two purposes: + 1. Creation an XML Node from a textual representation. F.e. reading a config file. See an XML2Node method. + 2. Handling an incoming XML stream. This is done by mangling + the __dispatch_depth parameter and redefining the dispatch method. + You do not need to use this class directly if you do not designing your own XML handler.""" + def __init__(self,data=None,initial_node=None): + """ Takes two optional parameters: "data" and "initial_node". + By default class initialised with empty Node class instance. + Though, if "initial_node" is provided it used as "starting point". + You can think about it as of "node upgrade". + "data" (if provided) feeded to parser immidiatedly after instance init. + """ + self.DEBUG(DBG_NODEBUILDER, "Preparing to handle incoming XML stream.", 'start') + self._parser = xml.parsers.expat.ParserCreate() + self._parser.StartElementHandler = self.starttag + self._parser.EndElementHandler = self.endtag + self._parser.CharacterDataHandler = self.handle_cdata + self._parser.StartNamespaceDeclHandler = self.handle_namespace_start + self._parser.buffer_text = True + self.Parse = self._parser.Parse + + self.__depth = 0 + self.__last_depth = 0 + self.__max_depth = 0 + self._dispatch_depth = 1 + self._document_attrs = None + self._document_nsp = None + self._mini_dom=initial_node + self.last_is_data = 1 + self._ptr=None + self.data_buffer = None + self.streamError = '' + if data: + self._parser.Parse(data,1) + + def check_data_buffer(self): + if self.data_buffer: + self._ptr.data.append(''.join(self.data_buffer)) + del self.data_buffer[:] + self.data_buffer = None + + def destroy(self): + """ Method used to allow class instance to be garbage-collected. """ + self.check_data_buffer() + self._parser.StartElementHandler = None + self._parser.EndElementHandler = None + self._parser.CharacterDataHandler = None + self._parser.StartNamespaceDeclHandler = None + + def starttag(self, tag, attrs): + """XML Parser callback. Used internally""" + self.check_data_buffer() + self._inc_depth() + self.DEBUG(DBG_NODEBUILDER, "DEPTH -> %i , tag -> %s, attrs -> %s" % (self.__depth, tag, `attrs`), 'down') + if self.__depth == self._dispatch_depth: + if not self._mini_dom : + self._mini_dom = Node(tag=tag, attrs=attrs, nsp = self._document_nsp, node_built=True) + else: + Node.__init__(self._mini_dom,tag=tag, attrs=attrs, nsp = self._document_nsp, node_built=True) + self._ptr = self._mini_dom + elif self.__depth > self._dispatch_depth: + self._ptr.kids.append(Node(tag=tag,parent=self._ptr,attrs=attrs, node_built=True)) + self._ptr = self._ptr.kids[-1] + if self.__depth == 1: + self._document_attrs = {} + self._document_nsp = {} + nsp, name = (['']+tag.split(':'))[-2:] + for attr,val in attrs.items(): + if attr == 'xmlns': + self._document_nsp[u''] = val + elif attr.startswith('xmlns:'): + self._document_nsp[attr[6:]] = val + else: + self._document_attrs[attr] = val + ns = self._document_nsp.get(nsp, 'http://www.gajim.org/xmlns/undeclared-root') + try: + self.stream_header_received(ns, name, attrs) + except ValueError, e: + self._document_attrs = None + raise ValueError(str(e)) + if not self.last_is_data and self._ptr.parent: + self._ptr.parent.data.append('') + self.last_is_data = 0 + + def endtag(self, tag ): + """XML Parser callback. Used internally""" + self.DEBUG(DBG_NODEBUILDER, "DEPTH -> %i , tag -> %s" % (self.__depth, tag), 'up') + self.check_data_buffer() + if self.__depth == self._dispatch_depth: + if self._mini_dom.getName() == 'error': + self.streamError = self._mini_dom.getChildren()[0].getName() + self.dispatch(self._mini_dom) + elif self.__depth > self._dispatch_depth: + self._ptr = self._ptr.parent + else: + self.DEBUG(DBG_NODEBUILDER, "Got higher than dispatch level. Stream terminated?", 'stop') + self._dec_depth() + self.last_is_data = 0 + if self.__depth == 0: self.stream_footer_received() + + def handle_cdata(self, data): + """XML Parser callback. Used internally""" + self.DEBUG(DBG_NODEBUILDER, data, 'data') + if self.last_is_data: + if self.data_buffer: + self.data_buffer.append(data) + elif self._ptr: + self.data_buffer = [data] + self.last_is_data = 1 + + def handle_namespace_start(self, prefix, uri): + """XML Parser callback. Used internally""" + self.check_data_buffer() + + def DEBUG(self, level, text, comment=None): + """ Gets all NodeBuilder walking events. Can be used for debugging if redefined.""" + def getDom(self): + """ Returns just built Node. """ + self.check_data_buffer() + return self._mini_dom + def dispatch(self,stanza): + """ Gets called when the NodeBuilder reaches some level of depth on it's way up with the built + node as argument. Can be redefined to convert incoming XML stanzas to program events. """ + def stream_header_received(self,ns,tag,attrs): + """ Method called when stream just opened. """ + self.check_data_buffer() + def stream_footer_received(self): + """ Method called when stream just closed. """ + self.check_data_buffer() + + def has_received_endtag(self, level=0): + """ Return True if at least one end tag was seen (at level) """ + return self.__depth <= level and self.__max_depth > level + + def _inc_depth(self): + self.__last_depth = self.__depth + self.__depth += 1 + self.__max_depth = max(self.__depth, self.__max_depth) + + def _dec_depth(self): + self.__last_depth = self.__depth + self.__depth -= 1 + +def XML2Node(xml): + """ Converts supplied textual string into XML node. Handy f.e. for reading configuration file. + Raises xml.parser.expat.parsererror if provided string is not well-formed XML. """ + return NodeBuilder(xml).getDom() + +def BadXML2Node(xml): + """ Converts supplied textual string into XML node. Survives if xml data is cutted half way round. + I.e. "<html>some text <br>some more text". Will raise xml.parser.expat.parsererror on misplaced + tags though. F.e. "<b>some text <br>some more text</b>" will not work.""" + return NodeBuilder(xml).getDom() diff --git a/src/xmpppy-0.5.0rc1/build/lib/xmpp/transports.py b/src/xmpppy-0.5.0rc1/build/lib/xmpp/transports.py new file mode 100644 index 00000000..0e3eec90 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/build/lib/xmpp/transports.py @@ -0,0 +1,339 @@ +## transports.py +## +## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: transports.py,v 1.35 2009/04/07 08:34:09 snakeru Exp $ + +""" +This module contains the low-level implementations of xmpppy connect methods or +(in other words) transports for xmpp-stanzas. +Currently here is three transports: +direct TCP connect - TCPsocket class +proxied TCP connect - HTTPPROXYsocket class (CONNECT proxies) +TLS connection - TLS class. Can be used for SSL connections also. + +Transports are stackable so you - f.e. TLS use HTPPROXYsocket or TCPsocket as more low-level transport. + +Also exception 'error' is defined to allow capture of this module specific exceptions. +""" + +import socket,select,base64,dispatcher,sys +from simplexml import ustr +from client import PlugIn +from protocol import * + +# determine which DNS resolution library is available +HAVE_DNSPYTHON = False +HAVE_PYDNS = False +try: + import dns.resolver # http://dnspython.org/ + HAVE_DNSPYTHON = True +except ImportError: + try: + import DNS # http://pydns.sf.net/ + HAVE_PYDNS = True + except ImportError: + pass + +DATA_RECEIVED='DATA RECEIVED' +DATA_SENT='DATA SENT' + +class error: + """An exception to be raised in case of low-level errors in methods of 'transports' module.""" + def __init__(self,comment): + """Cache the descriptive string""" + self._comment=comment + + def __str__(self): + """Serialise exception into pre-cached descriptive string.""" + return self._comment + +BUFLEN=1024 +class TCPsocket(PlugIn): + """ This class defines direct TCP connection method. """ + def __init__(self, server=None, use_srv=True): + """ Cache connection point 'server'. 'server' is the tuple of (host, port) + absolutely the same as standard tcp socket uses. However library will lookup for + ('_xmpp-client._tcp.' + host) SRV record in DNS and connect to the found (if it is) + server instead + """ + PlugIn.__init__(self) + self.DBG_LINE='socket' + self._exported_methods=[self.send,self.disconnect] + self._server, self.use_srv = server, use_srv + + def srv_lookup(self, server): + " SRV resolver. Takes server=(host, port) as argument. Returns new (host, port) pair " + if HAVE_DNSPYTHON or HAVE_PYDNS: + host, port = server + possible_queries = ['_xmpp-client._tcp.' + host] + + for query in possible_queries: + try: + if HAVE_DNSPYTHON: + answers = [x for x in dns.resolver.query(query, 'SRV')] + if answers: + host = str(answers[0].target) + port = int(answers[0].port) + break + elif HAVE_PYDNS: + # ensure we haven't cached an old configuration + DNS.DiscoverNameServers() + response = DNS.Request().req(query, qtype='SRV') + answers = response.answers + if len(answers) > 0: + # ignore the priority and weight for now + _, _, port, host = answers[0]['data'] + del _ + port = int(port) + break + except: + self.DEBUG('An error occurred while looking up %s' % query, 'warn') + server = (host, port) + else: + self.DEBUG("Could not load one of the supported DNS libraries (dnspython or pydns). SRV records will not be queried and you may need to set custom hostname/port for some servers to be accessible.\n",'warn') + # end of SRV resolver + return server + + def plugin(self, owner): + """ Fire up connection. Return non-empty string on success. + Also registers self.disconnected method in the owner's dispatcher. + Called internally. """ + if not self._server: self._server=(self._owner.Server,5222) + if self.use_srv: server=self.srv_lookup(self._server) + else: server=self._server + if not self.connect(server): return + self._owner.Connection=self + self._owner.RegisterDisconnectHandler(self.disconnected) + return 'ok' + + def getHost(self): + """ Return the 'host' value that is connection is [will be] made to.""" + return self._server[0] + def getPort(self): + """ Return the 'port' value that is connection is [will be] made to.""" + return self._server[1] + + def connect(self,server=None): + """ Try to connect to the given host/port. Does not lookup for SRV record. + Returns non-empty string on success. """ + try: + if not server: server=self._server + self._sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._sock.connect((server[0], int(server[1]))) + self._send=self._sock.sendall + self._recv=self._sock.recv + self.DEBUG("Successfully connected to remote host %s"%`server`,'start') + return 'ok' + except socket.error, (errno, strerror): + self.DEBUG("Failed to connect to remote host %s: %s (%s)"%(`server`, strerror, errno),'error') + except: pass + + def plugout(self): + """ Disconnect from the remote server and unregister self.disconnected method from + the owner's dispatcher. """ + self._sock.close() + if self._owner.__dict__.has_key('Connection'): + del self._owner.Connection + self._owner.UnregisterDisconnectHandler(self.disconnected) + + def receive(self): + """ Reads all pending incoming data. + In case of disconnection calls owner's disconnected() method and then raises IOError exception.""" + try: received = self._recv(BUFLEN) + except socket.sslerror,e: + self._seen_data=0 + if e[0]==socket.SSL_ERROR_WANT_READ: return '' + if e[0]==socket.SSL_ERROR_WANT_WRITE: return '' + self.DEBUG('Socket error while receiving data','error') + sys.exc_clear() + self._owner.disconnected() + raise IOError("Disconnected from server") + except: received = '' + + while self.pending_data(0): + try: add = self._recv(BUFLEN) + except: add='' + received +=add + if not add: break + + if len(received): # length of 0 means disconnect + self._seen_data=1 + self.DEBUG(received,'got') + if hasattr(self._owner, 'Dispatcher'): + self._owner.Dispatcher.Event('', DATA_RECEIVED, received) + else: + self.DEBUG('Socket error while receiving data','error') + self._owner.disconnected() + raise IOError("Disconnected from server") + return received + + def send(self,raw_data): + """ Writes raw outgoing data. Blocks until done. + If supplied data is unicode string, encodes it to utf-8 before send.""" + if type(raw_data)==type(u''): raw_data = raw_data.encode('utf-8') + elif type(raw_data)<>type(''): raw_data = ustr(raw_data).encode('utf-8') + try: + self._send(raw_data) + # Avoid printing messages that are empty keepalive packets. + if raw_data.strip(): + self.DEBUG(raw_data,'sent') + if hasattr(self._owner, 'Dispatcher'): # HTTPPROXYsocket will send data before we have a Dispatcher + self._owner.Dispatcher.Event('', DATA_SENT, raw_data) + except: + self.DEBUG("Socket error while sending data",'error') + self._owner.disconnected() + + def pending_data(self,timeout=0): + """ Returns true if there is a data ready to be read. """ + return select.select([self._sock],[],[],timeout)[0] + + def disconnect(self): + """ Closes the socket. """ + self.DEBUG("Closing socket",'stop') + self._sock.close() + + def disconnected(self): + """ Called when a Network Error or disconnection occurs. + Designed to be overidden. """ + self.DEBUG("Socket operation failed",'error') + +DBG_CONNECT_PROXY='CONNECTproxy' +class HTTPPROXYsocket(TCPsocket): + """ HTTP (CONNECT) proxy connection class. Uses TCPsocket as the base class + redefines only connect method. Allows to use HTTP proxies like squid with + (optionally) simple authentication (using login and password). """ + def __init__(self,proxy,server,use_srv=True): + """ Caches proxy and target addresses. + 'proxy' argument is a dictionary with mandatory keys 'host' and 'port' (proxy address) + and optional keys 'user' and 'password' to use for authentication. + 'server' argument is a tuple of host and port - just like TCPsocket uses. """ + TCPsocket.__init__(self,server,use_srv) + self.DBG_LINE=DBG_CONNECT_PROXY + self._proxy=proxy + + def plugin(self, owner): + """ Starts connection. Used interally. Returns non-empty string on success.""" + owner.debug_flags.append(DBG_CONNECT_PROXY) + return TCPsocket.plugin(self,owner) + + def connect(self,dupe=None): + """ Starts connection. Connects to proxy, supplies login and password to it + (if were specified while creating instance). Instructs proxy to make + connection to the target server. Returns non-empty sting on success. """ + if not TCPsocket.connect(self,(self._proxy['host'],self._proxy['port'])): return + self.DEBUG("Proxy server contacted, performing authentification",'start') + connector = ['CONNECT %s:%s HTTP/1.0'%self._server, + 'Proxy-Connection: Keep-Alive', + 'Pragma: no-cache', + 'Host: %s:%s'%self._server, + 'User-Agent: HTTPPROXYsocket/v0.1'] + if self._proxy.has_key('user') and self._proxy.has_key('password'): + credentials = '%s:%s'%(self._proxy['user'],self._proxy['password']) + credentials = base64.encodestring(credentials).strip() + connector.append('Proxy-Authorization: Basic '+credentials) + connector.append('\r\n') + self.send('\r\n'.join(connector)) + try: reply = self.receive().replace('\r','') + except IOError: + self.DEBUG('Proxy suddenly disconnected','error') + self._owner.disconnected() + return + try: proto,code,desc=reply.split('\n')[0].split(' ',2) + except: raise error('Invalid proxy reply') + if code<>'200': + self.DEBUG('Invalid proxy reply: %s %s %s'%(proto,code,desc),'error') + self._owner.disconnected() + return + while reply.find('\n\n') == -1: + try: reply += self.receive().replace('\r','') + except IOError: + self.DEBUG('Proxy suddenly disconnected','error') + self._owner.disconnected() + return + self.DEBUG("Authentification successfull. Jabber server contacted.",'ok') + return 'ok' + + def DEBUG(self,text,severity): + """Overwrites DEBUG tag to allow debug output be presented as "CONNECTproxy".""" + return self._owner.DEBUG(DBG_CONNECT_PROXY,text,severity) + +class TLS(PlugIn): + """ TLS connection used to encrypts already estabilished tcp connection.""" + def PlugIn(self,owner,now=0): + """ If the 'now' argument is true then starts using encryption immidiatedly. + If 'now' in false then starts encryption as soon as TLS feature is + declared by the server (if it were already declared - it is ok). + """ + if owner.__dict__.has_key('TLS'): return # Already enabled. + PlugIn.PlugIn(self,owner) + DBG_LINE='TLS' + if now: return self._startSSL() + if self._owner.Dispatcher.Stream.features: + try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) + except NodeProcessed: pass + else: self._owner.RegisterHandlerOnce('features',self.FeaturesHandler,xmlns=NS_STREAMS) + self.starttls=None + + def plugout(self,now=0): + """ Unregisters TLS handler's from owner's dispatcher. Take note that encription + can not be stopped once started. You can only break the connection and start over.""" + self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) + self._owner.UnregisterHandler('proceed',self.StartTLSHandler,xmlns=NS_TLS) + self._owner.UnregisterHandler('failure',self.StartTLSHandler,xmlns=NS_TLS) + + def FeaturesHandler(self, conn, feats): + """ Used to analyse server <features/> tag for TLS support. + If TLS is supported starts the encryption negotiation. Used internally""" + if not feats.getTag('starttls',namespace=NS_TLS): + self.DEBUG("TLS unsupported by remote server.",'warn') + return + self.DEBUG("TLS supported by remote server. Requesting TLS start.",'ok') + self._owner.RegisterHandlerOnce('proceed',self.StartTLSHandler,xmlns=NS_TLS) + self._owner.RegisterHandlerOnce('failure',self.StartTLSHandler,xmlns=NS_TLS) + self._owner.Connection.send('<starttls xmlns="%s"/>'%NS_TLS) + raise NodeProcessed + + def pending_data(self,timeout=0): + """ Returns true if there possible is a data ready to be read. """ + return self._tcpsock._seen_data or select.select([self._tcpsock._sock],[],[],timeout)[0] + + def _startSSL(self): + """ Immidiatedly switch socket to TLS mode. Used internally.""" + """ Here we should switch pending_data to hint mode.""" + tcpsock=self._owner.Connection + tcpsock._sslObj = socket.ssl(tcpsock._sock, None, None) + tcpsock._sslIssuer = tcpsock._sslObj.issuer() + tcpsock._sslServer = tcpsock._sslObj.server() + tcpsock._recv = tcpsock._sslObj.read + tcpsock._send = tcpsock._sslObj.write + + tcpsock._seen_data=1 + self._tcpsock=tcpsock + tcpsock.pending_data=self.pending_data + tcpsock._sock.setblocking(0) + + self.starttls='success' + + def StartTLSHandler(self, conn, starttls): + """ Handle server reply if TLS is allowed to process. Behaves accordingly. + Used internally.""" + if starttls.getNamespace()<>NS_TLS: return + self.starttls=starttls.getName() + if self.starttls=='failure': + self.DEBUG("Got starttls response: "+self.starttls,'error') + return + self.DEBUG("Got starttls proceed response. Switching to TLS/SSL...",'ok') + self._startSSL() + self._owner.Dispatcher.PlugOut() + dispatcher.Dispatcher().PlugIn(self._owner) diff --git a/src/xmpppy-0.5.0rc1/doc/advanced.html b/src/xmpppy-0.5.0rc1/doc/advanced.html new file mode 100644 index 00000000..4766eff0 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/doc/advanced.html @@ -0,0 +1,116 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<!-- $Id: advanced.html,v 1.2 2004/12/26 08:12:41 snakeru Exp $ --> +<html xml:lang="ru-RU" lang="ru-RU" xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta content="text/html; charset=koi8-r" http-equiv="content-type" /> + <title>Xmpppy usage - advanced.</title> + </head> +<!-- +Historical notes +Intro +simplexml +Dispatcher +--> + <body> + <h1>Introduction</h1> + <p>To write a programs using XMPP technology you must understand the basic + principles of it. Xmpppy uses it's own implementation of XML handling + procedures - so you should get used to it. Though it is simple enough I hope.</p> + <dl> + <dn>Node class</dn> + <dt> + <dl> + <dn>prototype</dn> + <dt>Node.__init__(name='', attrs={}, payload=[], parent=None, node=None)</dt> + <dn>Note that 'name' argument really consists of namespace and node name, space separated. Example:</dn> + <dt>node=Node('jabber:client message', attrs={'to':'target@jid.com'},payload=[Node('body',payload=['Hello target!'])])<br /> + or<br /> + node=Node('jabber:client message')<br /> + node['to']='target@jid.com'<br /> + node.NT.body='Hello target!' + </dt> + </dl> + NT stands for 'New Tag' and explicitly adds new child to the current node. + Also the T can be used. That means "find Tag" but if tag exists it acts just like NT otherwise. + </dt> + + <dn>Protocol class</dn> + <dt> + Uses similar syntax. We will use 'node' attribute now: + <dl> + <dn>prototype</dn> + <dt>Protocol.__init__(name=None, to=None, typ=None, frm=None, attrs={}, payload=[], timestamp=None, xmlns='jabber:client', node=None)</dt> + <dn>example</dn> + <dt>p=Protocol(node=node)<br /> + or<br /> + proto=Protocol('message',to='target@jid.com',payload=[Node('body',payload=['Hello target!'])])<br /> + or<br /> + proto=Protocol('message',to='target@jid.com')<br /> + proto.NT.body='Hello target!' + </dt> + </dl> + </dt> + + <dn>Message class</dn> + <dt> + Similar syntax: + <dl> + <dn>prototype</dn> + <dt>Message.__init__(to=None, body=None, typ=None, subject=None, attrs={}, frm=None, payload=[], timestamp=None, xmlns='jabber:client', node=None)</dt> + <dn>example</dn> + <dt>m=Message(node=proto)<br /> + or<br /> + m=Message('target@jid.com','Hello target!')<br /> + </dt> + </dl> + </dt> + + <dn>Iq class</dn> + <dt> + Similar syntax: + <dl> + <dn>prototype</dn> + <dt>Iq.__init__(typ=None, queryNS=None, attrs={}, to=None, frm=None, payload=[], xmlns='jabber:client', node=None)</dt> + <dn>example</dn> + <dt>iq=Iq('set',NS_AUTH,payload=[Node('username',payload=['user']),Node('password',payload=['secret'])])<br /> + or<br /> + iq=Iq('set',NS_AUTH)<br /> + iq.T.query.NT.username='user'<br /> + iq.T.query.NT.password='secret'<br /> + or<br /> + iq=Iq('set',NS_AUTH)<br /> + iq.T.query.T.username='user'<br /> + iq.T.query.T.password='secret'<br /> + As I already noted - 'T' acts just like 'NT' if tag doesn't exists. + </dt> + </dl> + </dt> + + <dn>Presence class</dn> + <dt> + Similar syntax: + <dl> + <dn>prototype</dn> + <dt>Presence.__init__(to=None, typ=None, priority=None, show=None, status=None, attrs={}, frm=None, timestamp=None, payload=[], xmlns='jabber:client', node=None)</dt> + <dn>example</dn> + <dt>pres=Presence(priority=5, show='xa',status="I'm away from my computer")<br /> + or<br /> + pres=Presence()<br /> + pres.setPriority(5) + pres.setShow('xa') + pres.setStatus("I'm away from my computer") + pres.setTimestamp() + or<br /> + pres=Presence()<br /> + pres.T.priority=5 + pres.T.show='xa' + pres.T.status="I'm away from my computer" + pres.setTimestamp() + </dt> + </dl> + </dt> + + </dl> + </body> +</html> diff --git a/src/xmpppy-0.5.0rc1/doc/basic.html b/src/xmpppy-0.5.0rc1/doc/basic.html new file mode 100644 index 00000000..86259d90 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/doc/basic.html @@ -0,0 +1,111 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xml:lang="ru-RU" lang="ru-RU" xmlns="http://www.w3.org/1999/xhtml"> +<!-- $Id: basic.html,v 1.5 2004/12/26 08:12:42 snakeru Exp $ --> + <head> + <meta content="text/html; charset=koi8-r" http-equiv="content-type" /> + <title>Xmpppy usage - basics.</title> + </head> + <body> +<!-- +basic +ïÂßÑÓÎÑÅÔÓÑ ÐÒÉÎÃÉÐÙ ÐÒÏÓÔÅÊÛÅÇÏ ÓËÒÉÐÔÏ×ÁÎÉÑ. æÁËÔÉÞÅÓËÉ ÕÒÏ×ÅÎØ README.py. +advanced +÷ÏÌØÎÏÅ ÉÚÌÏÖÅÎÉÅ ÐÒÅÄÍÅÔÁ +expert +ÏÐÉÓÁÎÉÅ API ËÁÖÄÏÇÏ ÍÏÄÕÌÑ +--> +<h1>Preface.</h1> +<p>English is not my native language. If you see any bugs in this text, please, let me know.</p> +<h1>Basic</h1> +<h2>Introduction.</h2> +<p>This documents topic is for people who want to quickly try xmpppy for a simple task, like +writing a command-line script for sending a single message.</p> +<h2>Writing a simple script</h2> +<p>This example demonstrates a simple script that sends message to one +recipient. +Example:</p> +<pre> +xsend test@jabber.org Hello there! +</pre> +<p>You don't have a similar tool in your toolkit? Using the xmpppy +library it can be created easily. +<br />What? Already have one? Hmm. Maybe you want to simplify things a bit, or just curious how +to do it one more way? Anyway - let's start now! +<br /> +First - we declare ourself as a python script and importing needed modules:</p> +<pre> +#!/usr/bin/python +import sys,os,xmpp +</pre> +<p>After it we have to check if we have enough arguments on the command-line:</p> +<pre> +if len(sys.argv) < 2: + print "Syntax: xsend JID text" + sys.exit(0) +</pre> +<p>After it we must decode arguments. Omitting all checks to simplify our script:</p> +<pre> +tojid=sys.argv[1] +text=' '.join(sys.argv[2:]) +</pre> +<p>One more non-jabber step: We have to to get our Jabber ID and login +details. Presuming that all info +stored in ~/.xsend file:</p> +<pre> +jidparams={} +if os.access(os.environ['HOME']+'/.xsend',os.R_OK): + for ln in open(os.environ['HOME']+'/.xsend').readlines(): + key,val=ln.strip().split('=',1) + jidparams[key.lower()]=val +for mandatory in ['jid','password']: + if mandatory not in jidparams.keys(): + open(os.environ['HOME']+'/.xsend','w').write('#JID=romeo@montague.net\n#PASSWORD=juliet\n') + print 'Please ensure the ~/.xsend file has valid JID for sending messages.' + sys.exit(0) +</pre> +<p>Phew! The most complex (non-jabber ;) ) part is finished. From now on we have to:</p> +<ul> + <li>connect to jabber server</li> + <li>authenticate ourselves</li> + <li>submit a message</li> +</ul> +<p>Let's start: +<br /> +0. To connect we must have a client instance. Calculating our server name and +creating Client class instance for it:</p> +<pre> +jid=xmpp.protocol.JID(jidparams['jid']) +cl=xmpp.Client(jid.getDomain(),debug=[]) +</pre> +<p>1. Connect and authenticate with credentials from the config file.</p> +<pre> +cl.connect() +cl.auth(jid.getNode(),jidparams['password']) +</pre> +<p>2. We can go online now (by sending the inital presence) but will not do that, as it is not nessessary for sending a message. +So we send a message now!</p> +<pre> +#cl.sendInitialPresence() +cl.send(xmpp.protocol.Message(tojid,text)) +</pre> +<p>We're done! The session must now be closed but since we have not registered +disconnect handler we will just leave it to python and TCP/IP layer. +All jabber servers that I know handle <span style="font-style: italic;">such</span> +disconnects correctly. +<br /> +You can download this script <a href="examples/xsend.py">here</a>.</p> +<h3>What now?</h3> +<p>If you were impressed of how the things were done with xmpppy, you may be interested in +more thorough examination of xmpppy library. The "advanced" and "expert" +parts of this document are here to help you. +<br /> +"<a href="advanced.html">Advanced</a>" (isn't writed yet) part is much like another tutorial and +describing common principles of XMPP usage via xmpppy prism. It describes ideas that are the foundation of XML handling with the +simplexml library, the essence of dispatcher's work and how messages are processed, and +some guidelines to write more complex programs and uses of the library. +<br /> +"<a href="apidocs/">Expert</a>" part is full library API description documentation. +This is epydoc generated output - all info is taken from the xmpppy code so you can re-generate it at any time. +</p> +</body> +</html> diff --git a/src/xmpppy-0.5.0rc1/doc/examples/README.py b/src/xmpppy-0.5.0rc1/doc/examples/README.py new file mode 100755 index 00000000..ae01dc62 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/doc/examples/README.py @@ -0,0 +1,71 @@ +#!/usr/bin/python +# -*- coding: koi8-r -*- +from xmpp import * + +def presenceHandler(conn,presence_node): + """ Handler for playing a sound when particular contact became online """ + targetJID='node@domain.org' + if presence_node.getFrom().bareMatch(targetJID): + # play a sound + pass +def iqHandler(conn,iq_node): + """ Handler for processing some "get" query from custom namespace""" + reply=iq_node.buildReply('result') + # ... put some content into reply node + conn.send(reply) + raise NodeProcessed # This stanza is fully processed +def messageHandler(conn,mess_node): pass + +if 1: + """ + Example 1: + Connecting to specified IP address. + Connecting to port 5223 - TLS is pre-started. + Using direct connect. + """ + # Born a client + cl=Client('ejabberd.somedomain.org') + # ...connect it to SSL port directly + if not cl.connect(server=('1.2.3.4',5223)): + raise IOError('Can not connect to server.') +else: + """ + Example 2: + Connecting to server via proxy. + Assuming that servername resolves to jabber server IP. + TLS will be started automatically if available. + """ + # Born a client + cl=Client('jabberd2.somedomain.org') + # ...connect via proxy + if not cl.connect(proxy={'host':'someproxy.somedomain.org','port':'8080','user':'proxyuser','password':'proxyuserpassword'}): + raise IOError('Can not connect to server.') +# ...authorize client +if not cl.auth('jabberuser','jabberuserpassword','optional resource name'): + raise IOError('Can not auth with server.') +# ...register some handlers (if you will register them before auth they will be thrown away) +cl.RegisterHandler('presence',presenceHandler) +cl.RegisterHandler('iq',iqHandler) +cl.RegisterHandler('message',messageHandler) +# ...become available +cl.sendInitPresence() +# ...work some time +cl.Process(1) +# ...if connection is brocken - restore it +if not cl.isConnected(): cl.reconnectAndReauth() +# ...send an ASCII message +cl.send(Message('test@jabber.org','Test message')) +# ...send a national message +cl.send(Message('test@jabber.org',unicode('ðÒÏ×ÅÒËÁ Ó×ÑÚÉ','koi8-r'))) +# ...send another national message +simplexml.ENCODING='koi8-r' +cl.send(Message('test@jabber.org','ðÒÏ×ÅÒËÁ Ó×ÑÚÉ 2')) +# ...work some more time - collect replies +cl.Process(1) +# ...and then disconnect. +cl.disconnect() + +""" +If you have used jabberpy before you will find xmpppy very similar. +See the docs for more info about library features. +""" diff --git a/src/xmpppy-0.5.0rc1/doc/examples/bot.py b/src/xmpppy-0.5.0rc1/doc/examples/bot.py new file mode 100755 index 00000000..42636300 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/doc/examples/bot.py @@ -0,0 +1,94 @@ +#!/usr/bin/python +# -*- coding: koi8-r -*- +# $Id: bot.py,v 1.2 2006/10/06 12:30:42 normanr Exp $ +import sys +import xmpp + +commands={} +i18n={'ru':{},'en':{}} +########################### user handlers start ################################## +i18n['en']['HELP']="This is example jabber bot.\nAvailable commands: %s" +def helpHandler(user,command,args,mess): + lst=commands.keys() + lst.sort() + return "HELP",', '.join(lst) + +i18n['en']['EMPTY']="%s" +i18n['en']['HOOK1']='Responce 1: %s' +def hook1Handler(user,command,args,mess): + return "HOOK1",'You requested: %s'%args + +i18n['en']['HOOK2']='Responce 2: %s' +def hook2Handler(user,command,args,mess): + return "HOOK2","hook2 called with %s"%(`(user,command,args,mess)`) + +i18n['en']['HOOK3']='Responce 3: static string' +def hook3Handler(user,command,args,mess): + return "HOOK3"*int(args) +########################### user handlers stop ################################### +############################ bot logic start ##################################### +i18n['en']["UNKNOWN COMMAND"]='Unknown command "%s". Try "help"' +i18n['en']["UNKNOWN USER"]="I do not know you. Register first." + +def messageCB(conn,mess): + text=mess.getBody() + user=mess.getFrom() + user.lang='en' # dup + if text.find(' ')+1: command,args=text.split(' ',1) + else: command,args=text,'' + cmd=command.lower() + + if commands.has_key(cmd): reply=commands[cmd](user,command,args,mess) + else: reply=("UNKNOWN COMMAND",cmd) + + if type(reply)==type(()): + key,args=reply + if i18n[user.lang].has_key(key): pat=i18n[user.lang][key] + elif i18n['en'].has_key(key): pat=i18n['en'][key] + else: pat="%s" + if type(pat)==type(''): reply=pat%args + else: reply=pat(**args) + else: + try: reply=i18n[user.lang][reply] + except KeyError: + try: reply=i18n['en'][reply] + except KeyError: pass + if reply: conn.send(xmpp.Message(mess.getFrom(),reply)) + +for i in globals().keys(): + if i[-7:]=='Handler' and i[:-7].lower()==i[:-7]: commands[i[:-7]]=globals()[i] + +############################# bot logic stop ##################################### + +def StepOn(conn): + try: + conn.Process(1) + except KeyboardInterrupt: return 0 + return 1 + +def GoOn(conn): + while StepOn(conn): pass + +if len(sys.argv)<3: + print "Usage: bot.py username@server.net password" +else: + jid=xmpp.JID(sys.argv[1]) + user,server,password=jid.getNode(),jid.getDomain(),sys.argv[2] + + conn=xmpp.Client(server)#,debug=[]) + conres=conn.connect() + if not conres: + print "Unable to connect to server %s!"%server + sys.exit(1) + if conres<>'tls': + print "Warning: unable to estabilish secure connection - TLS failed!" + authres=conn.auth(user,password) + if not authres: + print "Unable to authorize on %s - check login/password."%server + sys.exit(1) + if authres<>'sasl': + print "Warning: unable to perform SASL auth os %s. Old authentication method used!"%server + conn.RegisterHandler('message',messageCB) + conn.sendInitPresence() + print "Bot started." + GoOn(conn) diff --git a/src/xmpppy-0.5.0rc1/doc/examples/commandsbot.py b/src/xmpppy-0.5.0rc1/doc/examples/commandsbot.py new file mode 100644 index 00000000..f32c13c6 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/doc/examples/commandsbot.py @@ -0,0 +1,289 @@ +#!/usr/bin/python +""" The example of using xmpppy's Ad-Hoc Commands (JEP-0050) implementation. +""" +import xmpp +from xmpp.protocol import * + +options = { + 'JID': 'circles@example.com', + 'Password': '********', +} + +class TestCommand(xmpp.commands.Command_Handler_Prototype): + """ Example class. You should read source if you wish to understate how it works. This one + actually does some calculations.""" + name = 'testcommand' + description = 'Circle calculations' + def __init__(self, jid=''): + """ Initialize some internals. Set the first request handler to self.calcTypeForm. + """ + xmpp.commands.Command_Handler_Prototype.__init__(self,jid) + self.initial = { + 'execute': self.initialForm + } + + def initialForm(self, conn, request): + """ Assign a session id and send the first form. """ + sessionid = self.getSessionID() + self.sessions[sessionid] = { + 'jid':request.getFrom(), + 'data':{'type':None} + } + + # simulate that the client sent sessionid, so calcTypeForm will be able + # to continue + request.getTag(name="command").setAttr('sessionid', sessionid) + + return self.calcTypeForm(conn, request) + + def calcTypeForm(self, conn, request): + """ Send first form to the requesting user. """ + # get the session data + sessionid = request.getTagAttr('command','sessionid') + session = self.sessions[sessionid] + + # What to do when a user sends us a response? Note, that we should always + # include 'execute', as it is a default action when requester does not send + # exact action to do (should be set to the same as 'next' or 'complete' fields) + session['actions'] = { + 'cancel': self.cancel, + 'next': self.calcTypeFormAccept, + 'execute': self.calcTypeFormAccept, + } + + # The form to send + calctypefield = xmpp.DataField( + name='calctype', + desc='Calculation Type', + value=session['data']['type'], + options=[ + ['Calculate the diameter of a circle','circlediameter'], + ['Calculate the area of a circle','circlearea'] + ], + typ='list-single', + required=1) + + # We set label attribute... seems that the xmpppy.DataField cannot do that + calctypefield.setAttr('label', 'Calculation Type') + + form = xmpp.DataForm( + title='Select type of operation', + data=[ + 'Use the combobox to select the type of calculation you would like'\ + 'to do, then click Next.', + calctypefield]) + + # Build a reply with the form + reply = request.buildReply('result') + replypayload = [ + xmpp.Node('actions', + attrs={'execute':'next'}, + payload=[xmpp.Node('next')]), + form] + reply.addChild( + name='command', + namespace=NS_COMMANDS, + attrs={ + 'node':request.getTagAttr('command','node'), + 'sessionid':sessionid, + 'status':'executing'}, + payload=replypayload) + self._owner.send(reply) # Question: self._owner or conn? + raise xmpp.NodeProcessed + + def calcTypeFormAccept(self, conn, request): + """ Load the calcType form filled in by requester, then reply with + the second form. """ + # get the session data + sessionid = request.getTagAttr('command','sessionid') + session = self.sessions[sessionid] + + # load the form + node = request.getTag(name='command').getTag(name='x',namespace=NS_DATA) + form = xmpp.DataForm(node=node) + + # retrieve the data + session['data']['type'] = form.getField('calctype').getValue() + + # send second form + return self.calcDataForm(conn, request) + + def calcDataForm(self, conn, request, notavalue=None): + """ Send a form asking for diameter. """ + # get the session data + sessionid = request.getTagAttr('command','sessionid') + session = self.sessions[sessionid] + + # set the actions taken on requester's response + session['actions'] = { + 'cancel': self.cancel, + 'prev': self.calcTypeForm, + 'next': self.calcDataFormAccept, + 'execute': self.calcDataFormAccept + } + + # create a form + radiusfield = xmpp.DataField(desc='Radius',name='radius',typ='text-single') + radiusfield.setAttr('label', 'Radius') + + form = xmpp.DataForm( + title = 'Enter the radius', + data=[ + 'Enter the radius of the circle (numbers only)', + radiusfield]) + + # build a reply stanza + reply = request.buildReply('result') + replypayload = [ + xmpp.Node('actions', + attrs={'execute':'complete'}, + payload=[xmpp.Node('complete'),xmpp.Node('prev')]), + form] + + if notavalue: + replypayload.append(xmpp.Node('note', + attrs={'type': 'warn'}, + payload=['You have to enter valid number.'])) + + reply.addChild( + name='command', + namespace=NS_COMMANDS, + attrs={ + 'node':request.getTagAttr('command','node'), + 'sessionid':request.getTagAttr('command','sessionid'), + 'status':'executing'}, + payload=replypayload) + + self._owner.send(reply) + raise xmpp.NodeProcessed + + def calcDataFormAccept(self, conn, request): + """ Load the calcType form filled in by requester, then reply with the result. """ + # get the session data + sessionid = request.getTagAttr('command','sessionid') + session = self.sessions[sessionid] + + # load the form + node = request.getTag(name='command').getTag(name='x',namespace=NS_DATA) + form = xmpp.DataForm(node=node) + + # retrieve the data; if the entered value is not a number, return to second stage + try: + value = float(form.getField('radius').getValue()) + except: + self.calcDataForm(conn, request, notavalue=True) + + # calculate the answer + from math import pi + if session['data']['type'] == 'circlearea': + result = (value**2) * pi + else: + result = 2 * value * pi + + # build the result form + form = xmpp.DataForm( + typ='result', + data=[xmpp.DataField(desc='result', name='result', value=result)]) + + # build the reply stanza + reply = request.buildReply('result') + reply.addChild( + name='command', + namespace=NS_COMMANDS, + attrs={ + 'node':request.getTagAttr('command','node'), + 'sessionid':sessionid, + 'status':'completed'}, + payload=[form]) + + self._owner.send(reply) + + # erase the data about session + del self.sessions[sessionid] + + raise xmpp.NodeProcessed + + def cancel(self, conn, request): + """ Requester canceled the session, send a short reply. """ + # get the session id + sessionid = request.getTagAttr('command','sessionid') + + # send the reply + reply = request.buildReply('result') + reply.addChild( + name='command', + namespace=NS_COMMANDS, + attrs={ + 'node':request.getTagAttr('command','node'), + 'sessionid':sessionid, + 'status':'cancelled'}) + self._owner.send(reply) + + # erase the data about session + del self.sessions[sessionid] + + raise xmpp.NodeProcessed + +class ConnectionError: pass +class AuthorizationError: pass +class NotImplemented: pass + +class Bot: + """ The main bot class. """ + + def __init__(self, JID, Password): + """ Create a new bot. Connect to the server and log in. """ + + # connect... + jid = xmpp.JID(JID) + self.connection = xmpp.Client(jid.getDomain(), debug=['always', 'browser', 'testcommand']) + + result = self.connection.connect() + + if result is None: + raise ConnectionError + + # authorize + result = self.connection.auth(jid.getNode(), Password) + + if result is None: + raise AuthorizationError + + # plugins + # disco - needed by commands + + # warning: case of "plugin" method names are important! + # to attach a command to Commands class, use .plugin() + # to attach anything to Client class, use .PlugIn() + self.disco = xmpp.browser.Browser() + self.disco.PlugIn(self.connection) + self.disco.setDiscoHandler({ + 'info': { + 'ids': [{ + 'category': 'client', + 'type': 'pc', + 'name': 'Bot' + }], + 'features': [NS_DISCO_INFO], + } + }) + + self.commands = xmpp.commands.Commands(self.disco) + self.commands.PlugIn(self.connection) + + self.command_test = TestCommand() + self.command_test.plugin(self.commands) + + # presence + self.connection.sendInitPresence(requestRoster=0) + + def loop(self): + """ Do nothing except handling new xmpp stanzas. """ + try: + while self.connection.Process(1): + pass + except KeyboardInterrupt: + pass + +bot = Bot(**options) +bot.loop() diff --git a/src/xmpppy-0.5.0rc1/doc/examples/logger.py b/src/xmpppy-0.5.0rc1/doc/examples/logger.py new file mode 100755 index 00000000..b99686c1 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/doc/examples/logger.py @@ -0,0 +1,75 @@ +#!/usr/bin/python +# -*- coding: koi8-r -*- +from xmpp import * +import time,os + +#BOT=(botjid,password) +BOT=('test@penza-gsm.ru','test') +#CONF=(confjid,password) +CONF=('talks@conference.jabber.ru','') +LOGDIR='./' +PROXY={} +#PROXY={'host':'192.168.0.1','port':3128,'username':'luchs','password':'secret'} +####################################### + +def LOG(stanza,nick,text): + ts=stanza.getTimestamp() + if not ts: + ts=stanza.setTimestamp() + ts=stanza.getTimestamp() + tp=time.mktime(time.strptime(ts,'%Y%m%dT%H:%M:%S %Z'))+3600*3 + if time.localtime()[-1]: tp+=3600 + tp=time.localtime(tp) + fold=stanza.getFrom().getStripped().replace('@','%')+'_'+time.strftime("%Y.%m",tp) + day=time.strftime("%d",tp) + tm=time.strftime("%H:%M:%S",tp) + try: os.mkdir(LOGDIR+fold) + except: pass + fName='%s%s/%s.%s.html'%(LOGDIR,fold,fold,day) + try: open(fName) + except: + open(fName,'w').write("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xml:lang="ru-RU" lang="ru-RU" xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta content="text/html; charset=utf-8" http-equiv="content-type" /> + <title>%s logs for %s.%s.</title> + </head> + <body> +<table border="1"><tr><th>time</th><th>who</th><th>text</th></tr> +"""%(CONF[0],fold,day)) + text='<pre>%s</pre>'%text + open(fName,'a').write((u"<tr><td>%s</td><td>%s</td><td>%s</td></tr>\n"%(tm,nick,text)).encode('utf-8')) + print (u"<tr><td>%s</td><td>%s</td><td>%s</td></tr>\n"%(tm,nick,text)).encode('koi8-r','replace') +# print time.localtime(tp),nick,text + +def messageCB(sess,mess): + nick=mess.getFrom().getResource() + text=mess.getBody() + LOG(mess,nick,text) + +roster=[] +def presenceCB(sess,pres): + nick=pres.getFrom().getResource() + text='' + if pres.getType()=='unavailable': + if nick in roster: + text=nick+unicode(' ÐÏËÉÎÕÌ ËÏÎÆÅÒÅÎÃÉÀ','koi8-r') + roster.remove(nick) + else: + if nick not in roster: + text=nick+unicode(' ÐÒÉÛ£Ì × ËÏÎÆÅÒÅÎÃÉÀ','koi8-r') + roster.append(nick) + if text: LOG(pres,nick,text) + +if 1: + cl=Client(JID(BOT[0]).getDomain(),debug=[]) + cl.connect(proxy=PROXY) + cl.RegisterHandler('message',messageCB) + cl.RegisterHandler('presence',presenceCB) + cl.auth(JID(BOT[0]).getNode(),BOT[1]) + p=Presence(to='%s/logger'%CONF[0]) + p.setTag('x',namespace=NS_MUC).setTagData('password',CONF[1]) + p.getTag('x').addChild('history',{'maxchars':'0','maxstanzas':'0'}) + cl.send(p) + while 1: + cl.Process(1) diff --git a/src/xmpppy-0.5.0rc1/doc/examples/xsend.py b/src/xmpppy-0.5.0rc1/doc/examples/xsend.py new file mode 100755 index 00000000..59b202a9 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/doc/examples/xsend.py @@ -0,0 +1,44 @@ +#!/usr/bin/python +# $Id: xsend.py,v 1.8 2006/10/06 12:30:42 normanr Exp $ +import sys,os,xmpp,time + +if len(sys.argv) < 2: + print "Syntax: xsend JID text" + sys.exit(0) + +tojid=sys.argv[1] +text=' '.join(sys.argv[2:]) + +jidparams={} +if os.access(os.environ['HOME']+'/.xsend',os.R_OK): + for ln in open(os.environ['HOME']+'/.xsend').readlines(): + if not ln[0] in ('#',';'): + key,val=ln.strip().split('=',1) + jidparams[key.lower()]=val +for mandatory in ['jid','password']: + if mandatory not in jidparams.keys(): + open(os.environ['HOME']+'/.xsend','w').write('#Uncomment fields before use and type in correct credentials.\n#JID=romeo@montague.net/resource (/resource is optional)\n#PASSWORD=juliet\n') + print 'Please point ~/.xsend config file to valid JID for sending messages.' + sys.exit(0) + +jid=xmpp.protocol.JID(jidparams['jid']) +cl=xmpp.Client(jid.getDomain(),debug=[]) + +con=cl.connect() +if not con: + print 'could not connect!' + sys.exit() +print 'connected with',con +auth=cl.auth(jid.getNode(),jidparams['password'],resource=jid.getResource()) +if not auth: + print 'could not authenticate!' + sys.exit() +print 'authenticated using',auth + +#cl.SendInitPresence(requestRoster=0) # you may need to uncomment this for old server +id=cl.send(xmpp.protocol.Message(tojid,text)) +print 'sent message with id',id + +time.sleep(1) # some older servers will not send the message if you disconnect immediately after sending + +#cl.disconnect() diff --git a/src/xmpppy-0.5.0rc1/doc/examples/xtalk.py b/src/xmpppy-0.5.0rc1/doc/examples/xtalk.py new file mode 100644 index 00000000..8832875f --- /dev/null +++ b/src/xmpppy-0.5.0rc1/doc/examples/xtalk.py @@ -0,0 +1,83 @@ +#!/usr/bin/python +# $Id: xtalk.py,v 1.4 2008/08/09 17:00:18 normanr Exp $ +import sys,os,xmpp,time,select + +class Bot: + + def __init__(self,jabber,remotejid): + self.jabber = jabber + self.remotejid = remotejid + + def register_handlers(self): + self.jabber.RegisterHandler('message',self.xmpp_message) + + def xmpp_message(self, con, event): + type = event.getType() + fromjid = event.getFrom().getStripped() + body = event.getBody() + if type in ['message', 'chat', None] and fromjid == self.remotejid and body: + sys.stdout.write(body + '\n') + + def stdio_message(self, message): + m = xmpp.protocol.Message(to=self.remotejid,body=message,typ='chat') + self.jabber.send(m) + + def xmpp_connect(self): + con=self.jabber.connect() + if not con: + sys.stderr.write('could not connect!\n') + return False + sys.stderr.write('connected with %s\n'%con) + auth=self.jabber.auth(jid.getNode(),jidparams['password'],resource=jid.getResource()) + if not auth: + sys.stderr.write('could not authenticate!\n') + return False + sys.stderr.write('authenticated using %s\n'%auth) + self.register_handlers() + return con + +if __name__ == '__main__': + + if len(sys.argv) < 2: + print "Syntax: xtalk JID" + sys.exit(0) + + tojid=sys.argv[1] + + jidparams={} + if os.access(os.environ['HOME']+'/.xtalk',os.R_OK): + for ln in open(os.environ['HOME']+'/.xtalk').readlines(): + if not ln[0] in ('#',';'): + key,val=ln.strip().split('=',1) + jidparams[key.lower()]=val + for mandatory in ['jid','password']: + if mandatory not in jidparams.keys(): + open(os.environ['HOME']+'/.xtalk','w').write('#Uncomment fields before use and type in correct credentials.\n#JID=romeo@montague.net/resource (/resource is optional)\n#PASSWORD=juliet\n') + print 'Please point ~/.xtalk config file to valid JID for sending messages.' + sys.exit(0) + + jid=xmpp.protocol.JID(jidparams['jid']) + cl=xmpp.Client(jid.getDomain())#,debug=[]) + + bot=Bot(cl,tojid) + + if not bot.xmpp_connect(): + sys.stderr.write("Could not connect to server, or password mismatch!\n") + sys.exit(1) + + #cl.SendInitPresence(requestRoster=0) # you may need to uncomment this for old server + + socketlist = {cl.Connection._sock:'xmpp',sys.stdin:'stdio'} + online = 1 + + while online: + (i , o, e) = select.select(socketlist.keys(),[],[],1) + for each in i: + if socketlist[each] == 'xmpp': + cl.Process(1) + elif socketlist[each] == 'stdio': + msg = sys.stdin.readline().rstrip('\r\n') + bot.stdio_message(msg) + else: + raise Exception("Unknown socket type: %s" % repr(socketlist[each])) + #cl.disconnect() diff --git a/src/xmpppy-0.5.0rc1/doc/index.html b/src/xmpppy-0.5.0rc1/doc/index.html new file mode 100644 index 00000000..583c72a0 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/doc/index.html @@ -0,0 +1,166 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE html PUBLIC + "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>xmpppy: the jabber python project</title> + <link rel="stylesheet" type="text/css" href="xmpppy.css" /> + </head> + <body> + <table class="head"> + <tbody> + <tr> + <td class="head"> + <img src="xmpppy_title.png" alt="xmpppy" title="the xmpppy project" height="69" width="300" /> + </td> + <td class="sflogo"> + <a href="http://sourceforge.net/"> + <img src="http://sourceforge.net/sflogo.php?group_id=97081&type=4" alt="SourceForge Logo" title="SourceForge Logo" border="0" height="37" hspace="30" width="125" /> + </a> + </td> + </tr> + </tbody> + </table> + <table class="content"> + <tbody> + <tr> + <td class="leftside"> + <h3>about</h3> + <p><a href="http://xmpppy.sourceforge.net/">xmpppy</a> is a + <a href="http://www.python.org">Python</a> library + that is targeted to provide easy scripting with <a href="http://www.jabber.org">Jabber</a>. + Similar projects are <a href="http://twistedmatrix.com/projects/words/">Twisted Words</a> + and <a href="http://jabberpy.sourceforge.net/">jabber.py.</a></p> + <p>This library was not designed from scratch. It inherits some code from + jabberpy and have very similar API in many places. Though it is separate + project since it have almost completely different architecture and primarily + aims to work with jabberd2 - the new Open Source Jabber Server.</p> + <p>xmpppy is distributed under the terms of + <a href="http://www.gnu.org/licenses/gpl.txt">GNU General Public License</a> + and can be freely redistributed without any charge.</p> + <h3>documentation</h3> + <p>Documentation is now in the process of heavy development and not yet + finished but most critical docs exist - please feel free to ask any questions if + you will find the docs incomplete (see support section below).</p> + <div> + <ul> + <li><a href="basic.html">Basic documentation</a> - a simple script.</li> + <li><a href="advanced.html">Advanced documentation</a> - architecture of library and guidelines.</li> + <li><a href="apidocs/index.html">Expert documentation</a> - API docs.</li> + </ul> + </div> + <h3>examples</h3> + <p>For these who prefer reading samples of code than digging through [incomplete] docs - + here they are. Simple (but working) examples of xmpppy usage.</p> + <div> + <ul> + <li><a href="examples/README.py">README.py</a><br /> + Self-explanatory library usage example</li> + <li><a href="examples/xsend.py">xsend.py</a><br /> + Command-line utility for sending jabber messages</li> + <li><a href="examples/xtalk.py">xtalk.py</a><br /> + Command-line utility for chatting with a single user</li> + <li><a href="examples/bot.py">bot.py</a><br /> + Xmpppy bot framework, handles messages</li> + <li><a href="examples/commandsbot.py">commandsbot.py</a><br /> + Xmpppy bot framework, handles ad hoc commands</li> + <li><a href="examples/logger.py">logger.py</a><br /> + Simple conference logger bot</li> + </ul> + </div> + <p>You can also look to at the <a href="http://xmpppy.sourceforge.net/irc/">IRC transport</a>, + <a href="http://xmpppy.sourceforge.net/mail/">Mail transport</a>, + <a href="http://xmpppy.sourceforge.net/yahoo/">Yahoo transport</a> or + <a href="http://sourceforge.net/project/showfiles.php?group_id=97081&package_id=130713">xmppd</a> + project code if you wish to see the serious library usage.</p> + <h3>download</h3> + <div>You can look for released versions on + <a href="http://sourceforge.net/project/showfiles.php?group_id=97081">downloads page</a> + or alternatively you can grab the latest version directly from CVS tree + by typing the following commands: + <blockquote> + cvs -d:pserver:anonymous@xmpppy.cvs.sourceforge.net:/cvsroot/xmpppy login + </blockquote> + (hit "enter" when you will be prompted for password) + <blockquote> + cvs -z3 -d:pserver:anonymous@xmpppy.cvs.sourceforge.net:/cvsroot/xmpppy co xmpppy + </blockquote> + </div> + <p>You can also browse xmpppy (and several xmpppy-based + projects) CVS online <a href="http://xmpppy.cvs.sourceforge.net/xmpppy/xmpppy/">here</a>.</p> + <p>If you have an RSS feed reader, there is + an RSS feed of CVS commits <a href="http://xmpppy.sourceforge.net/cvs-xmpppy.xml">here</a>.</p> + <h3>support</h3> + <p>If you have any questions about using xmpppy you can join + <a href="http://lists.sourceforge.net/lists/listinfo/xmpppy-devel">xmpppy-devel maillist</a>. + Here you can always find the best support, as you can find the developers here.</p> + <h3>donations</h3> + <p>If you are willing to help you can consult + <a href="http://software.newsforge.com/article.pl?sid=05/01/06/1557225">this article</a> + for how to do it. Thanks!</p> + <p>If you want to donate some money to encourage me to continue work on + library (it helps, really!) you can do it via e-gold system (account 1552795) or via + bank transfer (contact me via jabber or email to get the details).</p> + <a name="author"></a> + <h3>author</h3> + <p>Alexey Nezhdanov<br /> + Russian, born 18 Nov 1976.<br /> + My timezone is GMT+3<br /> + e-mail & Jabber: snake at penza-gsm.ru<br /> + ICQ: 19515046</p> + <div>I'm seeking for a job over Internet. It may be jabber-related work or + any other.<br /> + Possible directions of work: + <ul> + <li>Python projects (preferred)</li> + <li>C++ projects</li> + <li>Remote systems administering</li> + </ul> + My skills: + <ul> + <li>16 years of programming. Basic -> Pascal -> C++ -> Python</li> + <li>9 years of system administrator work. DOS -> Win 3.1 -> Win95 -> Win98 -> linux2.2 -> linux2.4 -> linux2.6</li> + <li>Automation tasks</li> + <li>Automated instant messenging (state change/failures reporting)</li> + <li>Some research work: + <ul> + <li>wavelet audio analysis</li> + <li>speech recognising</li> + <li>realtime texture [de]compression (for 3D systems).</li> + </ul> + </li> + </ul> + </div> + </td> + <td class="rightside"> + <h3>downloads</h3> + <p> + <a href="http://sourceforge.net/project/showfiles.php?group_id=97081&package_id=103821">xmpppy</a><br /> + <a href="http://sourceforge.net/project/showfiles.php?group_id=97081&package_id=130713">xmppd.py</a><br /> + <a href="http://sourceforge.net/project/showfiles.php?group_id=97081&package_id=118831">xmpppy-irc</a><br /> + <a href="http://sourceforge.net/project/showfiles.php?group_id=97081&package_id=182511">xmpppy-yahoo</a><br /> + <a href="http://sourceforge.net/project/showfiles.php?group_id=97081&package_id=168390">pyGAIM-t</a> + </p> + <p><a href="http://sourceforge.net/project/showfiles.php?group_id=97081">List all files</a></p> + <h3>sourceforge</h3> + <p> + <a href="http://sourceforge.net/projects/xmpppy/">Project Summary</a><br /> + <a href="http://sourceforge.net/tracker/?atid=616918&group_id=97081">Feature Requests</a><br /> + <a href="http://sourceforge.net/tracker/?atid=616915&group_id=97081">Bugs</a><br /> + <a href="http://sourceforge.net/tracker/?atid=616917&group_id=97081">Patches</a><br /> + </p> + <h3>exits</h3> + <p> + <a href="http://www.jabber.org/">jabber.org</a><br /> + <a href="http://xmpppy.sourceforge.net/irc/">IRC transport</a><br /> + <a href="http://xmpppy.sourceforge.net/mail/">Mail transport</a><br /> + <a href="http://xmpppy.sourceforge.net/yahoo/">Yahoo transport</a><br /> + </p> + </td> + </tr> + </tbody> + </table> + <p>the <a href="http://sourceforge.net/projects/xmpppy/">xmpppy project</a></p> + </body> +</html> diff --git a/src/xmpppy-0.5.0rc1/doc/xmpppy.css b/src/xmpppy-0.5.0rc1/doc/xmpppy.css new file mode 100644 index 00000000..eeed2b2f --- /dev/null +++ b/src/xmpppy-0.5.0rc1/doc/xmpppy.css @@ -0,0 +1,70 @@ +/* xmpppy.css - The stylesheet of the xmpppy homepage + * + * Parts are taken of ickle stylesheet + * + * Copyleft 2005 by Marek Kubica + * + * Version 0.0.20050507 + * + */ + +/* set a background color, a light grey*/ +body { + background-color: #F2F2F2; +} + +/* fonts - no serif */ +a, p, ul, td, div { + font-family: sans-serif; +} + +/* hyperlinks: blue, not decorated, just bold */ +a { + text-decoration: none; + color: #00488F; + font-weight: bold; +} + +/* the head table, blue like the sf.net logo */ +table.head { + width: 100%; + background-color: #00488F; + text-align: right; + border-collapse: collapse; +} + +td.head { + padding: 0px; +} + +table.content { + padding: 5px; + border-spacing: 30px; +} + +td.sflogo { + width: 99%; +} + +/* the conentent of the left side fills 80% of the screen */ +td.leftside { + width: 80%; +} + +/* the links on the right side fill the remaining 20% + * and are displayed on top + */ +td.rightside { + width: 20%; + vertical-align: top; +} + +/* not simple bullets, but squares */ +ul { + list-style-type: square; +} + +/* blockquotes in italic */ +blockquote { + font-style: italic; +} diff --git a/src/xmpppy-0.5.0rc1/doc/xmpppy_title.png b/src/xmpppy-0.5.0rc1/doc/xmpppy_title.png Binary files differnew file mode 100644 index 00000000..6ffdfb4e --- /dev/null +++ b/src/xmpppy-0.5.0rc1/doc/xmpppy_title.png diff --git a/src/xmpppy-0.5.0rc1/setup.py b/src/xmpppy-0.5.0rc1/setup.py new file mode 100755 index 00000000..1e145617 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/setup.py @@ -0,0 +1,40 @@ +#!/usr/bin/python +# -*- coding: koi8-r -*- +from distutils.core import setup,sys +from setuptools import setup +import os + +if sys.version < '2.2.3': + from distutils.dist import DistributionMetadata + DistributionMetadata.classifiers = None + DistributionMetadata.download_url = None + +# Set proper release version in source code also!!! +setup(name='xmpppy', + version='0.5.0rc1', + author='Alexey Nezhdanov', + author_email='snakeru@users.sourceforge.net', + url='http://xmpppy.sourceforge.net/', + description='XMPP-IM-compliant library for jabber instant messenging.', + long_description="""This library provides functionality for writing xmpp-compliant +clients, servers and/or components/transports. + +It was initially designed as a \"rework\" of the jabberpy library but +has become a separate product. + +Unlike jabberpy it is distributed under the terms of GPL.""", + download_url='http://sourceforge.net/project/showfiles.php?group_id=97081&package_id=103821', + packages=['xmpp'], + license="GPL", + platforms="All", + keywords=['jabber','xmpp'], + classifiers = [ + 'Topic :: Communications :: Chat', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Natural Language :: English', + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + ], + ) diff --git a/src/xmpppy-0.5.0rc1/xmpp/__init__.py b/src/xmpppy-0.5.0rc1/xmpp/__init__.py new file mode 100644 index 00000000..ad03b288 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/xmpp/__init__.py @@ -0,0 +1,31 @@ +# $Id: __init__.py,v 1.9 2005/03/07 09:34:51 snakeru Exp $ + +""" +All features of xmpppy library contained within separate modules. +At present there are modules: +simplexml - XML handling routines +protocol - jabber-objects (I.e. JID and different stanzas and sub-stanzas) handling routines. +debug - Jacob Lundquist's debugging module. Very handy if you like colored debug. +auth - Non-SASL and SASL stuff. You will need it to auth as a client or transport. +transports - low level connection handling. TCP and TLS currently. HTTP support planned. +roster - simple roster for use in clients. +dispatcher - decision-making logic. Handles all hooks. The first who takes control over fresh stanzas. +features - different stuff that didn't worths separating into modules +browser - DISCO server framework. Allows to build dynamic disco tree. +filetransfer - Currently contains only IBB stuff. Can be used for bot-to-bot transfers. + +Most of the classes that is defined in all these modules is an ancestors of +class PlugIn so they share a single set of methods allowing you to compile +a featured XMPP client. For every instance of PlugIn class the 'owner' is the class +in what the plug was plugged. While plugging in such instance usually sets some +methods of owner to it's own ones for easy access. All session specific info stored +either in instance of PlugIn or in owner's instance. This is considered unhandy +and there are plans to port 'Session' class from xmppd.py project for storing all +session-related info. Though if you are not accessing instances variables directly +and use only methods for access all values you should not have any problems. + +""" + +import simplexml,protocol,debug,auth,transports,roster,dispatcher,features,browser,filetransfer,commands +from client import * +from protocol import * diff --git a/src/xmpppy-0.5.0rc1/xmpp/auth.py b/src/xmpppy-0.5.0rc1/xmpp/auth.py new file mode 100644 index 00000000..6e51d72b --- /dev/null +++ b/src/xmpppy-0.5.0rc1/xmpp/auth.py @@ -0,0 +1,326 @@ +## auth.py +## +## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: auth.py,v 1.41 2008/09/13 21:45:21 normanr Exp $ + +""" +Provides library with all Non-SASL and SASL authentication mechanisms. +Can be used both for client and transport authentication. +""" + +from protocol import * +from client import PlugIn +import sha,base64,random,dispatcher,re + +import md5 +def HH(some): return md5.new(some).hexdigest() +def H(some): return md5.new(some).digest() +def C(some): return ':'.join(some) + +class NonSASL(PlugIn): + """ Implements old Non-SASL (JEP-0078) authentication used in jabberd1.4 and transport authentication.""" + def __init__(self,user,password,resource): + """ Caches username, password and resource for auth. """ + PlugIn.__init__(self) + self.DBG_LINE='gen_auth' + self.user=user + self.password=password + self.resource=resource + + def plugin(self,owner): + """ Determine the best auth method (digest/0k/plain) and use it for auth. + Returns used method name on success. Used internally. """ + if not self.resource: return self.authComponent(owner) + self.DEBUG('Querying server about possible auth methods','start') + resp=owner.Dispatcher.SendAndWaitForResponse(Iq('get',NS_AUTH,payload=[Node('username',payload=[self.user])])) + if not isResultNode(resp): + self.DEBUG('No result node arrived! Aborting...','error') + return + iq=Iq(typ='set',node=resp) + query=iq.getTag('query') + query.setTagData('username',self.user) + query.setTagData('resource',self.resource) + + if query.getTag('digest'): + self.DEBUG("Performing digest authentication",'ok') + query.setTagData('digest',sha.new(owner.Dispatcher.Stream._document_attrs['id']+self.password).hexdigest()) + if query.getTag('password'): query.delChild('password') + method='digest' + elif query.getTag('token'): + token=query.getTagData('token') + seq=query.getTagData('sequence') + self.DEBUG("Performing zero-k authentication",'ok') + hash = sha.new(sha.new(self.password).hexdigest()+token).hexdigest() + for foo in xrange(int(seq)): hash = sha.new(hash).hexdigest() + query.setTagData('hash',hash) + method='0k' + else: + self.DEBUG("Sequre methods unsupported, performing plain text authentication",'warn') + query.setTagData('password',self.password) + method='plain' + resp=owner.Dispatcher.SendAndWaitForResponse(iq) + if isResultNode(resp): + self.DEBUG('Sucessfully authenticated with remove host.','ok') + owner.User=self.user + owner.Resource=self.resource + owner._registered_name=owner.User+'@'+owner.Server+'/'+owner.Resource + return method + self.DEBUG('Authentication failed!','error') + + def authComponent(self,owner): + """ Authenticate component. Send handshake stanza and wait for result. Returns "ok" on success. """ + self.handshake=0 + owner.send(Node(NS_COMPONENT_ACCEPT+' handshake',payload=[sha.new(owner.Dispatcher.Stream._document_attrs['id']+self.password).hexdigest()])) + owner.RegisterHandler('handshake',self.handshakeHandler,xmlns=NS_COMPONENT_ACCEPT) + while not self.handshake: + self.DEBUG("waiting on handshake",'notify') + owner.Process(1) + owner._registered_name=self.user + if self.handshake+1: return 'ok' + + def handshakeHandler(self,disp,stanza): + """ Handler for registering in dispatcher for accepting transport authentication. """ + if stanza.getName()=='handshake': self.handshake=1 + else: self.handshake=-1 + +class SASL(PlugIn): + """ Implements SASL authentication. """ + def __init__(self,username,password): + PlugIn.__init__(self) + self.username=username + self.password=password + + def plugin(self,owner): + if not self._owner.Dispatcher.Stream._document_attrs.has_key('version'): self.startsasl='not-supported' + elif self._owner.Dispatcher.Stream.features: + try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) + except NodeProcessed: pass + else: self.startsasl=None + + def auth(self): + """ Start authentication. Result can be obtained via "SASL.startsasl" attribute and will be + either "success" or "failure". Note that successfull auth will take at least + two Dispatcher.Process() calls. """ + if self.startsasl: pass + elif self._owner.Dispatcher.Stream.features: + try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) + except NodeProcessed: pass + else: self._owner.RegisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) + + def plugout(self): + """ Remove SASL handlers from owner's dispatcher. Used internally. """ + if self._owner.__dict__.has_key('features'): self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) + if self._owner.__dict__.has_key('challenge'): self._owner.UnregisterHandler('challenge',self.SASLHandler,xmlns=NS_SASL) + if self._owner.__dict__.has_key('failure'): self._owner.UnregisterHandler('failure',self.SASLHandler,xmlns=NS_SASL) + if self._owner.__dict__.has_key('success'): self._owner.UnregisterHandler('success',self.SASLHandler,xmlns=NS_SASL) + + def FeaturesHandler(self,conn,feats): + """ Used to determine if server supports SASL auth. Used internally. """ + if not feats.getTag('mechanisms',namespace=NS_SASL): + self.startsasl='not-supported' + self.DEBUG('SASL not supported by server','error') + return + mecs=[] + for mec in feats.getTag('mechanisms',namespace=NS_SASL).getTags('mechanism'): + mecs.append(mec.getData()) + self._owner.RegisterHandler('challenge',self.SASLHandler,xmlns=NS_SASL) + self._owner.RegisterHandler('failure',self.SASLHandler,xmlns=NS_SASL) + self._owner.RegisterHandler('success',self.SASLHandler,xmlns=NS_SASL) + if "ANONYMOUS" in mecs and self.username == None: + node=Node('auth',attrs={'xmlns':NS_SASL,'mechanism':'ANONYMOUS'}) + elif "DIGEST-MD5" in mecs: + node=Node('auth',attrs={'xmlns':NS_SASL,'mechanism':'DIGEST-MD5'}) + elif "PLAIN" in mecs: + sasl_data='%s\x00%s\x00%s'%(self.username+'@'+self._owner.Server,self.username,self.password) + node=Node('auth',attrs={'xmlns':NS_SASL,'mechanism':'PLAIN'},payload=[base64.encodestring(sasl_data).replace('\r','').replace('\n','')]) + else: + self.startsasl='failure' + self.DEBUG('I can only use DIGEST-MD5 and PLAIN mecanisms.','error') + return + self.startsasl='in-process' + self._owner.send(node.__str__()) + raise NodeProcessed + + def SASLHandler(self,conn,challenge): + """ Perform next SASL auth step. Used internally. """ + if challenge.getNamespace()<>NS_SASL: return + if challenge.getName()=='failure': + self.startsasl='failure' + try: reason=challenge.getChildren()[0] + except: reason=challenge + self.DEBUG('Failed SASL authentification: %s'%reason,'error') + raise NodeProcessed + elif challenge.getName()=='success': + self.startsasl='success' + self.DEBUG('Successfully authenticated with remote server.','ok') + handlers=self._owner.Dispatcher.dumpHandlers() + self._owner.Dispatcher.PlugOut() + dispatcher.Dispatcher().PlugIn(self._owner) + self._owner.Dispatcher.restoreHandlers(handlers) + self._owner.User=self.username + raise NodeProcessed +########################################3333 + incoming_data=challenge.getData() + chal={} + data=base64.decodestring(incoming_data) + self.DEBUG('Got challenge:'+data,'ok') + for pair in re.findall('(\w+\s*=\s*(?:(?:"[^"]+")|(?:[^,]+)))',data): + key,value=[x.strip() for x in pair.split('=', 1)] + if value[:1]=='"' and value[-1:]=='"': value=value[1:-1] + chal[key]=value + if chal.has_key('qop') and 'auth' in [x.strip() for x in chal['qop'].split(',')]: + resp={} + resp['username']=self.username + resp['realm']=self._owner.Server + resp['nonce']=chal['nonce'] + cnonce='' + for i in range(7): + cnonce+=hex(int(random.random()*65536*4096))[2:] + resp['cnonce']=cnonce + resp['nc']=('00000001') + resp['qop']='auth' + resp['digest-uri']='xmpp/'+self._owner.Server + A1=C([H(C([resp['username'],resp['realm'],self.password])),resp['nonce'],resp['cnonce']]) + A2=C(['AUTHENTICATE',resp['digest-uri']]) + response= HH(C([HH(A1),resp['nonce'],resp['nc'],resp['cnonce'],resp['qop'],HH(A2)])) + resp['response']=response + resp['charset']='utf-8' + sasl_data='' + for key in ['charset','username','realm','nonce','nc','cnonce','digest-uri','response','qop']: + if key in ['nc','qop','response','charset']: sasl_data+="%s=%s,"%(key,resp[key]) + else: sasl_data+='%s="%s",'%(key,resp[key]) +########################################3333 + node=Node('response',attrs={'xmlns':NS_SASL},payload=[base64.encodestring(sasl_data[:-1]).replace('\r','').replace('\n','')]) + self._owner.send(node.__str__()) + elif chal.has_key('rspauth'): self._owner.send(Node('response',attrs={'xmlns':NS_SASL}).__str__()) + else: + self.startsasl='failure' + self.DEBUG('Failed SASL authentification: unknown challenge','error') + raise NodeProcessed + +class Bind(PlugIn): + """ Bind some JID to the current connection to allow router know of our location.""" + def __init__(self): + PlugIn.__init__(self) + self.DBG_LINE='bind' + self.bound=None + + def plugin(self,owner): + """ Start resource binding, if allowed at this time. Used internally. """ + if self._owner.Dispatcher.Stream.features: + try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) + except NodeProcessed: pass + else: self._owner.RegisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) + + def plugout(self): + """ Remove Bind handler from owner's dispatcher. Used internally. """ + self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) + + def FeaturesHandler(self,conn,feats): + """ Determine if server supports resource binding and set some internal attributes accordingly. """ + if not feats.getTag('bind',namespace=NS_BIND): + self.bound='failure' + self.DEBUG('Server does not requested binding.','error') + return + if feats.getTag('session',namespace=NS_SESSION): self.session=1 + else: self.session=-1 + self.bound=[] + + def Bind(self,resource=None): + """ Perform binding. Use provided resource name or random (if not provided). """ + while self.bound is None and self._owner.Process(1): pass + if resource: resource=[Node('resource',payload=[resource])] + else: resource=[] + resp=self._owner.SendAndWaitForResponse(Protocol('iq',typ='set',payload=[Node('bind',attrs={'xmlns':NS_BIND},payload=resource)])) + if isResultNode(resp): + self.bound.append(resp.getTag('bind').getTagData('jid')) + self.DEBUG('Successfully bound %s.'%self.bound[-1],'ok') + jid=JID(resp.getTag('bind').getTagData('jid')) + self._owner.User=jid.getNode() + self._owner.Resource=jid.getResource() + resp=self._owner.SendAndWaitForResponse(Protocol('iq',typ='set',payload=[Node('session',attrs={'xmlns':NS_SESSION})])) + if isResultNode(resp): + self.DEBUG('Successfully opened session.','ok') + self.session=1 + return 'ok' + else: + self.DEBUG('Session open failed.','error') + self.session=0 + elif resp: self.DEBUG('Binding failed: %s.'%resp.getTag('error'),'error') + else: + self.DEBUG('Binding failed: timeout expired.','error') + return '' + +class ComponentBind(PlugIn): + """ ComponentBind some JID to the current connection to allow router know of our location.""" + def __init__(self, sasl): + PlugIn.__init__(self) + self.DBG_LINE='bind' + self.bound=None + self.needsUnregister=None + self.sasl = sasl + + def plugin(self,owner): + """ Start resource binding, if allowed at this time. Used internally. """ + if not self.sasl: + self.bound=[] + return + if self._owner.Dispatcher.Stream.features: + try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) + except NodeProcessed: pass + else: + self._owner.RegisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) + self.needsUnregister=1 + + def plugout(self): + """ Remove ComponentBind handler from owner's dispatcher. Used internally. """ + if self.needsUnregister: + self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) + + def FeaturesHandler(self,conn,feats): + """ Determine if server supports resource binding and set some internal attributes accordingly. """ + if not feats.getTag('bind',namespace=NS_BIND): + self.bound='failure' + self.DEBUG('Server does not requested binding.','error') + return + if feats.getTag('session',namespace=NS_SESSION): self.session=1 + else: self.session=-1 + self.bound=[] + + def Bind(self,domain=None): + """ Perform binding. Use provided domain name (if not provided). """ + while self.bound is None and self._owner.Process(1): pass + if self.sasl: + xmlns = NS_COMPONENT_1 + else: + xmlns = None + self.bindresponse = None + ttl = dispatcher.DefaultTimeout + self._owner.RegisterHandler('bind',self.BindHandler,xmlns=xmlns) + self._owner.send(Protocol('bind',attrs={'name':domain},xmlns=NS_COMPONENT_1)) + while self.bindresponse is None and self._owner.Process(1) and ttl > 0: ttl-=1 + self._owner.UnregisterHandler('bind',self.BindHandler,xmlns=xmlns) + resp=self.bindresponse + if resp and resp.getAttr('error'): + self.DEBUG('Binding failed: %s.'%resp.getAttr('error'),'error') + elif resp: + self.DEBUG('Successfully bound.','ok') + return 'ok' + else: + self.DEBUG('Binding failed: timeout expired.','error') + return '' + + def BindHandler(self,conn,bind): + self.bindresponse = bind + pass diff --git a/src/xmpppy-0.5.0rc1/xmpp/browser.py b/src/xmpppy-0.5.0rc1/xmpp/browser.py new file mode 100644 index 00000000..8848ea4e --- /dev/null +++ b/src/xmpppy-0.5.0rc1/xmpp/browser.py @@ -0,0 +1,221 @@ +## browser.py +## +## Copyright (C) 2004 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: browser.py,v 1.12 2007/05/13 17:55:14 normanr Exp $ + +"""Browser module provides DISCO server framework for your application. +This functionality can be used for very different purposes - from publishing +software version and supported features to building of "jabber site" that users +can navigate with their disco browsers and interact with active content. + +Such functionality is achieved via registering "DISCO handlers" that are +automatically called when user requests some node of your disco tree. +""" + +from dispatcher import * +from client import PlugIn + +class Browser(PlugIn): + """ WARNING! This class is for components only. It will not work in client mode! + + Standart xmpppy class that is ancestor of PlugIn and can be attached + to your application. + All processing will be performed in the handlers registered in the browser + instance. You can register any number of handlers ensuring that for each + node/jid combination only one (or none) handler registered. + You can register static information or the fully-blown function that will + calculate the answer dynamically. + Example of static info (see JEP-0030, examples 13-14): + # cl - your xmpppy connection instance. + b=xmpp.browser.Browser() + b.PlugIn(cl) + items=[] + item={} + item['jid']='catalog.shakespeare.lit' + item['node']='books' + item['name']='Books by and about Shakespeare' + items.append(item) + item={} + item['jid']='catalog.shakespeare.lit' + item['node']='clothing' + item['name']='Wear your literary taste with pride' + items.append(item) + item={} + item['jid']='catalog.shakespeare.lit' + item['node']='music' + item['name']='Music from the time of Shakespeare' + items.append(item) + info={'ids':[], 'features':[]} + b.setDiscoHandler({'items':items,'info':info}) + + items should be a list of item elements. + every item element can have any of these four keys: 'jid', 'node', 'name', 'action' + info should be a dicionary and must have keys 'ids' and 'features'. + Both of them should be lists: + ids is a list of dictionaries and features is a list of text strings. + Example (see JEP-0030, examples 1-2) + # cl - your xmpppy connection instance. + b=xmpp.browser.Browser() + b.PlugIn(cl) + items=[] + ids=[] + ids.append({'category':'conference','type':'text','name':'Play-Specific Chatrooms'}) + ids.append({'category':'directory','type':'chatroom','name':'Play-Specific Chatrooms'}) + features=[NS_DISCO_INFO,NS_DISCO_ITEMS,NS_MUC,NS_REGISTER,NS_SEARCH,NS_TIME,NS_VERSION] + info={'ids':ids,'features':features} + # info['xdata']=xmpp.protocol.DataForm() # JEP-0128 + b.setDiscoHandler({'items':[],'info':info}) + """ + def __init__(self): + """Initialises internal variables. Used internally.""" + PlugIn.__init__(self) + DBG_LINE='browser' + self._exported_methods=[] + self._handlers={'':{}} + + def plugin(self, owner): + """ Registers it's own iq handlers in your application dispatcher instance. + Used internally.""" + owner.RegisterHandler('iq',self._DiscoveryHandler,typ='get',ns=NS_DISCO_INFO) + owner.RegisterHandler('iq',self._DiscoveryHandler,typ='get',ns=NS_DISCO_ITEMS) + + def plugout(self): + """ Unregisters browser's iq handlers from your application dispatcher instance. + Used internally.""" + self._owner.UnregisterHandler('iq',self._DiscoveryHandler,typ='get',ns=NS_DISCO_INFO) + self._owner.UnregisterHandler('iq',self._DiscoveryHandler,typ='get',ns=NS_DISCO_ITEMS) + + def _traversePath(self,node,jid,set=0): + """ Returns dictionary and key or None,None + None - root node (w/o "node" attribute) + /a/b/c - node + /a/b/ - branch + Set returns '' or None as the key + get returns '' or None as the key or None as the dict. + Used internally.""" + if self._handlers.has_key(jid): cur=self._handlers[jid] + elif set: + self._handlers[jid]={} + cur=self._handlers[jid] + else: cur=self._handlers[''] + if node is None: node=[None] + else: node=node.replace('/',' /').split('/') + for i in node: + if i<>'' and cur.has_key(i): cur=cur[i] + elif set and i<>'': cur[i]={dict:cur,str:i}; cur=cur[i] + elif set or cur.has_key(''): return cur,'' + else: return None,None + if cur.has_key(1) or set: return cur,1 + raise "Corrupted data" + + def setDiscoHandler(self,handler,node='',jid=''): + """ This is the main method that you will use in this class. + It is used to register supplied DISCO handler (or dictionary with static info) + as handler of some disco tree branch. + If you do not specify the node this handler will be used for all queried nodes. + If you do not specify the jid this handler will be used for all queried JIDs. + + Usage: + cl.Browser.setDiscoHandler(someDict,node,jid) + or + cl.Browser.setDiscoHandler(someDISCOHandler,node,jid) + where + + someDict={ + 'items':[ + {'jid':'jid1','action':'action1','node':'node1','name':'name1'}, + {'jid':'jid2','action':'action2','node':'node2','name':'name2'}, + {'jid':'jid3','node':'node3','name':'name3'}, + {'jid':'jid4','node':'node4'} + ], + 'info' :{ + 'ids':[ + {'category':'category1','type':'type1','name':'name1'}, + {'category':'category2','type':'type2','name':'name2'}, + {'category':'category3','type':'type3','name':'name3'}, + ], + 'features':['feature1','feature2','feature3','feature4'], + 'xdata':DataForm + } + } + + and/or + + def someDISCOHandler(session,request,TYR): + # if TYR=='items': # returns items list of the same format as shown above + # elif TYR=='info': # returns info dictionary of the same format as shown above + # else: # this case is impossible for now. + """ + self.DEBUG('Registering handler %s for "%s" node->%s'%(handler,jid,node), 'info') + node,key=self._traversePath(node,jid,1) + node[key]=handler + + def getDiscoHandler(self,node='',jid=''): + """ Returns the previously registered DISCO handler + that is resonsible for this node/jid combination. + Used internally.""" + node,key=self._traversePath(node,jid) + if node: return node[key] + + def delDiscoHandler(self,node='',jid=''): + """ Unregisters DISCO handler that is resonsible for this + node/jid combination. When handler is unregistered the branch + is handled in the same way that it's parent branch from this moment. + """ + node,key=self._traversePath(node,jid) + if node: + handler=node[key] + del node[dict][node[str]] + return handler + + def _DiscoveryHandler(self,conn,request): + """ Servers DISCO iq request from the remote client. + Automatically determines the best handler to use and calls it + to handle the request. Used internally. + """ + node=request.getQuerynode() + if node: + nodestr=node + else: + nodestr='None' + handler=self.getDiscoHandler(node,request.getTo()) + if not handler: + self.DEBUG("No Handler for request with jid->%s node->%s ns->%s"%(request.getTo().__str__().encode('utf8'),nodestr.encode('utf8'),request.getQueryNS().encode('utf8')),'error') + conn.send(Error(request,ERR_ITEM_NOT_FOUND)) + raise NodeProcessed + self.DEBUG("Handling request with jid->%s node->%s ns->%s"%(request.getTo().__str__().encode('utf8'),nodestr.encode('utf8'),request.getQueryNS().encode('utf8')),'ok') + rep=request.buildReply('result') + if node: rep.setQuerynode(node) + q=rep.getTag('query') + if request.getQueryNS()==NS_DISCO_ITEMS: + # handler must return list: [{jid,action,node,name}] + if type(handler)==dict: lst=handler['items'] + else: lst=handler(conn,request,'items') + if lst==None: + conn.send(Error(request,ERR_ITEM_NOT_FOUND)) + raise NodeProcessed + for item in lst: q.addChild('item',item) + elif request.getQueryNS()==NS_DISCO_INFO: + if type(handler)==dict: dt=handler['info'] + else: dt=handler(conn,request,'info') + if dt==None: + conn.send(Error(request,ERR_ITEM_NOT_FOUND)) + raise NodeProcessed + # handler must return dictionary: + # {'ids':[{},{},{},{}], 'features':[fe,at,ur,es], 'xdata':DataForm} + for id in dt['ids']: q.addChild('identity',id) + for feature in dt['features']: q.addChild('feature',{'var':feature}) + if dt.has_key('xdata'): q.addChild(node=dt['xdata']) + conn.send(rep) + raise NodeProcessed diff --git a/src/xmpppy-0.5.0rc1/xmpp/client.py b/src/xmpppy-0.5.0rc1/xmpp/client.py new file mode 100644 index 00000000..4d932119 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/xmpp/client.py @@ -0,0 +1,325 @@ +## client.py +## +## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: client.py,v 1.61 2009/04/07 06:19:42 snakeru Exp $ + +""" +Provides PlugIn class functionality to develop extentions for xmpppy. +Also provides Client and Component classes implementations as the +examples of xmpppy structures usage. +These classes can be used for simple applications "AS IS" though. +""" + +import socket +import debug +Debug=debug +Debug.DEBUGGING_IS_ON=1 +Debug.Debug.colors['socket']=debug.color_dark_gray +Debug.Debug.colors['CONNECTproxy']=debug.color_dark_gray +Debug.Debug.colors['nodebuilder']=debug.color_brown +Debug.Debug.colors['client']=debug.color_cyan +Debug.Debug.colors['component']=debug.color_cyan +Debug.Debug.colors['dispatcher']=debug.color_green +Debug.Debug.colors['browser']=debug.color_blue +Debug.Debug.colors['auth']=debug.color_yellow +Debug.Debug.colors['roster']=debug.color_magenta +Debug.Debug.colors['ibb']=debug.color_yellow + +Debug.Debug.colors['down']=debug.color_brown +Debug.Debug.colors['up']=debug.color_brown +Debug.Debug.colors['data']=debug.color_brown +Debug.Debug.colors['ok']=debug.color_green +Debug.Debug.colors['warn']=debug.color_yellow +Debug.Debug.colors['error']=debug.color_red +Debug.Debug.colors['start']=debug.color_dark_gray +Debug.Debug.colors['stop']=debug.color_dark_gray +Debug.Debug.colors['sent']=debug.color_yellow +Debug.Debug.colors['got']=debug.color_bright_cyan + +DBG_CLIENT='client' +DBG_COMPONENT='component' + +class PlugIn: + """ Common xmpppy plugins infrastructure: plugging in/out, debugging. """ + def __init__(self): + self._exported_methods=[] + self.DBG_LINE=self.__class__.__name__.lower() + + def PlugIn(self,owner): + """ Attach to main instance and register ourself and all our staff in it. """ + self._owner=owner + if self.DBG_LINE not in owner.debug_flags: + owner.debug_flags.append(self.DBG_LINE) + self.DEBUG('Plugging %s into %s'%(self,self._owner),'start') + if owner.__dict__.has_key(self.__class__.__name__): + return self.DEBUG('Plugging ignored: another instance already plugged.','error') + self._old_owners_methods=[] + for method in self._exported_methods: + if owner.__dict__.has_key(method.__name__): + self._old_owners_methods.append(owner.__dict__[method.__name__]) + owner.__dict__[method.__name__]=method + owner.__dict__[self.__class__.__name__]=self + if self.__class__.__dict__.has_key('plugin'): return self.plugin(owner) + + def PlugOut(self): + """ Unregister all our staff from main instance and detach from it. """ + self.DEBUG('Plugging %s out of %s.'%(self,self._owner),'stop') + ret = None + if self.__class__.__dict__.has_key('plugout'): ret = self.plugout() + self._owner.debug_flags.remove(self.DBG_LINE) + for method in self._exported_methods: del self._owner.__dict__[method.__name__] + for method in self._old_owners_methods: self._owner.__dict__[method.__name__]=method + del self._owner.__dict__[self.__class__.__name__] + return ret + + def DEBUG(self,text,severity='info'): + """ Feed a provided debug line to main instance's debug facility along with our ID string. """ + self._owner.DEBUG(self.DBG_LINE,text,severity) + +import transports,dispatcher,auth,roster +class CommonClient: + """ Base for Client and Component classes.""" + def __init__(self,server,port=5222,debug=['always', 'nodebuilder']): + """ Caches server name and (optionally) port to connect to. "debug" parameter specifies + the debug IDs that will go into debug output. You can either specifiy an "include" + or "exclude" list. The latter is done via adding "always" pseudo-ID to the list. + Full list: ['nodebuilder', 'dispatcher', 'gen_auth', 'SASL_auth', 'bind', 'socket', + 'CONNECTproxy', 'TLS', 'roster', 'browser', 'ibb'] . """ + if self.__class__.__name__=='Client': self.Namespace,self.DBG='jabber:client',DBG_CLIENT + elif self.__class__.__name__=='Component': self.Namespace,self.DBG=dispatcher.NS_COMPONENT_ACCEPT,DBG_COMPONENT + self.defaultNamespace=self.Namespace + self.disconnect_handlers=[] + self.Server=server + self.Port=port + if debug and type(debug)<>list: debug=['always', 'nodebuilder'] + self._DEBUG=Debug.Debug(debug) + self.DEBUG=self._DEBUG.Show + self.debug_flags=self._DEBUG.debug_flags + self.debug_flags.append(self.DBG) + self._owner=self + self._registered_name=None + self.RegisterDisconnectHandler(self.DisconnectHandler) + self.connected='' + self._route=0 + + def RegisterDisconnectHandler(self,handler): + """ Register handler that will be called on disconnect.""" + self.disconnect_handlers.append(handler) + + def UnregisterDisconnectHandler(self,handler): + """ Unregister handler that is called on disconnect.""" + self.disconnect_handlers.remove(handler) + + def disconnected(self): + """ Called on disconnection. Calls disconnect handlers and cleans things up. """ + self.connected='' + self.DEBUG(self.DBG,'Disconnect detected','stop') + self.disconnect_handlers.reverse() + for i in self.disconnect_handlers: i() + self.disconnect_handlers.reverse() + if self.__dict__.has_key('TLS'): self.TLS.PlugOut() + + def DisconnectHandler(self): + """ Default disconnect handler. Just raises an IOError. + If you choosed to use this class in your production client, + override this method or at least unregister it. """ + raise IOError('Disconnected from server.') + + def event(self,eventName,args={}): + """ Default event handler. To be overriden. """ + print "Event: ",(eventName,args) + + def isConnected(self): + """ Returns connection state. F.e.: None / 'tls' / 'tcp+non_sasl' . """ + return self.connected + + def reconnectAndReauth(self): + """ Example of reconnection method. In fact, it can be used to batch connection and auth as well. """ + handlerssave=self.Dispatcher.dumpHandlers() + if self.__dict__.has_key('ComponentBind'): self.ComponentBind.PlugOut() + if self.__dict__.has_key('Bind'): self.Bind.PlugOut() + self._route=0 + if self.__dict__.has_key('NonSASL'): self.NonSASL.PlugOut() + if self.__dict__.has_key('SASL'): self.SASL.PlugOut() + if self.__dict__.has_key('TLS'): self.TLS.PlugOut() + self.Dispatcher.PlugOut() + if self.__dict__.has_key('HTTPPROXYsocket'): self.HTTPPROXYsocket.PlugOut() + if self.__dict__.has_key('TCPsocket'): self.TCPsocket.PlugOut() + if not self.connect(server=self._Server,proxy=self._Proxy): return + if not self.auth(self._User,self._Password,self._Resource): return + self.Dispatcher.restoreHandlers(handlerssave) + return self.connected + + def connect(self,server=None,proxy=None,ssl=None,use_srv=None): + """ Make a tcp/ip connection, protect it with tls/ssl if possible and start XMPP stream. + Returns None or 'tcp' or 'tls', depending on the result.""" + if not server: server=(self.Server,self.Port) + if proxy: sock=transports.HTTPPROXYsocket(proxy,server,use_srv) + else: sock=transports.TCPsocket(server,use_srv) + connected=sock.PlugIn(self) + if not connected: + sock.PlugOut() + return + self._Server,self._Proxy=server,proxy + self.connected='tcp' + if (ssl is None and self.Connection.getPort() in (5223, 443)) or ssl: + try: # FIXME. This should be done in transports.py + transports.TLS().PlugIn(self,now=1) + self.connected='ssl' + except socket.sslerror: + return + dispatcher.Dispatcher().PlugIn(self) + while self.Dispatcher.Stream._document_attrs is None: + if not self.Process(1): return + if self.Dispatcher.Stream._document_attrs.has_key('version') and self.Dispatcher.Stream._document_attrs['version']=='1.0': + while not self.Dispatcher.Stream.features and self.Process(1): pass # If we get version 1.0 stream the features tag MUST BE presented + return self.connected + +class Client(CommonClient): + """ Example client class, based on CommonClient. """ + def connect(self,server=None,proxy=None,secure=None,use_srv=True): + """ Connect to jabber server. If you want to specify different ip/port to connect to you can + pass it as tuple as first parameter. If there is HTTP proxy between you and server + specify it's address and credentials (if needed) in the second argument. + If you want ssl/tls support to be discovered and enable automatically - leave third argument as None. (ssl will be autodetected only if port is 5223 or 443) + If you want to force SSL start (i.e. if port 5223 or 443 is remapped to some non-standard port) then set it to 1. + If you want to disable tls/ssl support completely, set it to 0. + Example: connect(('192.168.5.5',5222),{'host':'proxy.my.net','port':8080,'user':'me','password':'secret'}) + Returns '' or 'tcp' or 'tls', depending on the result.""" + if not CommonClient.connect(self,server,proxy,secure,use_srv) or secure<>None and not secure: return self.connected + transports.TLS().PlugIn(self) + if not self.Dispatcher.Stream._document_attrs.has_key('version') or not self.Dispatcher.Stream._document_attrs['version']=='1.0': return self.connected + while not self.Dispatcher.Stream.features and self.Process(1): pass # If we get version 1.0 stream the features tag MUST BE presented + if not self.Dispatcher.Stream.features.getTag('starttls'): return self.connected # TLS not supported by server + while not self.TLS.starttls and self.Process(1): pass + if not hasattr(self, 'TLS') or self.TLS.starttls!='success': self.event('tls_failed'); return self.connected + self.connected='tls' + return self.connected + + def auth(self,user,password,resource='',sasl=1): + """ Authenticate connnection and bind resource. If resource is not provided + random one or library name used. """ + self._User,self._Password,self._Resource=user,password,resource + while not self.Dispatcher.Stream._document_attrs and self.Process(1): pass + if self.Dispatcher.Stream._document_attrs.has_key('version') and self.Dispatcher.Stream._document_attrs['version']=='1.0': + while not self.Dispatcher.Stream.features and self.Process(1): pass # If we get version 1.0 stream the features tag MUST BE presented + if sasl: auth.SASL(user,password).PlugIn(self) + if not sasl or self.SASL.startsasl=='not-supported': + if not resource: resource='xmpppy' + if auth.NonSASL(user,password,resource).PlugIn(self): + self.connected+='+old_auth' + return 'old_auth' + return + self.SASL.auth() + while self.SASL.startsasl=='in-process' and self.Process(1): pass + if self.SASL.startsasl=='success': + auth.Bind().PlugIn(self) + while self.Bind.bound is None and self.Process(1): pass + if self.Bind.Bind(resource): + self.connected+='+sasl' + return 'sasl' + else: + if self.__dict__.has_key('SASL'): self.SASL.PlugOut() + + def getRoster(self): + """ Return the Roster instance, previously plugging it in and + requesting roster from server if needed. """ + if not self.__dict__.has_key('Roster'): roster.Roster().PlugIn(self) + return self.Roster.getRoster() + + def sendInitPresence(self,requestRoster=1): + """ Send roster request and initial <presence/>. + You can disable the first by setting requestRoster argument to 0. """ + self.sendPresence(requestRoster=requestRoster) + + def sendPresence(self,jid=None,typ=None,requestRoster=0): + """ Send some specific presence state. + Can also request roster from server if according agrument is set.""" + if requestRoster: roster.Roster().PlugIn(self) + self.send(dispatcher.Presence(to=jid, typ=typ)) + +class Component(CommonClient): + """ Component class. The only difference from CommonClient is ability to perform component authentication. """ + def __init__(self,transport,port=5347,typ=None,debug=['always', 'nodebuilder'],domains=None,sasl=0,bind=0,route=0,xcp=0): + """ Init function for Components. + As components use a different auth mechanism which includes the namespace of the component. + Jabberd1.4 and Ejabberd use the default namespace then for all client messages. + Jabberd2 uses jabber:client. + 'transport' argument is a transport name that you are going to serve (f.e. "irc.localhost"). + 'port' can be specified if 'transport' resolves to correct IP. If it is not then you'll have to specify IP + and port while calling "connect()". + If you are going to serve several different domains with single Component instance - you must list them ALL + in the 'domains' argument. + For jabberd2 servers you should set typ='jabberd2' argument. + """ + CommonClient.__init__(self,transport,port=port,debug=debug) + self.typ=typ + self.sasl=sasl + self.bind=bind + self.route=route + self.xcp=xcp + if domains: + self.domains=domains + else: + self.domains=[transport] + + def connect(self,server=None,proxy=None): + """ This will connect to the server, and if the features tag is found then set + the namespace to be jabber:client as that is required for jabberd2. + 'server' and 'proxy' arguments have the same meaning as in xmpp.Client.connect() """ + if self.sasl: + self.Namespace=auth.NS_COMPONENT_1 + self.Server=server[0] + CommonClient.connect(self,server=server,proxy=proxy) + if self.connected and (self.typ=='jabberd2' or not self.typ and self.Dispatcher.Stream.features != None) and (not self.xcp): + self.defaultNamespace=auth.NS_CLIENT + self.Dispatcher.RegisterNamespace(self.defaultNamespace) + self.Dispatcher.RegisterProtocol('iq',dispatcher.Iq) + self.Dispatcher.RegisterProtocol('message',dispatcher.Message) + self.Dispatcher.RegisterProtocol('presence',dispatcher.Presence) + return self.connected + + def dobind(self, sasl): + # This has to be done before binding, because we can receive a route stanza before binding finishes + self._route = self.route + if self.bind: + for domain in self.domains: + auth.ComponentBind(sasl).PlugIn(self) + while self.ComponentBind.bound is None: self.Process(1) + if (not self.ComponentBind.Bind(domain)): + self.ComponentBind.PlugOut() + return + self.ComponentBind.PlugOut() + + def auth(self,name,password,dup=None): + """ Authenticate component "name" with password "password".""" + self._User,self._Password,self._Resource=name,password,'' + try: + if self.sasl: auth.SASL(name,password).PlugIn(self) + if not self.sasl or self.SASL.startsasl=='not-supported': + if auth.NonSASL(name,password,'').PlugIn(self): + self.dobind(sasl=False) + self.connected+='+old_auth' + return 'old_auth' + return + self.SASL.auth() + while self.SASL.startsasl=='in-process' and self.Process(1): pass + if self.SASL.startsasl=='success': + self.dobind(sasl=True) + self.connected+='+sasl' + return 'sasl' + else: + raise auth.NotAuthorized(self.SASL.startsasl) + except: + self.DEBUG(self.DBG,"Failed to authenticate %s"%name,'error') diff --git a/src/xmpppy-0.5.0rc1/xmpp/commands.py b/src/xmpppy-0.5.0rc1/xmpp/commands.py new file mode 100644 index 00000000..cdebf8f2 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/xmpp/commands.py @@ -0,0 +1,328 @@ +## $Id: commands.py,v 1.17 2007/08/28 09:54:15 normanr Exp $ + +## Ad-Hoc Command manager +## Mike Albon (c) 5th January 2005 + +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + + +"""This module is a ad-hoc command processor for xmpppy. It uses the plug-in mechanism like most of the core library. It depends on a DISCO browser manager. + +There are 3 classes here, a command processor Commands like the Browser, and a command template plugin Command, and an example command. + +To use this module: + + Instansiate the module with the parent transport and disco browser manager as parameters. + 'Plug in' commands using the command template. + The command feature must be added to existing disco replies where neccessary. + +What it supplies: + + Automatic command registration with the disco browser manager. + Automatic listing of commands in the public command list. + A means of handling requests, by redirection though the command manager. +""" + +from protocol import * +from client import PlugIn + +class Commands(PlugIn): + """Commands is an ancestor of PlugIn and can be attached to any session. + + The commands class provides a lookup and browse mechnism. It follows the same priciple of the Browser class, for Service Discovery to provide the list of commands, it adds the 'list' disco type to your existing disco handler function. + + How it works: + The commands are added into the existing Browser on the correct nodes. When the command list is built the supplied discovery handler function needs to have a 'list' option in type. This then gets enumerated, all results returned as None are ignored. + The command executed is then called using it's Execute method. All session management is handled by the command itself. + """ + def __init__(self, browser): + """Initialises class and sets up local variables""" + PlugIn.__init__(self) + DBG_LINE='commands' + self._exported_methods=[] + self._handlers={'':{}} + self._browser = browser + + def plugin(self, owner): + """Makes handlers within the session""" + # Plug into the session and the disco manager + # We only need get and set, results are not needed by a service provider, only a service user. + owner.RegisterHandler('iq',self._CommandHandler,typ='set',ns=NS_COMMANDS) + owner.RegisterHandler('iq',self._CommandHandler,typ='get',ns=NS_COMMANDS) + self._browser.setDiscoHandler(self._DiscoHandler,node=NS_COMMANDS,jid='') + + def plugout(self): + """Removes handlers from the session""" + # unPlug from the session and the disco manager + self._owner.UnregisterHandler('iq',self._CommandHandler,ns=NS_COMMANDS) + for jid in self._handlers: + self._browser.delDiscoHandler(self._DiscoHandler,node=NS_COMMANDS) + + def _CommandHandler(self,conn,request): + """The internal method to process the routing of command execution requests""" + # This is the command handler itself. + # We must: + # Pass on command execution to command handler + # (Do we need to keep session details here, or can that be done in the command?) + jid = str(request.getTo()) + try: + node = request.getTagAttr('command','node') + except: + conn.send(Error(request,ERR_BAD_REQUEST)) + raise NodeProcessed + if self._handlers.has_key(jid): + if self._handlers[jid].has_key(node): + self._handlers[jid][node]['execute'](conn,request) + else: + conn.send(Error(request,ERR_ITEM_NOT_FOUND)) + raise NodeProcessed + elif self._handlers[''].has_key(node): + self._handlers[''][node]['execute'](conn,request) + else: + conn.send(Error(request,ERR_ITEM_NOT_FOUND)) + raise NodeProcessed + + def _DiscoHandler(self,conn,request,typ): + """The internal method to process service discovery requests""" + # This is the disco manager handler. + if typ == 'items': + # We must: + # Generate a list of commands and return the list + # * This handler does not handle individual commands disco requests. + # Pseudo: + # Enumerate the 'item' disco of each command for the specified jid + # Build responce and send + # To make this code easy to write we add an 'list' disco type, it returns a tuple or 'none' if not advertised + list = [] + items = [] + jid = str(request.getTo()) + # Get specific jid based results + if self._handlers.has_key(jid): + for each in self._handlers[jid].keys(): + items.append((jid,each)) + else: + # Get generic results + for each in self._handlers[''].keys(): + items.append(('',each)) + if items != []: + for each in items: + i = self._handlers[each[0]][each[1]]['disco'](conn,request,'list') + if i != None: + list.append(Node(tag='item',attrs={'jid':i[0],'node':i[1],'name':i[2]})) + iq = request.buildReply('result') + if request.getQuerynode(): iq.setQuerynode(request.getQuerynode()) + iq.setQueryPayload(list) + conn.send(iq) + else: + conn.send(Error(request,ERR_ITEM_NOT_FOUND)) + raise NodeProcessed + elif typ == 'info': + return {'ids':[{'category':'automation','type':'command-list'}],'features':[]} + + def addCommand(self,name,cmddisco,cmdexecute,jid=''): + """The method to call if adding a new command to the session, the requred parameters of cmddisco and cmdexecute are the methods to enable that command to be executed""" + # This command takes a command object and the name of the command for registration + # We must: + # Add item into disco + # Add item into command list + if not self._handlers.has_key(jid): + self._handlers[jid]={} + self._browser.setDiscoHandler(self._DiscoHandler,node=NS_COMMANDS,jid=jid) + if self._handlers[jid].has_key(name): + raise NameError,'Command Exists' + else: + self._handlers[jid][name]={'disco':cmddisco,'execute':cmdexecute} + # Need to add disco stuff here + self._browser.setDiscoHandler(cmddisco,node=name,jid=jid) + + def delCommand(self,name,jid=''): + """Removed command from the session""" + # This command takes a command object and the name used for registration + # We must: + # Remove item from disco + # Remove item from command list + if not self._handlers.has_key(jid): + raise NameError,'Jid not found' + if not self._handlers[jid].has_key(name): + raise NameError, 'Command not found' + else: + #Do disco removal here + command = self.getCommand(name,jid)['disco'] + del self._handlers[jid][name] + self._browser.delDiscoHandler(command,node=name,jid=jid) + + def getCommand(self,name,jid=''): + """Returns the command tuple""" + # This gets the command object with name + # We must: + # Return item that matches this name + if not self._handlers.has_key(jid): + raise NameError,'Jid not found' + elif not self._handlers[jid].has_key(name): + raise NameError,'Command not found' + else: + return self._handlers[jid][name] + +class Command_Handler_Prototype(PlugIn): + """This is a prototype command handler, as each command uses a disco method + and execute method you can implement it any way you like, however this is + my first attempt at making a generic handler that you can hang process + stages on too. There is an example command below. + + The parameters are as follows: + name : the name of the command within the jabber environment + description : the natural language description + discofeatures : the features supported by the command + initial : the initial command in the from of {'execute':commandname} + + All stages set the 'actions' dictionary for each session to represent the possible options available. + """ + name = 'examplecommand' + count = 0 + description = 'an example command' + discofeatures = [NS_COMMANDS,NS_DATA] + # This is the command template + def __init__(self,jid=''): + """Set up the class""" + PlugIn.__init__(self) + DBG_LINE='command' + self.sessioncount = 0 + self.sessions = {} + # Disco information for command list pre-formatted as a tuple + self.discoinfo = {'ids':[{'category':'automation','type':'command-node','name':self.description}],'features': self.discofeatures} + self._jid = jid + + def plugin(self,owner): + """Plug command into the commands class""" + # The owner in this instance is the Command Processor + self._commands = owner + self._owner = owner._owner + self._commands.addCommand(self.name,self._DiscoHandler,self.Execute,jid=self._jid) + + def plugout(self): + """Remove command from the commands class""" + self._commands.delCommand(self.name,self._jid) + + def getSessionID(self): + """Returns an id for the command session""" + self.count = self.count+1 + return 'cmd-%s-%d'%(self.name,self.count) + + def Execute(self,conn,request): + """The method that handles all the commands, and routes them to the correct method for that stage.""" + # New request or old? + try: + session = request.getTagAttr('command','sessionid') + except: + session = None + try: + action = request.getTagAttr('command','action') + except: + action = None + if action == None: action = 'execute' + # Check session is in session list + if self.sessions.has_key(session): + if self.sessions[session]['jid']==request.getFrom(): + # Check action is vaild + if self.sessions[session]['actions'].has_key(action): + # Execute next action + self.sessions[session]['actions'][action](conn,request) + else: + # Stage not presented as an option + self._owner.send(Error(request,ERR_BAD_REQUEST)) + raise NodeProcessed + else: + # Jid and session don't match. Go away imposter + self._owner.send(Error(request,ERR_BAD_REQUEST)) + raise NodeProcessed + elif session != None: + # Not on this sessionid you won't. + self._owner.send(Error(request,ERR_BAD_REQUEST)) + raise NodeProcessed + else: + # New session + self.initial[action](conn,request) + + def _DiscoHandler(self,conn,request,type): + """The handler for discovery events""" + if type == 'list': + return (request.getTo(),self.name,self.description) + elif type == 'items': + return [] + elif type == 'info': + return self.discoinfo + +class TestCommand(Command_Handler_Prototype): + """ Example class. You should read source if you wish to understate how it works. + Generally, it presents a "master" that giudes user through to calculate something. + """ + name = 'testcommand' + description = 'a noddy example command' + def __init__(self,jid=''): + """ Init internal constants. """ + Command_Handler_Prototype.__init__(self,jid) + self.initial = {'execute':self.cmdFirstStage} + + def cmdFirstStage(self,conn,request): + """ Determine """ + # This is the only place this should be repeated as all other stages should have SessionIDs + try: + session = request.getTagAttr('command','sessionid') + except: + session = None + if session == None: + session = self.getSessionID() + self.sessions[session]={'jid':request.getFrom(),'actions':{'cancel':self.cmdCancel,'next':self.cmdSecondStage,'execute':self.cmdSecondStage},'data':{'type':None}} + # As this is the first stage we only send a form + reply = request.buildReply('result') + form = DataForm(title='Select type of operation',data=['Use the combobox to select the type of calculation you would like to do, then click Next',DataField(name='calctype',desc='Calculation Type',value=self.sessions[session]['data']['type'],options=[['circlediameter','Calculate the Diameter of a circle'],['circlearea','Calculate the area of a circle']],typ='list-single',required=1)]) + replypayload = [Node('actions',attrs={'execute':'next'},payload=[Node('next')]),form] + reply.addChild(name='command',namespace=NS_COMMANDS,attrs={'node':request.getTagAttr('command','node'),'sessionid':session,'status':'executing'},payload=replypayload) + self._owner.send(reply) + raise NodeProcessed + + def cmdSecondStage(self,conn,request): + form = DataForm(node = request.getTag(name='command').getTag(name='x',namespace=NS_DATA)) + self.sessions[request.getTagAttr('command','sessionid')]['data']['type']=form.getField('calctype').getValue() + self.sessions[request.getTagAttr('command','sessionid')]['actions']={'cancel':self.cmdCancel,None:self.cmdThirdStage,'previous':self.cmdFirstStage,'execute':self.cmdThirdStage,'next':self.cmdThirdStage} + # The form generation is split out to another method as it may be called by cmdThirdStage + self.cmdSecondStageReply(conn,request) + + def cmdSecondStageReply(self,conn,request): + reply = request.buildReply('result') + form = DataForm(title = 'Enter the radius', data=['Enter the radius of the circle (numbers only)',DataField(desc='Radius',name='radius',typ='text-single')]) + replypayload = [Node('actions',attrs={'execute':'complete'},payload=[Node('complete'),Node('prev')]),form] + reply.addChild(name='command',namespace=NS_COMMANDS,attrs={'node':request.getTagAttr('command','node'),'sessionid':request.getTagAttr('command','sessionid'),'status':'executing'},payload=replypayload) + self._owner.send(reply) + raise NodeProcessed + + def cmdThirdStage(self,conn,request): + form = DataForm(node = request.getTag(name='command').getTag(name='x',namespace=NS_DATA)) + try: + num = float(form.getField('radius').getValue()) + except: + self.cmdSecondStageReply(conn,request) + from math import pi + if self.sessions[request.getTagAttr('command','sessionid')]['data']['type'] == 'circlearea': + result = (num**2)*pi + else: + result = num*2*pi + reply = request.buildReply('result') + form = DataForm(typ='result',data=[DataField(desc='result',name='result',value=result)]) + reply.addChild(name='command',namespace=NS_COMMANDS,attrs={'node':request.getTagAttr('command','node'),'sessionid':request.getTagAttr('command','sessionid'),'status':'completed'},payload=[form]) + self._owner.send(reply) + raise NodeProcessed + + def cmdCancel(self,conn,request): + reply = request.buildReply('result') + reply.addChild(name='command',namespace=NS_COMMANDS,attrs={'node':request.getTagAttr('command','node'),'sessionid':request.getTagAttr('command','sessionid'),'status':'cancelled'}) + self._owner.send(reply) + del self.sessions[request.getTagAttr('command','sessionid')] diff --git a/src/xmpppy-0.5.0rc1/xmpp/debug.py b/src/xmpppy-0.5.0rc1/xmpp/debug.py new file mode 100644 index 00000000..34ade88f --- /dev/null +++ b/src/xmpppy-0.5.0rc1/xmpp/debug.py @@ -0,0 +1,423 @@ +## debug.py +## +## Copyright (C) 2003 Jacob Lundqvist +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU Lesser General Public License as published +## by the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU Lesser General Public License for more details. + +_version_ = '1.4.0' + +"""\ + +Generic debug class + +Other modules can always define extra debug flags for local usage, as long as +they make sure they append them to debug_flags + +Also its always a good thing to prefix local flags with something, to reduce risk +of coliding flags. Nothing breaks if two flags would be identical, but it might +activate unintended debugging. + +flags can be numeric, but that makes analysing harder, on creation its +not obvious what is activated, and when flag_show is given, output isnt +really meaningfull. + +This Debug class can either be initialized and used on app level, or used independantly +by the individual classes. + +For samples of usage, see samples subdir in distro source, and selftest +in this code + +""" + + + +import sys +import traceback +import time +import os + +import types + +if os.environ.has_key('TERM'): + colors_enabled=True +else: + colors_enabled=False + +color_none = chr(27) + "[0m" +color_black = chr(27) + "[30m" +color_red = chr(27) + "[31m" +color_green = chr(27) + "[32m" +color_brown = chr(27) + "[33m" +color_blue = chr(27) + "[34m" +color_magenta = chr(27) + "[35m" +color_cyan = chr(27) + "[36m" +color_light_gray = chr(27) + "[37m" +color_dark_gray = chr(27) + "[30;1m" +color_bright_red = chr(27) + "[31;1m" +color_bright_green = chr(27) + "[32;1m" +color_yellow = chr(27) + "[33;1m" +color_bright_blue = chr(27) + "[34;1m" +color_purple = chr(27) + "[35;1m" +color_bright_cyan = chr(27) + "[36;1m" +color_white = chr(27) + "[37;1m" + + +""" +Define your flags in yor modules like this: + +from debug import * + +DBG_INIT = 'init' ; debug_flags.append( DBG_INIT ) +DBG_CONNECTION = 'connection' ; debug_flags.append( DBG_CONNECTION ) + + The reason for having a double statement wis so we can validate params + and catch all undefined debug flags + + This gives us control over all used flags, and makes it easier to allow + global debugging in your code, just do something like + + foo = Debug( debug_flags ) + + group flags, that is a flag in it self containing multiple flags should be + defined without the debug_flags.append() sequence, since the parts are already + in the list, also they must of course be defined after the flags they depend on ;) + example: + +DBG_MULTI = [ DBG_INIT, DBG_CONNECTION ] + + + + NoDebug + ------- + To speed code up, typically for product releases or such + use this class instead if you globaly want to disable debugging +""" + + +class NoDebug: + def __init__( self, *args, **kwargs ): + self.debug_flags = [] + def show( self, *args, **kwargs): + pass + def Show( self, *args, **kwargs): + pass + def is_active( self, flag ): + pass + colors={} + def active_set( self, active_flags = None ): + return 0 + + +LINE_FEED = '\n' + + +class Debug: + def __init__( self, + # + # active_flags are those that will trigger output + # + active_flags = None, + # + # Log file should be file object or file namne + # + log_file = sys.stderr, + # + # prefix and sufix can either be set globaly or per call. + # personally I use this to color code debug statements + # with prefix = chr(27) + '[34m' + # sufix = chr(27) + '[37;1m\n' + # + prefix = 'DEBUG: ', + sufix = '\n', + # + # If you want unix style timestamps, + # 0 disables timestamps + # 1 before prefix, good when prefix is a string + # 2 after prefix, good when prefix is a color + # + time_stamp = 0, + # + # flag_show should normaly be of, but can be turned on to get a + # good view of what flags are actually used for calls, + # if it is not None, it should be a string + # flags for current call will be displayed + # with flag_show as separator + # recomended values vould be '-' or ':', but any string goes + # + flag_show = None, + # + # If you dont want to validate flags on each call to + # show(), set this to 0 + # + validate_flags = 1, + # + # If you dont want the welcome message, set to 0 + # default is to show welcome if any flags are active + welcome = -1 + ): + + self.debug_flags = [] + if welcome == -1: + if active_flags and len(active_flags): + welcome = 1 + else: + welcome = 0 + + self._remove_dupe_flags() + if log_file: + if type( log_file ) is type(''): + try: + self._fh = open(log_file,'w') + except: + print 'ERROR: can open %s for writing' + sys.exit(0) + else: ## assume its a stream type object + self._fh = log_file + else: + self._fh = sys.stdout + + if time_stamp not in (0,1,2): + msg2 = '%s' % time_stamp + raise 'Invalid time_stamp param', msg2 + self.prefix = prefix + self.sufix = sufix + self.time_stamp = time_stamp + self.flag_show = None # must be initialised after possible welcome + self.validate_flags = validate_flags + + self.active_set( active_flags ) + if welcome: + self.show('') + caller = sys._getframe(1) # used to get name of caller + try: + mod_name= ":%s" % caller.f_locals['__name__'] + except: + mod_name = "" + self.show('Debug created for %s%s' % (caller.f_code.co_filename, + mod_name )) + self.show(' flags defined: %s' % ','.join( self.active )) + + if type(flag_show) in (type(''), type(None)): + self.flag_show = flag_show + else: + msg2 = '%s' % type(flag_show ) + raise 'Invalid type for flag_show!', msg2 + + + + + + def show( self, msg, flag = None, prefix = None, sufix = None, + lf = 0 ): + """ + flag can be of folowing types: + None - this msg will always be shown if any debugging is on + flag - will be shown if flag is active + (flag1,flag2,,,) - will be shown if any of the given flags + are active + + if prefix / sufix are not given, default ones from init will be used + + lf = -1 means strip linefeed if pressent + lf = 1 means add linefeed if not pressent + """ + + if self.validate_flags: + self._validate_flag( flag ) + + if not self.is_active(flag): + return + if prefix: + pre = prefix + else: + pre = self.prefix + if sufix: + suf = sufix + else: + suf = self.sufix + + if self.time_stamp == 2: + output = '%s%s ' % ( pre, + time.strftime('%b %d %H:%M:%S', + time.localtime(time.time() )), + ) + elif self.time_stamp == 1: + output = '%s %s' % ( time.strftime('%b %d %H:%M:%S', + time.localtime(time.time() )), + pre, + ) + else: + output = pre + + if self.flag_show: + if flag: + output = '%s%s%s' % ( output, flag, self.flag_show ) + else: + # this call uses the global default, + # dont print "None", just show the separator + output = '%s %s' % ( output, self.flag_show ) + + output = '%s%s%s' % ( output, msg, suf ) + if lf: + # strip/add lf if needed + last_char = output[-1] + if lf == 1 and last_char != LINE_FEED: + output = output + LINE_FEED + elif lf == -1 and last_char == LINE_FEED: + output = output[:-1] + try: + self._fh.write( output ) + except: + # unicode strikes again ;) + s=u'' + for i in range(len(output)): + if ord(output[i]) < 128: + c = output[i] + else: + c = '?' + s=s+c + self._fh.write( '%s%s%s' % ( pre, s, suf )) + self._fh.flush() + + + def is_active( self, flag ): + 'If given flag(s) should generate output.' + + # try to abort early to quicken code + if not self.active: + return 0 + if not flag or flag in self.active: + return 1 + else: + # check for multi flag type: + if type( flag ) in ( type(()), type([]) ): + for s in flag: + if s in self.active: + return 1 + return 0 + + + def active_set( self, active_flags = None ): + "returns 1 if any flags where actually set, otherwise 0." + r = 0 + ok_flags = [] + if not active_flags: + #no debuging at all + self.active = [] + elif type( active_flags ) in ( types.TupleType, types.ListType ): + flags = self._as_one_list( active_flags ) + for t in flags: + if t not in self.debug_flags: + sys.stderr.write('Invalid debugflag given: %s\n' % t ) + ok_flags.append( t ) + + self.active = ok_flags + r = 1 + else: + # assume comma string + try: + flags = active_flags.split(',') + except: + self.show( '***' ) + self.show( '*** Invalid debug param given: %s' % active_flags ) + self.show( '*** please correct your param!' ) + self.show( '*** due to this, full debuging is enabled' ) + self.active = self.debug_flags + + for f in flags: + s = f.strip() + ok_flags.append( s ) + self.active = ok_flags + + self._remove_dupe_flags() + return r + + def active_get( self ): + "returns currently active flags." + return self.active + + + def _as_one_list( self, items ): + """ init param might contain nested lists, typically from group flags. + + This code organises lst and remves dupes + """ + if type( items ) <> type( [] ) and type( items ) <> type( () ): + return [ items ] + r = [] + for l in items: + if type( l ) == type([]): + lst2 = self._as_one_list( l ) + for l2 in lst2: + self._append_unique_str(r, l2 ) + elif l == None: + continue + else: + self._append_unique_str(r, l ) + return r + + + def _append_unique_str( self, lst, item ): + """filter out any dupes.""" + if type(item) <> type(''): + msg2 = '%s' % item + raise 'Invalid item type (should be string)',msg2 + if item not in lst: + lst.append( item ) + return lst + + + def _validate_flag( self, flags ): + 'verify that flag is defined.' + if flags: + for f in self._as_one_list( flags ): + if not f in self.debug_flags: + msg2 = '%s' % f + raise 'Invalid debugflag given', msg2 + + def _remove_dupe_flags( self ): + """ + if multiple instances of Debug is used in same app, + some flags might be created multiple time, filter out dupes + """ + unique_flags = [] + for f in self.debug_flags: + if f not in unique_flags: + unique_flags.append(f) + self.debug_flags = unique_flags + + colors={} + def Show(self, flag, msg, prefix=''): + msg=msg.replace('\r','\\r').replace('\n','\\n').replace('><','>\n <') + if not colors_enabled: pass + elif self.colors.has_key(prefix): msg=self.colors[prefix]+msg+color_none + else: msg=color_none+msg + if not colors_enabled: prefixcolor='' + elif self.colors.has_key(flag): prefixcolor=self.colors[flag] + else: prefixcolor=color_none + + if prefix=='error': + _exception = sys.exc_info() + if _exception[0]: + msg=msg+'\n'+''.join(traceback.format_exception(_exception[0], _exception[1], _exception[2])).rstrip() + + prefix= self.prefix+prefixcolor+(flag+' '*12)[:12]+' '+(prefix+' '*6)[:6] + self.show(msg, flag, prefix) + + def is_active( self, flag ): + if not self.active: return 0 + if not flag or flag in self.active and DBG_ALWAYS not in self.active or flag not in self.active and DBG_ALWAYS in self.active : return 1 + return 0 + +DBG_ALWAYS='always' + +##Uncomment this to effectively disable all debugging and all debugging overhead. +#Debug=NoDebug diff --git a/src/xmpppy-0.5.0rc1/xmpp/dispatcher.py b/src/xmpppy-0.5.0rc1/xmpp/dispatcher.py new file mode 100644 index 00000000..cc94ee04 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/xmpp/dispatcher.py @@ -0,0 +1,373 @@ +## transports.py +## +## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: dispatcher.py,v 1.42 2007/05/18 23:18:36 normanr Exp $ + +""" +Main xmpppy mechanism. Provides library with methods to assign different handlers +to different XMPP stanzas. +Contains one tunable attribute: DefaultTimeout (25 seconds by default). It defines time that +Dispatcher.SendAndWaitForResponce method will wait for reply stanza before giving up. +""" + +import simplexml,time,sys +from protocol import * +from client import PlugIn + +DefaultTimeout=25 +ID=0 + +class Dispatcher(PlugIn): + """ Ancestor of PlugIn class. Handles XMPP stream, i.e. aware of stream headers. + Can be plugged out/in to restart these headers (used for SASL f.e.). """ + def __init__(self): + PlugIn.__init__(self) + DBG_LINE='dispatcher' + self.handlers={} + self._expected={} + self._defaultHandler=None + self._pendingExceptions=[] + self._eventHandler=None + self._cycleHandlers=[] + self._exported_methods=[self.Process,self.RegisterHandler,self.RegisterDefaultHandler,\ + self.RegisterEventHandler,self.UnregisterCycleHandler,self.RegisterCycleHandler,\ + self.RegisterHandlerOnce,self.UnregisterHandler,self.RegisterProtocol,\ + self.WaitForResponse,self.SendAndWaitForResponse,self.send,self.disconnect,\ + self.SendAndCallForResponse, ] + + def dumpHandlers(self): + """ Return set of user-registered callbacks in it's internal format. + Used within the library to carry user handlers set over Dispatcher replugins. """ + return self.handlers + def restoreHandlers(self,handlers): + """ Restores user-registered callbacks structure from dump previously obtained via dumpHandlers. + Used within the library to carry user handlers set over Dispatcher replugins. """ + self.handlers=handlers + + def _init(self): + """ Registers default namespaces/protocols/handlers. Used internally. """ + self.RegisterNamespace('unknown') + self.RegisterNamespace(NS_STREAMS) + self.RegisterNamespace(self._owner.defaultNamespace) + self.RegisterProtocol('iq',Iq) + self.RegisterProtocol('presence',Presence) + self.RegisterProtocol('message',Message) + self.RegisterDefaultHandler(self.returnStanzaHandler) + self.RegisterHandler('error',self.streamErrorHandler,xmlns=NS_STREAMS) + + def plugin(self, owner): + """ Plug the Dispatcher instance into Client class instance and send initial stream header. Used internally.""" + self._init() + for method in self._old_owners_methods: + if method.__name__=='send': self._owner_send=method; break + self._owner.lastErrNode=None + self._owner.lastErr=None + self._owner.lastErrCode=None + self.StreamInit() + + def plugout(self): + """ Prepares instance to be destructed. """ + self.Stream.dispatch=None + self.Stream.DEBUG=None + self.Stream.features=None + self.Stream.destroy() + + def StreamInit(self): + """ Send an initial stream header. """ + self.Stream=simplexml.NodeBuilder() + self.Stream._dispatch_depth=2 + self.Stream.dispatch=self.dispatch + self.Stream.stream_header_received=self._check_stream_start + self._owner.debug_flags.append(simplexml.DBG_NODEBUILDER) + self.Stream.DEBUG=self._owner.DEBUG + self.Stream.features=None + self._metastream=Node('stream:stream') + self._metastream.setNamespace(self._owner.Namespace) + self._metastream.setAttr('version','1.0') + self._metastream.setAttr('xmlns:stream',NS_STREAMS) + self._metastream.setAttr('to',self._owner.Server) + self._owner.send("<?xml version='1.0'?>%s>"%str(self._metastream)[:-2]) + + def _check_stream_start(self,ns,tag,attrs): + if ns<>NS_STREAMS or tag<>'stream': + raise ValueError('Incorrect stream start: (%s,%s). Terminating.'%(tag,ns)) + + def Process(self, timeout=0): + """ Check incoming stream for data waiting. If "timeout" is positive - block for as max. this time. + Returns: + 1) length of processed data if some data were processed; + 2) '0' string if no data were processed but link is alive; + 3) 0 (zero) if underlying connection is closed. + Take note that in case of disconnection detect during Process() call + disconnect handlers are called automatically. + """ + for handler in self._cycleHandlers: handler(self) + if len(self._pendingExceptions) > 0: + _pendingException = self._pendingExceptions.pop() + raise _pendingException[0], _pendingException[1], _pendingException[2] + if self._owner.Connection.pending_data(timeout): + try: data=self._owner.Connection.receive() + except IOError: return + self.Stream.Parse(data) + if len(self._pendingExceptions) > 0: + _pendingException = self._pendingExceptions.pop() + raise _pendingException[0], _pendingException[1], _pendingException[2] + if data: return len(data) + return '0' # It means that nothing is received but link is alive. + + def RegisterNamespace(self,xmlns,order='info'): + """ Creates internal structures for newly registered namespace. + You can register handlers for this namespace afterwards. By default one namespace + already registered (jabber:client or jabber:component:accept depending on context. """ + self.DEBUG('Registering namespace "%s"'%xmlns,order) + self.handlers[xmlns]={} + self.RegisterProtocol('unknown',Protocol,xmlns=xmlns) + self.RegisterProtocol('default',Protocol,xmlns=xmlns) + + def RegisterProtocol(self,tag_name,Proto,xmlns=None,order='info'): + """ Used to declare some top-level stanza name to dispatcher. + Needed to start registering handlers for such stanzas. + Iq, message and presence protocols are registered by default. """ + if not xmlns: xmlns=self._owner.defaultNamespace + self.DEBUG('Registering protocol "%s" as %s(%s)'%(tag_name,Proto,xmlns), order) + self.handlers[xmlns][tag_name]={type:Proto, 'default':[]} + + def RegisterNamespaceHandler(self,xmlns,handler,typ='',ns='', makefirst=0, system=0): + """ Register handler for processing all stanzas for specified namespace. """ + self.RegisterHandler('default', handler, typ, ns, xmlns, makefirst, system) + + def RegisterHandler(self,name,handler,typ='',ns='',xmlns=None, makefirst=0, system=0): + """Register user callback as stanzas handler of declared type. Callback must take + (if chained, see later) arguments: dispatcher instance (for replying), incomed + return of previous handlers. + The callback must raise xmpp.NodeProcessed just before return if it want preven + callbacks to be called with the same stanza as argument _and_, more importantly + library from returning stanza to sender with error set (to be enabled in 0.2 ve + Arguments: + "name" - name of stanza. F.e. "iq". + "handler" - user callback. + "typ" - value of stanza's "type" attribute. If not specified any value match + "ns" - namespace of child that stanza must contain. + "chained" - chain together output of several handlers. + "makefirst" - insert handler in the beginning of handlers list instead of + adding it to the end. Note that more common handlers (i.e. w/o "typ" and " + will be called first nevertheless. + "system" - call handler even if NodeProcessed Exception were raised already. + """ + if not xmlns: xmlns=self._owner.defaultNamespace + self.DEBUG('Registering handler %s for "%s" type->%s ns->%s(%s)'%(handler,name,typ,ns,xmlns), 'info') + if not typ and not ns: typ='default' + if not self.handlers.has_key(xmlns): self.RegisterNamespace(xmlns,'warn') + if not self.handlers[xmlns].has_key(name): self.RegisterProtocol(name,Protocol,xmlns,'warn') + if not self.handlers[xmlns][name].has_key(typ+ns): self.handlers[xmlns][name][typ+ns]=[] + if makefirst: self.handlers[xmlns][name][typ+ns].insert(0,{'func':handler,'system':system}) + else: self.handlers[xmlns][name][typ+ns].append({'func':handler,'system':system}) + + def RegisterHandlerOnce(self,name,handler,typ='',ns='',xmlns=None,makefirst=0, system=0): + """ Unregister handler after first call (not implemented yet). """ + if not xmlns: xmlns=self._owner.defaultNamespace + self.RegisterHandler(name, handler, typ, ns, xmlns, makefirst, system) + + def UnregisterHandler(self,name,handler,typ='',ns='',xmlns=None): + """ Unregister handler. "typ" and "ns" must be specified exactly the same as with registering.""" + if not xmlns: xmlns=self._owner.defaultNamespace + if not self.handlers.has_key(xmlns): return + if not typ and not ns: typ='default' + for pack in self.handlers[xmlns][name][typ+ns]: + if handler==pack['func']: break + else: pack=None + try: self.handlers[xmlns][name][typ+ns].remove(pack) + except ValueError: pass + + def RegisterDefaultHandler(self,handler): + """ Specify the handler that will be used if no NodeProcessed exception were raised. + This is returnStanzaHandler by default. """ + self._defaultHandler=handler + + def RegisterEventHandler(self,handler): + """ Register handler that will process events. F.e. "FILERECEIVED" event. """ + self._eventHandler=handler + + def returnStanzaHandler(self,conn,stanza): + """ Return stanza back to the sender with <feature-not-implemennted/> error set. """ + if stanza.getType() in ['get','set']: + conn.send(Error(stanza,ERR_FEATURE_NOT_IMPLEMENTED)) + + def streamErrorHandler(self,conn,error): + name,text='error',error.getData() + for tag in error.getChildren(): + if tag.getNamespace()==NS_XMPP_STREAMS: + if tag.getName()=='text': text=tag.getData() + else: name=tag.getName() + if name in stream_exceptions.keys(): exc=stream_exceptions[name] + else: exc=StreamError + raise exc((name,text)) + + def RegisterCycleHandler(self,handler): + """ Register handler that will be called on every Dispatcher.Process() call. """ + if handler not in self._cycleHandlers: self._cycleHandlers.append(handler) + + def UnregisterCycleHandler(self,handler): + """ Unregister handler that will is called on every Dispatcher.Process() call.""" + if handler in self._cycleHandlers: self._cycleHandlers.remove(handler) + + def Event(self,realm,event,data): + """ Raise some event. Takes three arguments: + 1) "realm" - scope of event. Usually a namespace. + 2) "event" - the event itself. F.e. "SUCESSFULL SEND". + 3) data that comes along with event. Depends on event.""" + if self._eventHandler: self._eventHandler(realm,event,data) + + def dispatch(self,stanza,session=None,direct=0): + """ Main procedure that performs XMPP stanza recognition and calling apppropriate handlers for it. + Called internally. """ + if not session: session=self + session.Stream._mini_dom=None + name=stanza.getName() + + if not direct and self._owner._route: + if name == 'route': + if stanza.getAttr('error') == None: + if len(stanza.getChildren()) == 1: + stanza = stanza.getChildren()[0] + name=stanza.getName() + else: + for each in stanza.getChildren(): + self.dispatch(each,session,direct=1) + return + elif name == 'presence': + return + elif name in ('features','bind'): + pass + else: + raise UnsupportedStanzaType(name) + + if name=='features': session.Stream.features=stanza + + xmlns=stanza.getNamespace() + if not self.handlers.has_key(xmlns): + self.DEBUG("Unknown namespace: " + xmlns,'warn') + xmlns='unknown' + if not self.handlers[xmlns].has_key(name): + self.DEBUG("Unknown stanza: " + name,'warn') + name='unknown' + else: + self.DEBUG("Got %s/%s stanza"%(xmlns,name), 'ok') + + if stanza.__class__.__name__=='Node': stanza=self.handlers[xmlns][name][type](node=stanza) + + typ=stanza.getType() + if not typ: typ='' + stanza.props=stanza.getProperties() + ID=stanza.getID() + + session.DEBUG("Dispatching %s stanza with type->%s props->%s id->%s"%(name,typ,stanza.props,ID),'ok') + + list=['default'] # we will use all handlers: + if self.handlers[xmlns][name].has_key(typ): list.append(typ) # from very common... + for prop in stanza.props: + if self.handlers[xmlns][name].has_key(prop): list.append(prop) + if typ and self.handlers[xmlns][name].has_key(typ+prop): list.append(typ+prop) # ...to very particular + + chain=self.handlers[xmlns]['default']['default'] + for key in list: + if key: chain = chain + self.handlers[xmlns][name][key] + + output='' + if session._expected.has_key(ID): + user=0 + if type(session._expected[ID])==type(()): + cb,args=session._expected[ID] + session.DEBUG("Expected stanza arrived. Callback %s(%s) found!"%(cb,args),'ok') + try: cb(session,stanza,**args) + except Exception, typ: + if typ.__class__.__name__<>'NodeProcessed': raise + else: + session.DEBUG("Expected stanza arrived!",'ok') + session._expected[ID]=stanza + else: user=1 + for handler in chain: + if user or handler['system']: + try: + handler['func'](session,stanza) + except Exception, typ: + if typ.__class__.__name__<>'NodeProcessed': + self._pendingExceptions.insert(0, sys.exc_info()) + return + user=0 + if user and self._defaultHandler: self._defaultHandler(session,stanza) + + def WaitForResponse(self, ID, timeout=DefaultTimeout): + """ Block and wait until stanza with specific "id" attribute will come. + If no such stanza is arrived within timeout, return None. + If operation failed for some reason then owner's attributes + lastErrNode, lastErr and lastErrCode are set accordingly. """ + self._expected[ID]=None + has_timed_out=0 + abort_time=time.time() + timeout + self.DEBUG("Waiting for ID:%s with timeout %s..." % (ID,timeout),'wait') + while not self._expected[ID]: + if not self.Process(0.04): + self._owner.lastErr="Disconnect" + return None + if time.time() > abort_time: + self._owner.lastErr="Timeout" + return None + response=self._expected[ID] + del self._expected[ID] + if response.getErrorCode(): + self._owner.lastErrNode=response + self._owner.lastErr=response.getError() + self._owner.lastErrCode=response.getErrorCode() + return response + + def SendAndWaitForResponse(self, stanza, timeout=DefaultTimeout): + """ Put stanza on the wire and wait for recipient's response to it. """ + return self.WaitForResponse(self.send(stanza),timeout) + + def SendAndCallForResponse(self, stanza, func, args={}): + """ Put stanza on the wire and call back when recipient replies. + Additional callback arguments can be specified in args. """ + self._expected[self.send(stanza)]=(func,args) + + def send(self,stanza): + """ Serialise stanza and put it on the wire. Assign an unique ID to it before send. + Returns assigned ID.""" + if type(stanza) in [type(''), type(u'')]: return self._owner_send(stanza) + if not isinstance(stanza,Protocol): _ID=None + elif not stanza.getID(): + global ID + ID+=1 + _ID=`ID` + stanza.setID(_ID) + else: _ID=stanza.getID() + if self._owner._registered_name and not stanza.getAttr('from'): stanza.setAttr('from',self._owner._registered_name) + if self._owner._route and stanza.getName()!='bind': + to=self._owner.Server + if stanza.getTo() and stanza.getTo().getDomain(): + to=stanza.getTo().getDomain() + frm=stanza.getFrom() + if frm.getDomain(): + frm=frm.getDomain() + route=Protocol('route',to=to,frm=frm,payload=[stanza]) + stanza=route + stanza.setNamespace(self._owner.Namespace) + stanza.setParent(self._metastream) + self._owner_send(stanza) + return _ID + + def disconnect(self): + """ Send a stream terminator and and handle all incoming stanzas before stream closure. """ + self._owner_send('</stream:stream>') + while self.Process(1): pass diff --git a/src/xmpppy-0.5.0rc1/xmpp/features.py b/src/xmpppy-0.5.0rc1/xmpp/features.py new file mode 100644 index 00000000..c7993c29 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/xmpp/features.py @@ -0,0 +1,182 @@ +## features.py +## +## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: features.py,v 1.25 2009/04/07 07:11:48 snakeru Exp $ + +""" +This module contains variable stuff that is not worth splitting into separate modules. +Here is: + DISCO client and agents-to-DISCO and browse-to-DISCO emulators. + IBR and password manager. + jabber:iq:privacy methods +All these methods takes 'disp' first argument that should be already connected +(and in most cases already authorised) dispatcher instance. +""" + +from protocol import * + +REGISTER_DATA_RECEIVED='REGISTER DATA RECEIVED' + +### DISCO ### http://jabber.org/protocol/disco ### JEP-0030 #################### +### Browse ### jabber:iq:browse ### JEP-0030 ################################### +### Agents ### jabber:iq:agents ### JEP-0030 ################################### +def _discover(disp,ns,jid,node=None,fb2b=0,fb2a=1): + """ Try to obtain info from the remote object. + If remote object doesn't support disco fall back to browse (if fb2b is true) + and if it doesnt support browse (or fb2b is not true) fall back to agents protocol + (if gb2a is true). Returns obtained info. Used internally. """ + iq=Iq(to=jid,typ='get',queryNS=ns) + if node: iq.setQuerynode(node) + rep=disp.SendAndWaitForResponse(iq) + if fb2b and not isResultNode(rep): rep=disp.SendAndWaitForResponse(Iq(to=jid,typ='get',queryNS=NS_BROWSE)) # Fallback to browse + if fb2a and not isResultNode(rep): rep=disp.SendAndWaitForResponse(Iq(to=jid,typ='get',queryNS=NS_AGENTS)) # Fallback to agents + if isResultNode(rep): return [n for n in rep.getQueryPayload() if isinstance(n, Node)] + return [] + +def discoverItems(disp,jid,node=None): + """ Query remote object about any items that it contains. Return items list. """ + """ According to JEP-0030: + query MAY have node attribute + item: MUST HAVE jid attribute and MAY HAVE name, node, action attributes. + action attribute of item can be either of remove or update value.""" + ret=[] + for i in _discover(disp,NS_DISCO_ITEMS,jid,node): + if i.getName()=='agent' and i.getTag('name'): i.setAttr('name',i.getTagData('name')) + ret.append(i.attrs) + return ret + +def discoverInfo(disp,jid,node=None): + """ Query remote object about info that it publishes. Returns identities and features lists.""" + """ According to JEP-0030: + query MAY have node attribute + identity: MUST HAVE category and name attributes and MAY HAVE type attribute. + feature: MUST HAVE var attribute""" + identities , features = [] , [] + for i in _discover(disp,NS_DISCO_INFO,jid,node): + if i.getName()=='identity': identities.append(i.attrs) + elif i.getName()=='feature': features.append(i.getAttr('var')) + elif i.getName()=='agent': + if i.getTag('name'): i.setAttr('name',i.getTagData('name')) + if i.getTag('description'): i.setAttr('name',i.getTagData('description')) + identities.append(i.attrs) + if i.getTag('groupchat'): features.append(NS_GROUPCHAT) + if i.getTag('register'): features.append(NS_REGISTER) + if i.getTag('search'): features.append(NS_SEARCH) + return identities , features + +### Registration ### jabber:iq:register ### JEP-0077 ########################### +def getRegInfo(disp,host,info={},sync=True): + """ Gets registration form from remote host. + You can pre-fill the info dictionary. + F.e. if you are requesting info on registering user joey than specify + info as {'username':'joey'}. See JEP-0077 for details. + 'disp' must be connected dispatcher instance.""" + iq=Iq('get',NS_REGISTER,to=host) + for i in info.keys(): iq.setTagData(i,info[i]) + if sync: + resp=disp.SendAndWaitForResponse(iq) + _ReceivedRegInfo(disp.Dispatcher,resp, host) + return resp + else: disp.SendAndCallForResponse(iq,_ReceivedRegInfo, {'agent': host}) + +def _ReceivedRegInfo(con, resp, agent): + iq=Iq('get',NS_REGISTER,to=agent) + if not isResultNode(resp): return + df=resp.getTag('query',namespace=NS_REGISTER).getTag('x',namespace=NS_DATA) + if df: + con.Event(NS_REGISTER,REGISTER_DATA_RECEIVED,(agent, DataForm(node=df))) + return + df=DataForm(typ='form') + for i in resp.getQueryPayload(): + if type(i)<>type(iq): pass + elif i.getName()=='instructions': df.addInstructions(i.getData()) + else: df.setField(i.getName()).setValue(i.getData()) + con.Event(NS_REGISTER,REGISTER_DATA_RECEIVED,(agent, df)) + +def register(disp,host,info): + """ Perform registration on remote server with provided info. + disp must be connected dispatcher instance. + Returns true or false depending on registration result. + If registration fails you can get additional info from the dispatcher's owner + attributes lastErrNode, lastErr and lastErrCode. + """ + iq=Iq('set',NS_REGISTER,to=host) + if type(info)<>type({}): info=info.asDict() + for i in info.keys(): iq.setTag('query').setTagData(i,info[i]) + resp=disp.SendAndWaitForResponse(iq) + if isResultNode(resp): return 1 + +def unregister(disp,host): + """ Unregisters with host (permanently removes account). + disp must be connected and authorized dispatcher instance. + Returns true on success.""" + resp=disp.SendAndWaitForResponse(Iq('set',NS_REGISTER,to=host,payload=[Node('remove')])) + if isResultNode(resp): return 1 + +def changePasswordTo(disp,newpassword,host=None): + """ Changes password on specified or current (if not specified) server. + disp must be connected and authorized dispatcher instance. + Returns true on success.""" + if not host: host=disp._owner.Server + resp=disp.SendAndWaitForResponse(Iq('set',NS_REGISTER,to=host,payload=[Node('username',payload=[disp._owner.Server]),Node('password',payload=[newpassword])])) + if isResultNode(resp): return 1 + +### Privacy ### jabber:iq:privacy ### draft-ietf-xmpp-im-19 #################### +#type=[jid|group|subscription] +#action=[allow|deny] + +def getPrivacyLists(disp): + """ Requests privacy lists from connected server. + Returns dictionary of existing lists on success.""" + try: + dict={'lists':[]} + resp=disp.SendAndWaitForResponse(Iq('get',NS_PRIVACY)) + if not isResultNode(resp): return + for list in resp.getQueryPayload(): + if list.getName()=='list': dict['lists'].append(list.getAttr('name')) + else: dict[list.getName()]=list.getAttr('name') + return dict + except: pass + +def getPrivacyList(disp,listname): + """ Requests specific privacy list listname. Returns list of XML nodes (rules) + taken from the server responce.""" + try: + resp=disp.SendAndWaitForResponse(Iq('get',NS_PRIVACY,payload=[Node('list',{'name':listname})])) + if isResultNode(resp): return resp.getQueryPayload()[0] + except: pass + +def setActivePrivacyList(disp,listname=None,typ='active'): + """ Switches privacy list 'listname' to specified type. + By default the type is 'active'. Returns true on success.""" + if listname: attrs={'name':listname} + else: attrs={} + resp=disp.SendAndWaitForResponse(Iq('set',NS_PRIVACY,payload=[Node(typ,attrs)])) + if isResultNode(resp): return 1 + +def setDefaultPrivacyList(disp,listname=None): + """ Sets the default privacy list as 'listname'. Returns true on success.""" + return setActivePrivacyList(disp,listname,'default') + +def setPrivacyList(disp,list): + """ Set the ruleset. 'list' should be the simpleXML node formatted + according to RFC 3921 (XMPP-IM) (I.e. Node('list',{'name':listname},payload=[...]) ) + Returns true on success.""" + resp=disp.SendAndWaitForResponse(Iq('set',NS_PRIVACY,payload=[list])) + if isResultNode(resp): return 1 + +def delPrivacyList(disp,listname): + """ Deletes privacy list 'listname'. Returns true on success.""" + resp=disp.SendAndWaitForResponse(Iq('set',NS_PRIVACY,payload=[Node('list',{'name':listname})])) + if isResultNode(resp): return 1 diff --git a/src/xmpppy-0.5.0rc1/xmpp/filetransfer.py b/src/xmpppy-0.5.0rc1/xmpp/filetransfer.py new file mode 100644 index 00000000..87ddc219 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/xmpp/filetransfer.py @@ -0,0 +1,199 @@ +## filetransfer.py +## +## Copyright (C) 2004 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: filetransfer.py,v 1.6 2004/12/25 20:06:59 snakeru Exp $ + +""" +This module contains IBB class that is the simple implementation of JEP-0047. +Note that this is just a transport for data. You have to negotiate data transfer before +(via StreamInitiation most probably). Unfortunately SI is not implemented yet. +""" + +from protocol import * +from dispatcher import PlugIn +import base64 + +class IBB(PlugIn): + """ IBB used to transfer small-sized data chunk over estabilished xmpp connection. + Data is split into small blocks (by default 3000 bytes each), encoded as base 64 + and sent to another entity that compiles these blocks back into the data chunk. + This is very inefficiend but should work under any circumstances. Note that + using IBB normally should be the last resort. + """ + def __init__(self): + """ Initialise internal variables. """ + PlugIn.__init__(self) + self.DBG_LINE='ibb' + self._exported_methods=[self.OpenStream] + self._streams={} + self._ampnode=Node(NS_AMP+' amp',payload=[Node('rule',{'condition':'deliver-at','value':'stored','action':'error'}),Node('rule',{'condition':'match-resource','value':'exact','action':'error'})]) + + def plugin(self,owner): + """ Register handlers for receiving incoming datastreams. Used internally. """ + self._owner.RegisterHandlerOnce('iq',self.StreamOpenReplyHandler) # Move to StreamOpen and specify stanza id + self._owner.RegisterHandler('iq',self.IqHandler,ns=NS_IBB) + self._owner.RegisterHandler('message',self.ReceiveHandler,ns=NS_IBB) + + def IqHandler(self,conn,stanza): + """ Handles streams state change. Used internally. """ + typ=stanza.getType() + self.DEBUG('IqHandler called typ->%s'%typ,'info') + if typ=='set' and stanza.getTag('open',namespace=NS_IBB): self.StreamOpenHandler(conn,stanza) + elif typ=='set' and stanza.getTag('close',namespace=NS_IBB): self.StreamCloseHandler(conn,stanza) + elif typ=='result': self.StreamCommitHandler(conn,stanza) + elif typ=='error': self.StreamOpenReplyHandler(conn,stanza) + else: conn.send(Error(stanza,ERR_BAD_REQUEST)) + raise NodeProcessed + + def StreamOpenHandler(self,conn,stanza): + """ Handles opening of new incoming stream. Used internally. """ + """ +<iq type='set' + from='romeo@montague.net/orchard' + to='juliet@capulet.com/balcony' + id='inband_1'> + <open sid='mySID' + block-size='4096' + xmlns='http://jabber.org/protocol/ibb'/> +</iq> +""" + err=None + sid,blocksize=stanza.getTagAttr('open','sid'),stanza.getTagAttr('open','block-size') + self.DEBUG('StreamOpenHandler called sid->%s blocksize->%s'%(sid,blocksize),'info') + try: blocksize=int(blocksize) + except: err=ERR_BAD_REQUEST + if not sid or not blocksize: err=ERR_BAD_REQUEST + elif sid in self._streams.keys(): err=ERR_UNEXPECTED_REQUEST + if err: rep=Error(stanza,err) + else: + self.DEBUG("Opening stream: id %s, block-size %s"%(sid,blocksize),'info') + rep=Protocol('iq',stanza.getFrom(),'result',stanza.getTo(),{'id':stanza.getID()}) + self._streams[sid]={'direction':'<'+str(stanza.getFrom()),'block-size':blocksize,'fp':open('/tmp/xmpp_file_'+sid,'w'),'seq':0,'syn_id':stanza.getID()} + conn.send(rep) + + def OpenStream(self,sid,to,fp,blocksize=3000): + """ Start new stream. You should provide stream id 'sid', the endpoind jid 'to', + the file object containing info for send 'fp'. Also the desired blocksize can be specified. + Take into account that recommended stanza size is 4k and IBB uses base64 encoding + that increases size of data by 1/3.""" + if sid in self._streams.keys(): return + if not JID(to).getResource(): return + self._streams[sid]={'direction':'|>'+to,'block-size':blocksize,'fp':fp,'seq':0} + self._owner.RegisterCycleHandler(self.SendHandler) + syn=Protocol('iq',to,'set',payload=[Node(NS_IBB+' open',{'sid':sid,'block-size':blocksize})]) + self._owner.send(syn) + self._streams[sid]['syn_id']=syn.getID() + return self._streams[sid] + + def SendHandler(self,conn): + """ Send next portion of data if it is time to do it. Used internally. """ + self.DEBUG('SendHandler called','info') + for sid in self._streams.keys(): + stream=self._streams[sid] + if stream['direction'][:2]=='|>': cont=1 + elif stream['direction'][0]=='>': + chunk=stream['fp'].read(stream['block-size']) + if chunk: + datanode=Node(NS_IBB+' data',{'sid':sid,'seq':stream['seq']},base64.encodestring(chunk)) + stream['seq']+=1 + if stream['seq']==65536: stream['seq']=0 + conn.send(Protocol('message',stream['direction'][1:],payload=[datanode,self._ampnode])) + else: + """ notify the other side about stream closing + notify the local user about sucessfull send + delete the local stream""" + conn.send(Protocol('iq',stream['direction'][1:],'set',payload=[Node(NS_IBB+' close',{'sid':sid})])) + conn.Event(self.DBG_LINE,'SUCCESSFULL SEND',stream) + del self._streams[sid] + self._owner.UnregisterCycleHandler(self.SendHandler) + + """ +<message from='romeo@montague.net/orchard' to='juliet@capulet.com/balcony' id='msg1'> + <data xmlns='http://jabber.org/protocol/ibb' sid='mySID' seq='0'> + qANQR1DBwU4DX7jmYZnncmUQB/9KuKBddzQH+tZ1ZywKK0yHKnq57kWq+RFtQdCJ + WpdWpR0uQsuJe7+vh3NWn59/gTc5MDlX8dS9p0ovStmNcyLhxVgmqS8ZKhsblVeu + IpQ0JgavABqibJolc3BKrVtVV1igKiX/N7Pi8RtY1K18toaMDhdEfhBRzO/XB0+P + AQhYlRjNacGcslkhXqNjK5Va4tuOAPy2n1Q8UUrHbUd0g+xJ9Bm0G0LZXyvCWyKH + kuNEHFQiLuCY6Iv0myq6iX6tjuHehZlFSh80b5BVV9tNLwNR5Eqz1klxMhoghJOA + </data> + <amp xmlns='http://jabber.org/protocol/amp'> + <rule condition='deliver-at' value='stored' action='error'/> + <rule condition='match-resource' value='exact' action='error'/> + </amp> +</message> +""" + + def ReceiveHandler(self,conn,stanza): + """ Receive next portion of incoming datastream and store it write + it to temporary file. Used internally. + """ + sid,seq,data=stanza.getTagAttr('data','sid'),stanza.getTagAttr('data','seq'),stanza.getTagData('data') + self.DEBUG('ReceiveHandler called sid->%s seq->%s'%(sid,seq),'info') + try: seq=int(seq); data=base64.decodestring(data) + except: seq=''; data='' + err=None + if not sid in self._streams.keys(): err=ERR_ITEM_NOT_FOUND + else: + stream=self._streams[sid] + if not data: err=ERR_BAD_REQUEST + elif seq<>stream['seq']: err=ERR_UNEXPECTED_REQUEST + else: + self.DEBUG('Successfull receive sid->%s %s+%s bytes'%(sid,stream['fp'].tell(),len(data)),'ok') + stream['seq']+=1 + stream['fp'].write(data) + if err: + self.DEBUG('Error on receive: %s'%err,'error') + conn.send(Error(Iq(to=stanza.getFrom(),frm=stanza.getTo(),payload=[Node(NS_IBB+' close')]),err,reply=0)) + + def StreamCloseHandler(self,conn,stanza): + """ Handle stream closure due to all data transmitted. + Raise xmpppy event specifying successfull data receive. """ + sid=stanza.getTagAttr('close','sid') + self.DEBUG('StreamCloseHandler called sid->%s'%sid,'info') + if sid in self._streams.keys(): + conn.send(stanza.buildReply('result')) + conn.Event(self.DBG_LINE,'SUCCESSFULL RECEIVE',self._streams[sid]) + del self._streams[sid] + else: conn.send(Error(stanza,ERR_ITEM_NOT_FOUND)) + + def StreamBrokenHandler(self,conn,stanza): + """ Handle stream closure due to all some error while receiving data. + Raise xmpppy event specifying unsuccessfull data receive. """ + syn_id=stanza.getID() + self.DEBUG('StreamBrokenHandler called syn_id->%s'%syn_id,'info') + for sid in self._streams.keys(): + stream=self._streams[sid] + if stream['syn_id']==syn_id: + if stream['direction'][0]=='<': conn.Event(self.DBG_LINE,'ERROR ON RECEIVE',stream) + else: conn.Event(self.DBG_LINE,'ERROR ON SEND',stream) + del self._streams[sid] + + def StreamOpenReplyHandler(self,conn,stanza): + """ Handle remote side reply about is it agree or not to receive our datastream. + Used internally. Raises xmpppy event specfiying if the data transfer + is agreed upon.""" + syn_id=stanza.getID() + self.DEBUG('StreamOpenReplyHandler called syn_id->%s'%syn_id,'info') + for sid in self._streams.keys(): + stream=self._streams[sid] + if stream['syn_id']==syn_id: + if stanza.getType()=='error': + if stream['direction'][0]=='<': conn.Event(self.DBG_LINE,'ERROR ON RECEIVE',stream) + else: conn.Event(self.DBG_LINE,'ERROR ON SEND',stream) + del self._streams[sid] + elif stanza.getType()=='result': + if stream['direction'][0]=='|': + stream['direction']=stream['direction'][1:] + conn.Event(self.DBG_LINE,'STREAM COMMITTED',stream) + else: conn.send(Error(stanza,ERR_UNEXPECTED_REQUEST)) diff --git a/src/xmpppy-0.5.0rc1/xmpp/jep0106.py b/src/xmpppy-0.5.0rc1/xmpp/jep0106.py new file mode 100644 index 00000000..fcf11145 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/xmpp/jep0106.py @@ -0,0 +1,57 @@ + +# JID Escaping XEP-0106 for the xmpppy based transports written by Norman Rasmussen + +"""This file is the XEP-0106 commands. + +Implemented commands as follows: + +4.2 Encode : Encoding Transformation +4.3 Decode : Decoding Transformation + + +""" + +xep0106mapping = [ + [' ' ,'20'], + ['"' ,'22'], + ['&' ,'26'], + ['\'','27'], + ['/' ,'2f'], + [':' ,'3a'], + ['<' ,'3c'], + ['>' ,'3e'], + ['@' ,'40']] + +def JIDEncode(str): + str = str.replace('\\5c', '\\5c5c') + for each in xep0106mapping: + str = str.replace('\\' + each[1], '\\5c' + each[1]) + for each in xep0106mapping: + str = str.replace(each[0], '\\' + each[1]) + return str + +def JIDDecode(str): + for each in xep0106mapping: + str = str.replace('\\' + each[1], each[0]) + return str.replace('\\5c', '\\') + +if __name__ == "__main__": + def test(before,valid): + during = JIDEncode(before) + after = JIDDecode(during) + if during == valid and after == before: + print 'PASS Before: ' + before + print 'PASS During: ' + during + else: + print 'FAIL Before: ' + before + print 'FAIL During: ' + during + print 'FAIL After : ' + after + print + + test('jid escaping',r'jid\20escaping') + test(r'\3and\2is\5@example.com',r'\5c3and\2is\5\40example.com') + test(r'\3catsand\2catsis\5cats@example.com',r'\5c3catsand\2catsis\5c5cats\40example.com') + test(r'\2plus\2is\4',r'\2plus\2is\4') + test(r'foo\bar',r'foo\bar') + test(r'foob\41r',r'foob\41r') + test('here\'s_a wild_&_/cr%zy/_address@example.com',r'here\27s_a\20wild_\26_\2fcr%zy\2f_address\40example.com') diff --git a/src/xmpppy-0.5.0rc1/xmpp/protocol.py b/src/xmpppy-0.5.0rc1/xmpp/protocol.py new file mode 100644 index 00000000..3e49b8d2 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/xmpp/protocol.py @@ -0,0 +1,860 @@ +## protocol.py +## +## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: protocol.py,v 1.60 2009/04/07 11:14:28 snakeru Exp $ + +""" +Protocol module contains tools that is needed for processing of +xmpp-related data structures. +""" + +from simplexml import Node,ustr +import time +NS_ACTIVITY ='http://jabber.org/protocol/activity' # XEP-0108 +NS_ADDRESS ='http://jabber.org/protocol/address' # XEP-0033 +NS_ADMIN ='http://jabber.org/protocol/admin' # XEP-0133 +NS_ADMIN_ADD_USER =NS_ADMIN+'#add-user' # XEP-0133 +NS_ADMIN_DELETE_USER =NS_ADMIN+'#delete-user' # XEP-0133 +NS_ADMIN_DISABLE_USER =NS_ADMIN+'#disable-user' # XEP-0133 +NS_ADMIN_REENABLE_USER =NS_ADMIN+'#reenable-user' # XEP-0133 +NS_ADMIN_END_USER_SESSION =NS_ADMIN+'#end-user-session' # XEP-0133 +NS_ADMIN_GET_USER_PASSWORD =NS_ADMIN+'#get-user-password' # XEP-0133 +NS_ADMIN_CHANGE_USER_PASSWORD =NS_ADMIN+'#change-user-password' # XEP-0133 +NS_ADMIN_GET_USER_ROSTER =NS_ADMIN+'#get-user-roster' # XEP-0133 +NS_ADMIN_GET_USER_LASTLOGIN =NS_ADMIN+'#get-user-lastlogin' # XEP-0133 +NS_ADMIN_USER_STATS =NS_ADMIN+'#user-stats' # XEP-0133 +NS_ADMIN_EDIT_BLACKLIST =NS_ADMIN+'#edit-blacklist' # XEP-0133 +NS_ADMIN_EDIT_WHITELIST =NS_ADMIN+'#edit-whitelist' # XEP-0133 +NS_ADMIN_REGISTERED_USERS_NUM =NS_ADMIN+'#get-registered-users-num' # XEP-0133 +NS_ADMIN_DISABLED_USERS_NUM =NS_ADMIN+'#get-disabled-users-num' # XEP-0133 +NS_ADMIN_ONLINE_USERS_NUM =NS_ADMIN+'#get-online-users-num' # XEP-0133 +NS_ADMIN_ACTIVE_USERS_NUM =NS_ADMIN+'#get-active-users-num' # XEP-0133 +NS_ADMIN_IDLE_USERS_NUM =NS_ADMIN+'#get-idle-users-num' # XEP-0133 +NS_ADMIN_REGISTERED_USERS_LIST =NS_ADMIN+'#get-registered-users-list' # XEP-0133 +NS_ADMIN_DISABLED_USERS_LIST =NS_ADMIN+'#get-disabled-users-list' # XEP-0133 +NS_ADMIN_ONLINE_USERS_LIST =NS_ADMIN+'#get-online-users-list' # XEP-0133 +NS_ADMIN_ACTIVE_USERS_LIST =NS_ADMIN+'#get-active-users-list' # XEP-0133 +NS_ADMIN_IDLE_USERS_LIST =NS_ADMIN+'#get-idle-users-list' # XEP-0133 +NS_ADMIN_ANNOUNCE =NS_ADMIN+'#announce' # XEP-0133 +NS_ADMIN_SET_MOTD =NS_ADMIN+'#set-motd' # XEP-0133 +NS_ADMIN_EDIT_MOTD =NS_ADMIN+'#edit-motd' # XEP-0133 +NS_ADMIN_DELETE_MOTD =NS_ADMIN+'#delete-motd' # XEP-0133 +NS_ADMIN_SET_WELCOME =NS_ADMIN+'#set-welcome' # XEP-0133 +NS_ADMIN_DELETE_WELCOME =NS_ADMIN+'#delete-welcome' # XEP-0133 +NS_ADMIN_EDIT_ADMIN =NS_ADMIN+'#edit-admin' # XEP-0133 +NS_ADMIN_RESTART =NS_ADMIN+'#restart' # XEP-0133 +NS_ADMIN_SHUTDOWN =NS_ADMIN+'#shutdown' # XEP-0133 +NS_AGENTS ='jabber:iq:agents' # XEP-0094 (historical) +NS_AMP ='http://jabber.org/protocol/amp' # XEP-0079 +NS_AMP_ERRORS =NS_AMP+'#errors' # XEP-0079 +NS_AUTH ='jabber:iq:auth' # XEP-0078 +NS_AVATAR ='jabber:iq:avatar' # XEP-0008 (historical) +NS_BIND ='urn:ietf:params:xml:ns:xmpp-bind' # RFC 3920 +NS_BROWSE ='jabber:iq:browse' # XEP-0011 (historical) +NS_BYTESTREAM ='http://jabber.org/protocol/bytestreams' # XEP-0065 +NS_CAPS ='http://jabber.org/protocol/caps' # XEP-0115 +NS_CHATSTATES ='http://jabber.org/protocol/chatstates' # XEP-0085 +NS_CLIENT ='jabber:client' # RFC 3921 +NS_COMMANDS ='http://jabber.org/protocol/commands' # XEP-0050 +NS_COMPONENT_ACCEPT ='jabber:component:accept' # XEP-0114 +NS_COMPONENT_1 ='http://jabberd.jabberstudio.org/ns/component/1.0' # Jabberd2 +NS_COMPRESS ='http://jabber.org/protocol/compress' # XEP-0138 +NS_DATA ='jabber:x:data' # XEP-0004 +NS_DATA_LAYOUT ='http://jabber.org/protocol/xdata-layout' # XEP-0141 +NS_DATA_VALIDATE ='http://jabber.org/protocol/xdata-validate' # XEP-0122 +NS_DELAY ='jabber:x:delay' # XEP-0091 (deprecated) +NS_DIALBACK ='jabber:server:dialback' # RFC 3921 +NS_DISCO ='http://jabber.org/protocol/disco' # XEP-0030 +NS_DISCO_INFO =NS_DISCO+'#info' # XEP-0030 +NS_DISCO_ITEMS =NS_DISCO+'#items' # XEP-0030 +NS_ENCRYPTED ='jabber:x:encrypted' # XEP-0027 +NS_EVENT ='jabber:x:event' # XEP-0022 (deprecated) +NS_FEATURE ='http://jabber.org/protocol/feature-neg' # XEP-0020 +NS_FILE ='http://jabber.org/protocol/si/profile/file-transfer' # XEP-0096 +NS_GATEWAY ='jabber:iq:gateway' # XEP-0100 +NS_GEOLOC ='http://jabber.org/protocol/geoloc' # XEP-0080 +NS_GROUPCHAT ='gc-1.0' # XEP-0045 +NS_HTTP_BIND ='http://jabber.org/protocol/httpbind' # XEP-0124 +NS_IBB ='http://jabber.org/protocol/ibb' # XEP-0047 +NS_INVISIBLE ='presence-invisible' # Jabberd2 +NS_IQ ='iq' # Jabberd2 +NS_LAST ='jabber:iq:last' # XEP-0012 +NS_MESSAGE ='message' # Jabberd2 +NS_MOOD ='http://jabber.org/protocol/mood' # XEP-0107 +NS_MUC ='http://jabber.org/protocol/muc' # XEP-0045 +NS_MUC_ADMIN =NS_MUC+'#admin' # XEP-0045 +NS_MUC_OWNER =NS_MUC+'#owner' # XEP-0045 +NS_MUC_UNIQUE =NS_MUC+'#unique' # XEP-0045 +NS_MUC_USER =NS_MUC+'#user' # XEP-0045 +NS_MUC_REGISTER =NS_MUC+'#register' # XEP-0045 +NS_MUC_REQUEST =NS_MUC+'#request' # XEP-0045 +NS_MUC_ROOMCONFIG =NS_MUC+'#roomconfig' # XEP-0045 +NS_MUC_ROOMINFO =NS_MUC+'#roominfo' # XEP-0045 +NS_MUC_ROOMS =NS_MUC+'#rooms' # XEP-0045 +NS_MUC_TRAFIC =NS_MUC+'#traffic' # XEP-0045 +NS_NICK ='http://jabber.org/protocol/nick' # XEP-0172 +NS_OFFLINE ='http://jabber.org/protocol/offline' # XEP-0013 +NS_PHYSLOC ='http://jabber.org/protocol/physloc' # XEP-0112 +NS_PRESENCE ='presence' # Jabberd2 +NS_PRIVACY ='jabber:iq:privacy' # RFC 3921 +NS_PRIVATE ='jabber:iq:private' # XEP-0049 +NS_PUBSUB ='http://jabber.org/protocol/pubsub' # XEP-0060 +NS_REGISTER ='jabber:iq:register' # XEP-0077 +NS_RC ='http://jabber.org/protocol/rc' # XEP-0146 +NS_ROSTER ='jabber:iq:roster' # RFC 3921 +NS_ROSTERX ='http://jabber.org/protocol/rosterx' # XEP-0144 +NS_RPC ='jabber:iq:rpc' # XEP-0009 +NS_SASL ='urn:ietf:params:xml:ns:xmpp-sasl' # RFC 3920 +NS_SEARCH ='jabber:iq:search' # XEP-0055 +NS_SERVER ='jabber:server' # RFC 3921 +NS_SESSION ='urn:ietf:params:xml:ns:xmpp-session' # RFC 3921 +NS_SI ='http://jabber.org/protocol/si' # XEP-0096 +NS_SI_PUB ='http://jabber.org/protocol/sipub' # XEP-0137 +NS_SIGNED ='jabber:x:signed' # XEP-0027 +NS_STANZAS ='urn:ietf:params:xml:ns:xmpp-stanzas' # RFC 3920 +NS_STREAMS ='http://etherx.jabber.org/streams' # RFC 3920 +NS_TIME ='jabber:iq:time' # XEP-0090 (deprecated) +NS_TLS ='urn:ietf:params:xml:ns:xmpp-tls' # RFC 3920 +NS_VACATION ='http://jabber.org/protocol/vacation' # XEP-0109 +NS_VCARD ='vcard-temp' # XEP-0054 +NS_VCARD_UPDATE ='vcard-temp:x:update' # XEP-0153 +NS_VERSION ='jabber:iq:version' # XEP-0092 +NS_WAITINGLIST ='http://jabber.org/protocol/waitinglist' # XEP-0130 +NS_XHTML_IM ='http://jabber.org/protocol/xhtml-im' # XEP-0071 +NS_XMPP_STREAMS ='urn:ietf:params:xml:ns:xmpp-streams' # RFC 3920 + +xmpp_stream_error_conditions=""" +bad-format -- -- -- The entity has sent XML that cannot be processed. +bad-namespace-prefix -- -- -- The entity has sent a namespace prefix that is unsupported, or has sent no namespace prefix on an element that requires such a prefix. +conflict -- -- -- The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the existing stream. +connection-timeout -- -- -- The entity has not generated any traffic over the stream for some period of time. +host-gone -- -- -- The value of the 'to' attribute provided by the initiating entity in the stream header corresponds to a hostname that is no longer hosted by the server. +host-unknown -- -- -- The value of the 'to' attribute provided by the initiating entity in the stream header does not correspond to a hostname that is hosted by the server. +improper-addressing -- -- -- A stanza sent between two servers lacks a 'to' or 'from' attribute (or the attribute has no value). +internal-server-error -- -- -- The server has experienced a misconfiguration or an otherwise-undefined internal error that prevents it from servicing the stream. +invalid-from -- cancel -- -- The JID or hostname provided in a 'from' address does not match an authorized JID or validated domain negotiated between servers via SASL or dialback, or between a client and a server via authentication and resource authorization. +invalid-id -- -- -- The stream ID or dialback ID is invalid or does not match an ID previously provided. +invalid-namespace -- -- -- The streams namespace name is something other than "http://etherx.jabber.org/streams" or the dialback namespace name is something other than "jabber:server:dialback". +invalid-xml -- -- -- The entity has sent invalid XML over the stream to a server that performs validation. +not-authorized -- -- -- The entity has attempted to send data before the stream has been authenticated, or otherwise is not authorized to perform an action related to stream negotiation. +policy-violation -- -- -- The entity has violated some local service policy. +remote-connection-failed -- -- -- The server is unable to properly connect to a remote resource that is required for authentication or authorization. +resource-constraint -- -- -- The server lacks the system resources necessary to service the stream. +restricted-xml -- -- -- The entity has attempted to send restricted XML features such as a comment, processing instruction, DTD, entity reference, or unescaped character. +see-other-host -- -- -- The server will not provide service to the initiating entity but is redirecting traffic to another host. +system-shutdown -- -- -- The server is being shut down and all active streams are being closed. +undefined-condition -- -- -- The error condition is not one of those defined by the other conditions in this list. +unsupported-encoding -- -- -- The initiating entity has encoded the stream in an encoding that is not supported by the server. +unsupported-stanza-type -- -- -- The initiating entity has sent a first-level child of the stream that is not supported by the server. +unsupported-version -- -- -- The value of the 'version' attribute provided by the initiating entity in the stream header specifies a version of XMPP that is not supported by the server. +xml-not-well-formed -- -- -- The initiating entity has sent XML that is not well-formed.""" +xmpp_stanza_error_conditions=""" +bad-request -- 400 -- modify -- The sender has sent XML that is malformed or that cannot be processed. +conflict -- 409 -- cancel -- Access cannot be granted because an existing resource or session exists with the same name or address. +feature-not-implemented -- 501 -- cancel -- The feature requested is not implemented by the recipient or server and therefore cannot be processed. +forbidden -- 403 -- auth -- The requesting entity does not possess the required permissions to perform the action. +gone -- 302 -- modify -- The recipient or server can no longer be contacted at this address. +internal-server-error -- 500 -- wait -- The server could not process the stanza because of a misconfiguration or an otherwise-undefined internal server error. +item-not-found -- 404 -- cancel -- The addressed JID or item requested cannot be found. +jid-malformed -- 400 -- modify -- The value of the 'to' attribute in the sender's stanza does not adhere to the syntax defined in Addressing Scheme. +not-acceptable -- 406 -- cancel -- The recipient or server understands the request but is refusing to process it because it does not meet criteria defined by the recipient or server. +not-allowed -- 405 -- cancel -- The recipient or server does not allow any entity to perform the action. +not-authorized -- 401 -- auth -- The sender must provide proper credentials before being allowed to perform the action, or has provided improper credentials. +payment-required -- 402 -- auth -- The requesting entity is not authorized to access the requested service because payment is required. +recipient-unavailable -- 404 -- wait -- The intended recipient is temporarily unavailable. +redirect -- 302 -- modify -- The recipient or server is redirecting requests for this information to another entity. +registration-required -- 407 -- auth -- The requesting entity is not authorized to access the requested service because registration is required. +remote-server-not-found -- 404 -- cancel -- A remote server or service specified as part or all of the JID of the intended recipient does not exist. +remote-server-timeout -- 504 -- wait -- A remote server or service specified as part or all of the JID of the intended recipient could not be contacted within a reasonable amount of time. +resource-constraint -- 500 -- wait -- The server or recipient lacks the system resources necessary to service the request. +service-unavailable -- 503 -- cancel -- The server or recipient does not currently provide the requested service. +subscription-required -- 407 -- auth -- The requesting entity is not authorized to access the requested service because a subscription is required. +undefined-condition -- 500 -- -- +unexpected-request -- 400 -- wait -- The recipient or server understood the request but was not expecting it at this time (e.g., the request was out of order).""" +sasl_error_conditions=""" +aborted -- -- -- The receiving entity acknowledges an <abort/> element sent by the initiating entity; sent in reply to the <abort/> element. +incorrect-encoding -- -- -- The data provided by the initiating entity could not be processed because the [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003. encoding is incorrect (e.g., because the encoding does not adhere to the definition in Section 3 of [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003.); sent in reply to a <response/> element or an <auth/> element with initial response data. +invalid-authzid -- -- -- The authzid provided by the initiating entity is invalid, either because it is incorrectly formatted or because the initiating entity does not have permissions to authorize that ID; sent in reply to a <response/> element or an <auth/> element with initial response data. +invalid-mechanism -- -- -- The initiating entity did not provide a mechanism or requested a mechanism that is not supported by the receiving entity; sent in reply to an <auth/> element. +mechanism-too-weak -- -- -- The mechanism requested by the initiating entity is weaker than server policy permits for that initiating entity; sent in reply to a <response/> element or an <auth/> element with initial response data. +not-authorized -- -- -- The authentication failed because the initiating entity did not provide valid credentials (this includes but is not limited to the case of an unknown username); sent in reply to a <response/> element or an <auth/> element with initial response data. +temporary-auth-failure -- -- -- The authentication failed because of a temporary error condition within the receiving entity; sent in reply to an <auth/> element or <response/> element.""" + +ERRORS,_errorcodes={},{} +for ns,errname,errpool in [(NS_XMPP_STREAMS,'STREAM',xmpp_stream_error_conditions), + (NS_STANZAS ,'ERR' ,xmpp_stanza_error_conditions), + (NS_SASL ,'SASL' ,sasl_error_conditions)]: + for err in errpool.split('\n')[1:]: + cond,code,typ,text=err.split(' -- ') + name=errname+'_'+cond.upper().replace('-','_') + locals()[name]=ns+' '+cond + ERRORS[ns+' '+cond]=[code,typ,text] + if code: _errorcodes[code]=cond +del ns,errname,errpool,err,cond,code,typ,text + +def isResultNode(node): + """ Returns true if the node is a positive reply. """ + return node and node.getType()=='result' +def isErrorNode(node): + """ Returns true if the node is a negative reply. """ + return node and node.getType()=='error' + +class NodeProcessed(Exception): + """ Exception that should be raised by handler when the handling should be stopped. """ +class StreamError(Exception): + """ Base exception class for stream errors.""" +class BadFormat(StreamError): pass +class BadNamespacePrefix(StreamError): pass +class Conflict(StreamError): pass +class ConnectionTimeout(StreamError): pass +class HostGone(StreamError): pass +class HostUnknown(StreamError): pass +class ImproperAddressing(StreamError): pass +class InternalServerError(StreamError): pass +class InvalidFrom(StreamError): pass +class InvalidID(StreamError): pass +class InvalidNamespace(StreamError): pass +class InvalidXML(StreamError): pass +class NotAuthorized(StreamError): pass +class PolicyViolation(StreamError): pass +class RemoteConnectionFailed(StreamError): pass +class ResourceConstraint(StreamError): pass +class RestrictedXML(StreamError): pass +class SeeOtherHost(StreamError): pass +class SystemShutdown(StreamError): pass +class UndefinedCondition(StreamError): pass +class UnsupportedEncoding(StreamError): pass +class UnsupportedStanzaType(StreamError): pass +class UnsupportedVersion(StreamError): pass +class XMLNotWellFormed(StreamError): pass + +stream_exceptions = {'bad-format': BadFormat, + 'bad-namespace-prefix': BadNamespacePrefix, + 'conflict': Conflict, + 'connection-timeout': ConnectionTimeout, + 'host-gone': HostGone, + 'host-unknown': HostUnknown, + 'improper-addressing': ImproperAddressing, + 'internal-server-error': InternalServerError, + 'invalid-from': InvalidFrom, + 'invalid-id': InvalidID, + 'invalid-namespace': InvalidNamespace, + 'invalid-xml': InvalidXML, + 'not-authorized': NotAuthorized, + 'policy-violation': PolicyViolation, + 'remote-connection-failed': RemoteConnectionFailed, + 'resource-constraint': ResourceConstraint, + 'restricted-xml': RestrictedXML, + 'see-other-host': SeeOtherHost, + 'system-shutdown': SystemShutdown, + 'undefined-condition': UndefinedCondition, + 'unsupported-encoding': UnsupportedEncoding, + 'unsupported-stanza-type': UnsupportedStanzaType, + 'unsupported-version': UnsupportedVersion, + 'xml-not-well-formed': XMLNotWellFormed} + +class JID: + """ JID object. JID can be built from string, modified, compared, serialised into string. """ + def __init__(self, jid=None, node='', domain='', resource=''): + """ Constructor. JID can be specified as string (jid argument) or as separate parts. + Examples: + JID('node@domain/resource') + JID(node='node',domain='domain.org') + """ + if not jid and not domain: raise ValueError('JID must contain at least domain name') + elif type(jid)==type(self): self.node,self.domain,self.resource=jid.node,jid.domain,jid.resource + elif domain: self.node,self.domain,self.resource=node,domain,resource + else: + if jid.find('@')+1: self.node,jid=jid.split('@',1) + else: self.node='' + if jid.find('/')+1: self.domain,self.resource=jid.split('/',1) + else: self.domain,self.resource=jid,'' + def getNode(self): + """ Return the node part of the JID """ + return self.node + def setNode(self,node): + """ Set the node part of the JID to new value. Specify None to remove the node part.""" + self.node=node.lower() + def getDomain(self): + """ Return the domain part of the JID """ + return self.domain + def setDomain(self,domain): + """ Set the domain part of the JID to new value.""" + self.domain=domain.lower() + def getResource(self): + """ Return the resource part of the JID """ + return self.resource + def setResource(self,resource): + """ Set the resource part of the JID to new value. Specify None to remove the resource part.""" + self.resource=resource + def getStripped(self): + """ Return the bare representation of JID. I.e. string value w/o resource. """ + return self.__str__(0) + def __eq__(self, other): + """ Compare the JID to another instance or to string for equality. """ + try: other=JID(other) + except ValueError: return 0 + return self.resource==other.resource and self.__str__(0) == other.__str__(0) + def __ne__(self, other): + """ Compare the JID to another instance or to string for non-equality. """ + return not self.__eq__(other) + def bareMatch(self, other): + """ Compare the node and domain parts of the JID's for equality. """ + return self.__str__(0) == JID(other).__str__(0) + def __str__(self,wresource=1): + """ Serialise JID into string. """ + if self.node: jid=self.node+'@'+self.domain + else: jid=self.domain + if wresource and self.resource: return jid+'/'+self.resource + return jid + def __hash__(self): + """ Produce hash of the JID, Allows to use JID objects as keys of the dictionary. """ + return hash(self.__str__()) + +class Protocol(Node): + """ A "stanza" object class. Contains methods that are common for presences, iqs and messages. """ + def __init__(self, name=None, to=None, typ=None, frm=None, attrs={}, payload=[], timestamp=None, xmlns=None, node=None): + """ Constructor, name is the name of the stanza i.e. 'message' or 'presence' or 'iq'. + to is the value of 'to' attribure, 'typ' - 'type' attribute + frn - from attribure, attrs - other attributes mapping, payload - same meaning as for simplexml payload definition + timestamp - the time value that needs to be stamped over stanza + xmlns - namespace of top stanza node + node - parsed or unparsed stana to be taken as prototype. + """ + if not attrs: attrs={} + if to: attrs['to']=to + if frm: attrs['from']=frm + if typ: attrs['type']=typ + Node.__init__(self, tag=name, attrs=attrs, payload=payload, node=node) + if not node and xmlns: self.setNamespace(xmlns) + if self['to']: self.setTo(self['to']) + if self['from']: self.setFrom(self['from']) + if node and type(self)==type(node) and self.__class__==node.__class__ and self.attrs.has_key('id'): del self.attrs['id'] + self.timestamp=None + for x in self.getTags('x',namespace=NS_DELAY): + try: + if not self.getTimestamp() or x.getAttr('stamp')<self.getTimestamp(): self.setTimestamp(x.getAttr('stamp')) + except: pass + if timestamp is not None: self.setTimestamp(timestamp) # To auto-timestamp stanza just pass timestamp='' + def getTo(self): + """ Return value of the 'to' attribute. """ + try: return self['to'] + except: return None + def getFrom(self): + """ Return value of the 'from' attribute. """ + try: return self['from'] + except: return None + def getTimestamp(self): + """ Return the timestamp in the 'yyyymmddThhmmss' format. """ + return self.timestamp + def getID(self): + """ Return the value of the 'id' attribute. """ + return self.getAttr('id') + def setTo(self,val): + """ Set the value of the 'to' attribute. """ + self.setAttr('to', JID(val)) + def getType(self): + """ Return the value of the 'type' attribute. """ + return self.getAttr('type') + def setFrom(self,val): + """ Set the value of the 'from' attribute. """ + self.setAttr('from', JID(val)) + def setType(self,val): + """ Set the value of the 'type' attribute. """ + self.setAttr('type', val) + def setID(self,val): + """ Set the value of the 'id' attribute. """ + self.setAttr('id', val) + def getError(self): + """ Return the error-condition (if present) or the textual description of the error (otherwise). """ + errtag=self.getTag('error') + if errtag: + for tag in errtag.getChildren(): + if tag.getName()<>'text': return tag.getName() + return errtag.getData() + def getErrorCode(self): + """ Return the error code. Obsolette. """ + return self.getTagAttr('error','code') + def setError(self,error,code=None): + """ Set the error code. Obsolette. Use error-conditions instead. """ + if code: + if str(code) in _errorcodes.keys(): error=ErrorNode(_errorcodes[str(code)],text=error) + else: error=ErrorNode(ERR_UNDEFINED_CONDITION,code=code,typ='cancel',text=error) + elif type(error) in [type(''),type(u'')]: error=ErrorNode(error) + self.setType('error') + self.addChild(node=error) + def setTimestamp(self,val=None): + """Set the timestamp. timestamp should be the yyyymmddThhmmss string.""" + if not val: val=time.strftime('%Y%m%dT%H:%M:%S', time.gmtime()) + self.timestamp=val + self.setTag('x',{'stamp':self.timestamp},namespace=NS_DELAY) + def getProperties(self): + """ Return the list of namespaces to which belongs the direct childs of element""" + props=[] + for child in self.getChildren(): + prop=child.getNamespace() + if prop not in props: props.append(prop) + return props + def __setitem__(self,item,val): + """ Set the item 'item' to the value 'val'.""" + if item in ['to','from']: val=JID(val) + return self.setAttr(item,val) + +class Message(Protocol): + """ XMPP Message stanza - "push" mechanism.""" + def __init__(self, to=None, body=None, typ=None, subject=None, attrs={}, frm=None, payload=[], timestamp=None, xmlns=NS_CLIENT, node=None): + """ Create message object. You can specify recipient, text of message, type of message + any additional attributes, sender of the message, any additional payload (f.e. jabber:x:delay element) and namespace in one go. + Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as message. """ + Protocol.__init__(self, 'message', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node) + if body: self.setBody(body) + if subject: self.setSubject(subject) + def getBody(self): + """ Returns text of the message. """ + return self.getTagData('body') + def getSubject(self): + """ Returns subject of the message. """ + return self.getTagData('subject') + def getThread(self): + """ Returns thread of the message. """ + return self.getTagData('thread') + def setBody(self,val): + """ Sets the text of the message. """ + self.setTagData('body',val) + def setSubject(self,val): + """ Sets the subject of the message. """ + self.setTagData('subject',val) + def setThread(self,val): + """ Sets the thread of the message. """ + self.setTagData('thread',val) + def buildReply(self,text=None): + """ Builds and returns another message object with specified text. + The to, from and thread properties of new message are pre-set as reply to this message. """ + m=Message(to=self.getFrom(),frm=self.getTo(),body=text) + th=self.getThread() + if th: m.setThread(th) + return m + +class Presence(Protocol): + """ XMPP Presence object.""" + def __init__(self, to=None, typ=None, priority=None, show=None, status=None, attrs={}, frm=None, timestamp=None, payload=[], xmlns=NS_CLIENT, node=None): + """ Create presence object. You can specify recipient, type of message, priority, show and status values + any additional attributes, sender of the presence, timestamp, any additional payload (f.e. jabber:x:delay element) and namespace in one go. + Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as presence. """ + Protocol.__init__(self, 'presence', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node) + if priority: self.setPriority(priority) + if show: self.setShow(show) + if status: self.setStatus(status) + def getPriority(self): + """ Returns the priority of the message. """ + return self.getTagData('priority') + def getShow(self): + """ Returns the show value of the message. """ + return self.getTagData('show') + def getStatus(self): + """ Returns the status string of the message. """ + return self.getTagData('status') + def setPriority(self,val): + """ Sets the priority of the message. """ + self.setTagData('priority',val) + def setShow(self,val): + """ Sets the show value of the message. """ + self.setTagData('show',val) + def setStatus(self,val): + """ Sets the status string of the message. """ + self.setTagData('status',val) + + def _muc_getItemAttr(self,tag,attr): + for xtag in self.getTags('x'): + for child in xtag.getTags(tag): + return child.getAttr(attr) + def _muc_getSubTagDataAttr(self,tag,attr): + for xtag in self.getTags('x'): + for child in xtag.getTags('item'): + for cchild in child.getTags(tag): + return cchild.getData(),cchild.getAttr(attr) + return None,None + def getRole(self): + """Returns the presence role (for groupchat)""" + return self._muc_getItemAttr('item','role') + def getAffiliation(self): + """Returns the presence affiliation (for groupchat)""" + return self._muc_getItemAttr('item','affiliation') + def getNick(self): + """Returns the nick value (for nick change in groupchat)""" + return self._muc_getItemAttr('item','nick') + def getJid(self): + """Returns the presence jid (for groupchat)""" + return self._muc_getItemAttr('item','jid') + def getReason(self): + """Returns the reason of the presence (for groupchat)""" + return self._muc_getSubTagDataAttr('reason','')[0] + def getActor(self): + """Returns the reason of the presence (for groupchat)""" + return self._muc_getSubTagDataAttr('actor','jid')[1] + def getStatusCode(self): + """Returns the status code of the presence (for groupchat)""" + return self._muc_getItemAttr('status','code') + +class Iq(Protocol): + """ XMPP Iq object - get/set dialog mechanism. """ + def __init__(self, typ=None, queryNS=None, attrs={}, to=None, frm=None, payload=[], xmlns=NS_CLIENT, node=None): + """ Create Iq object. You can specify type, query namespace + any additional attributes, recipient of the iq, sender of the iq, any additional payload (f.e. jabber:x:data node) and namespace in one go. + Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as an iq. """ + Protocol.__init__(self, 'iq', to=to, typ=typ, attrs=attrs, frm=frm, xmlns=xmlns, node=node) + if payload: self.setQueryPayload(payload) + if queryNS: self.setQueryNS(queryNS) + def getQueryNS(self): + """ Return the namespace of the 'query' child element.""" + tag=self.getTag('query') + if tag: return tag.getNamespace() + def getQuerynode(self): + """ Return the 'node' attribute value of the 'query' child element.""" + return self.getTagAttr('query','node') + def getQueryPayload(self): + """ Return the 'query' child element payload.""" + tag=self.getTag('query') + if tag: return tag.getPayload() + def getQueryChildren(self): + """ Return the 'query' child element child nodes.""" + tag=self.getTag('query') + if tag: return tag.getChildren() + def setQueryNS(self,namespace): + """ Set the namespace of the 'query' child element.""" + self.setTag('query').setNamespace(namespace) + def setQueryPayload(self,payload): + """ Set the 'query' child element payload.""" + self.setTag('query').setPayload(payload) + def setQuerynode(self,node): + """ Set the 'node' attribute value of the 'query' child element.""" + self.setTagAttr('query','node',node) + def buildReply(self,typ): + """ Builds and returns another Iq object of specified type. + The to, from and query child node of new Iq are pre-set as reply to this Iq. """ + iq=Iq(typ,to=self.getFrom(),frm=self.getTo(),attrs={'id':self.getID()}) + if self.getTag('query'): iq.setQueryNS(self.getQueryNS()) + return iq + +class ErrorNode(Node): + """ XMPP-style error element. + In the case of stanza error should be attached to XMPP stanza. + In the case of stream-level errors should be used separately. """ + def __init__(self,name,code=None,typ=None,text=None): + """ Create new error node object. + Mandatory parameter: name - name of error condition. + Optional parameters: code, typ, text. Used for backwards compartibility with older jabber protocol.""" + if ERRORS.has_key(name): + cod,type,txt=ERRORS[name] + ns=name.split()[0] + else: cod,ns,type,txt='500',NS_STANZAS,'cancel','' + if typ: type=typ + if code: cod=code + if text: txt=text + Node.__init__(self,'error',{},[Node(name)]) + if type: self.setAttr('type',type) + if not cod: self.setName('stream:error') + if txt: self.addChild(node=Node(ns+' text',{},[txt])) + if cod: self.setAttr('code',cod) + +class Error(Protocol): + """ Used to quickly transform received stanza into error reply.""" + def __init__(self,node,error,reply=1): + """ Create error reply basing on the received 'node' stanza and the 'error' error condition. + If the 'node' is not the received stanza but locally created ('to' and 'from' fields needs not swapping) + specify the 'reply' argument as false.""" + if reply: Protocol.__init__(self,to=node.getFrom(),frm=node.getTo(),node=node) + else: Protocol.__init__(self,node=node) + self.setError(error) + if node.getType()=='error': self.__str__=self.__dupstr__ + def __dupstr__(self,dup1=None,dup2=None): + """ Dummy function used as preventor of creating error node in reply to error node. + I.e. you will not be able to serialise "double" error into string. + """ + return '' + +class DataField(Node): + """ This class is used in the DataForm class to describe the single data item. + If you are working with jabber:x:data (XEP-0004, XEP-0068, XEP-0122) + then you will need to work with instances of this class. """ + def __init__(self,name=None,value=None,typ=None,required=0,label=None,desc=None,options=[],node=None): + """ Create new data field of specified name,value and type. + Also 'required','desc' and 'options' fields can be set. + Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new datafiled. + """ + Node.__init__(self,'field',node=node) + if name: self.setVar(name) + if type(value) in [list,tuple]: self.setValues(value) + elif value: self.setValue(value) + if typ: self.setType(typ) + elif not typ and not node: self.setType('text-single') + if required: self.setRequired(required) + if label: self.setLabel(label) + if desc: self.setDesc(desc) + if options: self.setOptions(options) + def setRequired(self,req=1): + """ Change the state of the 'required' flag. """ + if req: self.setTag('required') + else: + try: self.delChild('required') + except ValueError: return + def isRequired(self): + """ Returns in this field a required one. """ + return self.getTag('required') + def setLabel(self,label): + """ Set the label of this field. """ + self.setAttr('label',label) + def getLabel(self): + """ Return the label of this field. """ + return self.getAttr('label') + def setDesc(self,desc): + """ Set the description of this field. """ + self.setTagData('desc',desc) + def getDesc(self): + """ Return the description of this field. """ + return self.getTagData('desc') + def setValue(self,val): + """ Set the value of this field. """ + self.setTagData('value',val) + def getValue(self): + return self.getTagData('value') + def setValues(self,lst): + """ Set the values of this field as values-list. + Replaces all previous filed values! If you need to just add a value - use addValue method.""" + while self.getTag('value'): self.delChild('value') + for val in lst: self.addValue(val) + def addValue(self,val): + """ Add one more value to this field. Used in 'get' iq's or such.""" + self.addChild('value',{},[val]) + def getValues(self): + """ Return the list of values associated with this field.""" + ret=[] + for tag in self.getTags('value'): ret.append(tag.getData()) + return ret + def getOptions(self): + """ Return label-option pairs list associated with this field.""" + ret=[] + for tag in self.getTags('option'): ret.append([tag.getAttr('label'),tag.getTagData('value')]) + return ret + def setOptions(self,lst): + """ Set label-option pairs list associated with this field.""" + while self.getTag('option'): self.delChild('option') + for opt in lst: self.addOption(opt) + def addOption(self,opt): + """ Add one more label-option pair to this field.""" + if type(opt) in [str,unicode]: self.addChild('option').setTagData('value',opt) + else: self.addChild('option',{'label':opt[0]}).setTagData('value',opt[1]) + def getType(self): + """ Get type of this field. """ + return self.getAttr('type') + def setType(self,val): + """ Set type of this field. """ + return self.setAttr('type',val) + def getVar(self): + """ Get 'var' attribute value of this field. """ + return self.getAttr('var') + def setVar(self,val): + """ Set 'var' attribute value of this field. """ + return self.setAttr('var',val) + +class DataReported(Node): + """ This class is used in the DataForm class to describe the 'reported data field' data items which are used in + 'multiple item form results' (as described in XEP-0004). + Represents the fields that will be returned from a search. This information is useful when + you try to use the jabber:iq:search namespace to return dynamic form information. + """ + def __init__(self,node=None): + """ Create new empty 'reported data' field. However, note that, according XEP-0004: + * It MUST contain one or more DataFields. + * Contained DataFields SHOULD possess a 'type' and 'label' attribute in addition to 'var' attribute + * Contained DataFields SHOULD NOT contain a <value/> element. + Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new + dataitem. + """ + Node.__init__(self,'reported',node=node) + if node: + newkids=[] + for n in self.getChildren(): + if n.getName()=='field': newkids.append(DataField(node=n)) + else: newkids.append(n) + self.kids=newkids + def getField(self,name): + """ Return the datafield object with name 'name' (if exists). """ + return self.getTag('field',attrs={'var':name}) + def setField(self,name,typ=None,label=None): + """ Create if nessessary or get the existing datafield object with name 'name' and return it. + If created, attributes 'type' and 'label' are applied to new datafield.""" + f=self.getField(name) + if f: return f + return self.addChild(node=DataField(name,None,typ,0,label)) + def asDict(self): + """ Represent dataitem as simple dictionary mapping of datafield names to their values.""" + ret={} + for field in self.getTags('field'): + name=field.getAttr('var') + typ=field.getType() + if isinstance(typ,(str,unicode)) and typ[-6:]=='-multi': + val=[] + for i in field.getTags('value'): val.append(i.getData()) + else: val=field.getTagData('value') + ret[name]=val + if self.getTag('instructions'): ret['instructions']=self.getInstructions() + return ret + def __getitem__(self,name): + """ Simple dictionary interface for getting datafields values by their names.""" + item=self.getField(name) + if item: return item.getValue() + raise IndexError('No such field') + def __setitem__(self,name,val): + """ Simple dictionary interface for setting datafields values by their names.""" + return self.setField(name).setValue(val) + +class DataItem(Node): + """ This class is used in the DataForm class to describe data items which are used in 'multiple + item form results' (as described in XEP-0004). + """ + def __init__(self,node=None): + """ Create new empty data item. However, note that, according XEP-0004, DataItem MUST contain ALL + DataFields described in DataReported. + Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new + dataitem. + """ + Node.__init__(self,'item',node=node) + if node: + newkids=[] + for n in self.getChildren(): + if n.getName()=='field': newkids.append(DataField(node=n)) + else: newkids.append(n) + self.kids=newkids + def getField(self,name): + """ Return the datafield object with name 'name' (if exists). """ + return self.getTag('field',attrs={'var':name}) + def setField(self,name): + """ Create if nessessary or get the existing datafield object with name 'name' and return it. """ + f=self.getField(name) + if f: return f + return self.addChild(node=DataField(name)) + def asDict(self): + """ Represent dataitem as simple dictionary mapping of datafield names to their values.""" + ret={} + for field in self.getTags('field'): + name=field.getAttr('var') + typ=field.getType() + if isinstance(typ,(str,unicode)) and typ[-6:]=='-multi': + val=[] + for i in field.getTags('value'): val.append(i.getData()) + else: val=field.getTagData('value') + ret[name]=val + if self.getTag('instructions'): ret['instructions']=self.getInstructions() + return ret + def __getitem__(self,name): + """ Simple dictionary interface for getting datafields values by their names.""" + item=self.getField(name) + if item: return item.getValue() + raise IndexError('No such field') + def __setitem__(self,name,val): + """ Simple dictionary interface for setting datafields values by their names.""" + return self.setField(name).setValue(val) + +class DataForm(Node): + """ DataForm class. Used for manipulating dataforms in XMPP. + Relevant XEPs: 0004, 0068, 0122. + Can be used in disco, pub-sub and many other applications.""" + def __init__(self, typ=None, data=[], title=None, node=None): + """ + Create new dataform of type 'typ'; 'data' is the list of DataReported, + DataItem and DataField instances that this dataform contains; 'title' + is the title string. + You can specify the 'node' argument as the other node to be used as + base for constructing this dataform. + + title and instructions is optional and SHOULD NOT contain newlines. + Several instructions MAY be present. + 'typ' can be one of ('form' | 'submit' | 'cancel' | 'result' ) + 'typ' of reply iq can be ( 'result' | 'set' | 'set' | 'result' ) respectively. + 'cancel' form can not contain any fields. All other forms contains AT LEAST one field. + 'title' MAY be included in forms of type "form" and "result" + """ + Node.__init__(self,'x',node=node) + if node: + newkids=[] + for n in self.getChildren(): + if n.getName()=='field': newkids.append(DataField(node=n)) + elif n.getName()=='item': newkids.append(DataItem(node=n)) + elif n.getName()=='reported': newkids.append(DataReported(node=n)) + else: newkids.append(n) + self.kids=newkids + if typ: self.setType(typ) + self.setNamespace(NS_DATA) + if title: self.setTitle(title) + if type(data)==type({}): + newdata=[] + for name in data.keys(): newdata.append(DataField(name,data[name])) + data=newdata + for child in data: + if type(child) in [type(''),type(u'')]: self.addInstructions(child) + elif child.__class__.__name__=='DataField': self.kids.append(child) + elif child.__class__.__name__=='DataItem': self.kids.append(child) + elif child.__class__.__name__=='DataReported': self.kids.append(child) + else: self.kids.append(DataField(node=child)) + def getType(self): + """ Return the type of dataform. """ + return self.getAttr('type') + def setType(self,typ): + """ Set the type of dataform. """ + self.setAttr('type',typ) + def getTitle(self): + """ Return the title of dataform. """ + return self.getTagData('title') + def setTitle(self,text): + """ Set the title of dataform. """ + self.setTagData('title',text) + def getInstructions(self): + """ Return the instructions of dataform. """ + return self.getTagData('instructions') + def setInstructions(self,text): + """ Set the instructions of dataform. """ + self.setTagData('instructions',text) + def addInstructions(self,text): + """ Add one more instruction to the dataform. """ + self.addChild('instructions',{},[text]) + def getField(self,name): + """ Return the datafield object with name 'name' (if exists). """ + return self.getTag('field',attrs={'var':name}) + def setField(self,name): + """ Create if nessessary or get the existing datafield object with name 'name' and return it. """ + f=self.getField(name) + if f: return f + return self.addChild(node=DataField(name)) + def asDict(self): + """ Represent dataform as simple dictionary mapping of datafield names to their values.""" + ret={} + for field in self.getTags('field'): + name=field.getAttr('var') + typ=field.getType() + if isinstance(typ,(str,unicode)) and typ[-6:]=='-multi': + val=[] + for i in field.getTags('value'): val.append(i.getData()) + else: val=field.getTagData('value') + ret[name]=val + if self.getTag('instructions'): ret['instructions']=self.getInstructions() + return ret + def __getitem__(self,name): + """ Simple dictionary interface for getting datafields values by their names.""" + item=self.getField(name) + if item: return item.getValue() + raise IndexError('No such field') + def __setitem__(self,name,val): + """ Simple dictionary interface for setting datafields values by their names.""" + return self.setField(name).setValue(val) diff --git a/src/xmpppy-0.5.0rc1/xmpp/roster.py b/src/xmpppy-0.5.0rc1/xmpp/roster.py new file mode 100644 index 00000000..676a4c9a --- /dev/null +++ b/src/xmpppy-0.5.0rc1/xmpp/roster.py @@ -0,0 +1,184 @@ +## roster.py +## +## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: roster.py,v 1.20 2005/07/13 13:22:52 snakeru Exp $ + +""" +Simple roster implementation. Can be used though for different tasks like +mass-renaming of contacts. +""" + +from protocol import * +from client import PlugIn + +class Roster(PlugIn): + """ Defines a plenty of methods that will allow you to manage roster. + Also automatically track presences from remote JIDs taking into + account that every JID can have multiple resources connected. Does not + currently support 'error' presences. + You can also use mapping interface for access to the internal representation of + contacts in roster. + """ + def __init__(self): + """ Init internal variables. """ + PlugIn.__init__(self) + self.DBG_LINE='roster' + self._data = {} + self.set=None + self._exported_methods=[self.getRoster] + + def plugin(self,owner,request=1): + """ Register presence and subscription trackers in the owner's dispatcher. + Also request roster from server if the 'request' argument is set. + Used internally.""" + self._owner.RegisterHandler('iq',self.RosterIqHandler,'result',NS_ROSTER) + self._owner.RegisterHandler('iq',self.RosterIqHandler,'set',NS_ROSTER) + self._owner.RegisterHandler('presence',self.PresenceHandler) + if request: self.Request() + + def Request(self,force=0): + """ Request roster from server if it were not yet requested + (or if the 'force' argument is set). """ + if self.set is None: self.set=0 + elif not force: return + self._owner.send(Iq('get',NS_ROSTER)) + self.DEBUG('Roster requested from server','start') + + def getRoster(self): + """ Requests roster from server if neccessary and returns self.""" + if not self.set: self.Request() + while not self.set: self._owner.Process(10) + return self + + def RosterIqHandler(self,dis,stanza): + """ Subscription tracker. Used internally for setting items state in + internal roster representation. """ + for item in stanza.getTag('query').getTags('item'): + jid=item.getAttr('jid') + if item.getAttr('subscription')=='remove': + if self._data.has_key(jid): del self._data[jid] + raise NodeProcessed # a MUST + self.DEBUG('Setting roster item %s...'%jid,'ok') + if not self._data.has_key(jid): self._data[jid]={} + self._data[jid]['name']=item.getAttr('name') + self._data[jid]['ask']=item.getAttr('ask') + self._data[jid]['subscription']=item.getAttr('subscription') + self._data[jid]['groups']=[] + if not self._data[jid].has_key('resources'): self._data[jid]['resources']={} + for group in item.getTags('group'): self._data[jid]['groups'].append(group.getData()) + self._data[self._owner.User+'@'+self._owner.Server]={'resources':{},'name':None,'ask':None,'subscription':None,'groups':None,} + self.set=1 + raise NodeProcessed # a MUST. Otherwise you'll get back an <iq type='error'/> + + def PresenceHandler(self,dis,pres): + """ Presence tracker. Used internally for setting items' resources state in + internal roster representation. """ + jid=JID(pres.getFrom()) + if not self._data.has_key(jid.getStripped()): self._data[jid.getStripped()]={'name':None,'ask':None,'subscription':'none','groups':['Not in roster'],'resources':{}} + + item=self._data[jid.getStripped()] + typ=pres.getType() + + if not typ: + self.DEBUG('Setting roster item %s for resource %s...'%(jid.getStripped(),jid.getResource()),'ok') + item['resources'][jid.getResource()]=res={'show':None,'status':None,'priority':'0','timestamp':None} + if pres.getTag('show'): res['show']=pres.getShow() + if pres.getTag('status'): res['status']=pres.getStatus() + if pres.getTag('priority'): res['priority']=pres.getPriority() + if not pres.getTimestamp(): pres.setTimestamp() + res['timestamp']=pres.getTimestamp() + elif typ=='unavailable' and item['resources'].has_key(jid.getResource()): del item['resources'][jid.getResource()] + # Need to handle type='error' also + + def _getItemData(self,jid,dataname): + """ Return specific jid's representation in internal format. Used internally. """ + jid=jid[:(jid+'/').find('/')] + return self._data[jid][dataname] + def _getResourceData(self,jid,dataname): + """ Return specific jid's resource representation in internal format. Used internally. """ + if jid.find('/')+1: + jid,resource=jid.split('/',1) + if self._data[jid]['resources'].has_key(resource): return self._data[jid]['resources'][resource][dataname] + elif self._data[jid]['resources'].keys(): + lastpri=-129 + for r in self._data[jid]['resources'].keys(): + if int(self._data[jid]['resources'][r]['priority'])>lastpri: resource,lastpri=r,int(self._data[jid]['resources'][r]['priority']) + return self._data[jid]['resources'][resource][dataname] + def delItem(self,jid): + """ Delete contact 'jid' from roster.""" + self._owner.send(Iq('set',NS_ROSTER,payload=[Node('item',{'jid':jid,'subscription':'remove'})])) + def getAsk(self,jid): + """ Returns 'ask' value of contact 'jid'.""" + return self._getItemData(jid,'ask') + def getGroups(self,jid): + """ Returns groups list that contact 'jid' belongs to.""" + return self._getItemData(jid,'groups') + def getName(self,jid): + """ Returns name of contact 'jid'.""" + return self._getItemData(jid,'name') + def getPriority(self,jid): + """ Returns priority of contact 'jid'. 'jid' should be a full (not bare) JID.""" + return self._getResourceData(jid,'priority') + def getRawRoster(self): + """ Returns roster representation in internal format. """ + return self._data + def getRawItem(self,jid): + """ Returns roster item 'jid' representation in internal format. """ + return self._data[jid[:(jid+'/').find('/')]] + def getShow(self, jid): + """ Returns 'show' value of contact 'jid'. 'jid' should be a full (not bare) JID.""" + return self._getResourceData(jid,'show') + def getStatus(self, jid): + """ Returns 'status' value of contact 'jid'. 'jid' should be a full (not bare) JID.""" + return self._getResourceData(jid,'status') + def getSubscription(self,jid): + """ Returns 'subscription' value of contact 'jid'.""" + return self._getItemData(jid,'subscription') + def getResources(self,jid): + """ Returns list of connected resources of contact 'jid'.""" + return self._data[jid[:(jid+'/').find('/')]]['resources'].keys() + def setItem(self,jid,name=None,groups=[]): + """ Creates/renames contact 'jid' and sets the groups list that it now belongs to.""" + iq=Iq('set',NS_ROSTER) + query=iq.getTag('query') + attrs={'jid':jid} + if name: attrs['name']=name + item=query.setTag('item',attrs) + for group in groups: item.addChild(node=Node('group',payload=[group])) + self._owner.send(iq) + def getItems(self): + """ Return list of all [bare] JIDs that the roster is currently tracks.""" + return self._data.keys() + def keys(self): + """ Same as getItems. Provided for the sake of dictionary interface.""" + return self._data.keys() + def __getitem__(self,item): + """ Get the contact in the internal format. Raises KeyError if JID 'item' is not in roster.""" + return self._data[item] + def getItem(self,item): + """ Get the contact in the internal format (or None if JID 'item' is not in roster).""" + if self._data.has_key(item): return self._data[item] + def Subscribe(self,jid): + """ Send subscription request to JID 'jid'.""" + self._owner.send(Presence(jid,'subscribe')) + def Unsubscribe(self,jid): + """ Ask for removing our subscription for JID 'jid'.""" + self._owner.send(Presence(jid,'unsubscribe')) + def Authorize(self,jid): + """ Authorise JID 'jid'. Works only if these JID requested auth previously. """ + self._owner.send(Presence(jid,'subscribed')) + def Unauthorize(self,jid): + """ Unauthorise JID 'jid'. Use for declining authorisation request + or for removing existing authorization. """ + self._owner.send(Presence(jid,'unsubscribed')) diff --git a/src/xmpppy-0.5.0rc1/xmpp/session.py b/src/xmpppy-0.5.0rc1/xmpp/session.py new file mode 100644 index 00000000..24066b32 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/xmpp/session.py @@ -0,0 +1,349 @@ +## +## XMPP server +## +## Copyright (C) 2004 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +__version__="$Id" + +""" +When your handler is called it is getting the session instance as the first argument. +This is the difference from xmpppy 0.1 where you got the "Client" instance. +With Session class you can have "multi-session" client instead of having +one client for each connection. Is is specifically important when you are +writing the server. +""" + +from protocol import * + +# Transport-level flags +SOCKET_UNCONNECTED =0 +SOCKET_ALIVE =1 +SOCKET_DEAD =2 +# XML-level flags +STREAM__NOT_OPENED =1 +STREAM__OPENED =2 +STREAM__CLOSING =3 +STREAM__CLOSED =4 +# XMPP-session flags +SESSION_NOT_AUTHED =1 +SESSION_AUTHED =2 +SESSION_BOUND =3 +SESSION_OPENED =4 +SESSION_CLOSED =5 + +class Session: + """ + The Session class instance is used for storing all session-related info like + credentials, socket/xml stream/session state flags, roster items (in case of + client type connection) etc. + Session object have no means of discovering is any info is ready to be read. + Instead you should use poll() (recomended) or select() methods for this purpose. + Session can be one of two types: 'server' and 'client'. 'server' session handles + inbound connection and 'client' one used to create an outbound one. + Session instance have multitude of internal attributes. The most imporant is the 'peer' one. + It is set once the peer is authenticated (client). + """ + def __init__(self,socket,owner,xmlns=None,peer=None): + """ When the session is created it's type (client/server) is determined from the beginning. + socket argument is the pre-created socket-like object. + It must have the following methods: send, recv, fileno, close. + owner is the 'master' instance that have Dispatcher plugged into it and generally + will take care about all session events. + xmlns is the stream namespace that will be used. Client must set this argument + If server sets this argument than stream will be dropped if opened with some another namespace. + peer is the name of peer instance. This is the flag that differentiates client session from + server session. Client must set it to the name of the server that will be connected, server must + leave this argument alone. + """ + self.xmlns=xmlns + if peer: + self.TYP='client' + self.peer=peer + self._socket_state=SOCKET_UNCONNECTED + else: + self.TYP='server' + self.peer=None + self._socket_state=SOCKET_ALIVE + self._sock=socket + self._send=socket.send + self._recv=socket.recv + self.fileno=socket.fileno + self._registered=0 + + self.Dispatcher=owner.Dispatcher + self.DBG_LINE='session' + self.DEBUG=owner.Dispatcher.DEBUG + self._expected={} + self._owner=owner + if self.TYP=='server': self.ID=`random.random()`[2:] + else: self.ID=None + + self.sendbuffer='' + self._stream_pos_queued=None + self._stream_pos_sent=0 + self.deliver_key_queue=[] + self.deliver_queue_map={} + self.stanza_queue=[] + + self._session_state=SESSION_NOT_AUTHED + self.waiting_features=[] + for feature in [NS_TLS,NS_SASL,NS_BIND,NS_SESSION]: + if feature in owner.features: self.waiting_features.append(feature) + self.features=[] + self.feature_in_process=None + self.slave_session=None + self.StartStream() + + def StartStream(self): + """ This method is used to initialise the internal xml expat parser + and to send initial stream header (in case of client connection). + Should be used after initial connection and after every stream restart.""" + self._stream_state=STREAM__NOT_OPENED + self.Stream=simplexml.NodeBuilder() + self.Stream._dispatch_depth=2 + self.Stream.dispatch=self._dispatch + self.Parse=self.Stream.Parse + self.Stream.stream_footer_received=self._stream_close + if self.TYP=='client': + self.Stream.stream_header_received=self._catch_stream_id + self._stream_open() + else: + self.Stream.stream_header_received=self._stream_open + + def receive(self): + """ Reads all pending incoming data. + Raises IOError on disconnection. + Blocks until at least one byte is read.""" + try: received = self._recv(10240) + except: received = '' + + if len(received): # length of 0 means disconnect + self.DEBUG(`self.fileno()`+' '+received,'got') + else: + self.DEBUG('Socket error while receiving data','error') + self.set_socket_state(SOCKET_DEAD) + raise IOError("Peer disconnected") + return received + + def sendnow(self,chunk): + """ Put chunk into "immidiatedly send" queue. + Should only be used for auth/TLS stuff and like. + If you just want to shedule regular stanza for delivery use enqueue method. + """ + if isinstance(chunk,Node): chunk = chunk.__str__().encode('utf-8') + elif type(chunk)==type(u''): chunk = chunk.encode('utf-8') + self.enqueue(chunk) + + def enqueue(self,stanza): + """ Takes Protocol instance as argument. + Puts stanza into "send" fifo queue. Items into the send queue are hold until + stream authenticated. After that this method is effectively the same as "sendnow" method.""" + if isinstance(stanza,Protocol): + self.stanza_queue.append(stanza) + else: self.sendbuffer+=stanza + if self._socket_state>=SOCKET_ALIVE: self.push_queue() + + def push_queue(self,failreason=ERR_RECIPIENT_UNAVAILABLE): + """ If stream is authenticated than move items from "send" queue to "immidiatedly send" queue. + Else if the stream is failed then return all queued stanzas with error passed as argument. + Otherwise do nothing.""" + # If the stream authed - convert stanza_queue into sendbuffer and set the checkpoints + + if self._stream_state>=STREAM__CLOSED or self._socket_state>=SOCKET_DEAD: # the stream failed. Return all stanzas that are still waiting for delivery. + self._owner.deactivatesession(self) + for key in self.deliver_key_queue: # Not sure. May be I + self._dispatch(Error(self.deliver_queue_map[key],failreason),trusted=1) # should simply re-dispatch it? + for stanza in self.stanza_queue: # But such action can invoke + self._dispatch(Error(stanza,failreason),trusted=1) # Infinite loops in case of S2S connection... + self.deliver_queue_map,self.deliver_key_queue,self.stanza_queue={},[],[] + return + elif self._session_state>=SESSION_AUTHED: # FIXME! äÏÌÖÅÎ ÂÙÔØ ËÁËÏÊ-ÔÏ ÄÒÕÇÏÊ ÆÌÁÇ. + #### LOCK_QUEUE + for stanza in self.stanza_queue: + txt=stanza.__str__().encode('utf-8') + self.sendbuffer+=txt + self._stream_pos_queued+=len(txt) # should be re-evaluated for SSL connection. + self.deliver_queue_map[self._stream_pos_queued]=stanza # position of the stream when stanza will be successfully and fully sent + self.deliver_key_queue.append(self._stream_pos_queued) + self.stanza_queue=[] + #### UNLOCK_QUEUE + + def flush_queue(self): + """ Put the "immidiatedly send" queue content on the wire. Blocks until at least one byte sent.""" + if self.sendbuffer: + try: + # LOCK_QUEUE + sent=self._send(self.sendbuffer) # âÌÏËÉÒÕÀÝÁÑ ÛÔÕÞËÁ! + except: + # UNLOCK_QUEUE + self.set_socket_state(SOCKET_DEAD) + self.DEBUG("Socket error while sending data",'error') + return self.terminate_stream() + self.DEBUG(`self.fileno()`+' '+self.sendbuffer[:sent],'sent') + self._stream_pos_sent+=sent + self.sendbuffer=self.sendbuffer[sent:] + self._stream_pos_delivered=self._stream_pos_sent # Should be acquired from socket somehow. Take SSL into account. + while self.deliver_key_queue and self._stream_pos_delivered>self.deliver_key_queue[0]: + del self.deliver_queue_map[self.deliver_key_queue[0]] + self.deliver_key_queue.remove(self.deliver_key_queue[0]) + # UNLOCK_QUEUE + + def _dispatch(self,stanza,trusted=0): + """ This is callback that is used to pass the received stanza forth to owner's dispatcher + _if_ the stream is authorised. Otherwise the stanza is just dropped. + The 'trusted' argument is used to emulate stanza receive. + This method is used internally. + """ + self._owner.packets+=1 + if self._stream_state==STREAM__OPENED or trusted: # if the server really should reject all stanzas after he is closed stream (himeself)? + self.DEBUG(stanza.__str__(),'dispatch') + stanza.trusted=trusted + return self.Dispatcher.dispatch(stanza,self) + + def _catch_stream_id(self,ns=None,tag='stream',attrs={}): + """ This callback is used to detect the stream namespace of incoming stream. Used internally. """ + if not attrs.has_key('id') or not attrs['id']: + return self.terminate_stream(STREAM_INVALID_XML) + self.ID=attrs['id'] + if not attrs.has_key('version'): self._owner.Dialback(self) + + def _stream_open(self,ns=None,tag='stream',attrs={}): + """ This callback is used to handle opening stream tag of the incoming stream. + In the case of client session it just make some validation. + Server session also sends server headers and if the stream valid the features node. + Used internally. """ + text='<?xml version="1.0" encoding="utf-8"?>\n<stream:stream' + if self.TYP=='client': + text+=' to="%s"'%self.peer + else: + text+=' id="%s"'%self.ID + if not attrs.has_key('to'): text+=' from="%s"'%self._owner.servernames[0] + else: text+=' from="%s"'%attrs['to'] + if attrs.has_key('xml:lang'): text+=' xml:lang="%s"'%attrs['xml:lang'] + if self.xmlns: xmlns=self.xmlns + else: xmlns=NS_SERVER + text+=' xmlns:db="%s" xmlns:stream="%s" xmlns="%s"'%(NS_DIALBACK,NS_STREAMS,xmlns) + if attrs.has_key('version') or self.TYP=='client': text+=' version="1.0"' + self.sendnow(text+'>') + self.set_stream_state(STREAM__OPENED) + if self.TYP=='client': return + if tag<>'stream': return self.terminate_stream(STREAM_INVALID_XML) + if ns<>NS_STREAMS: return self.terminate_stream(STREAM_INVALID_NAMESPACE) + if self.Stream.xmlns<>self.xmlns: return self.terminate_stream(STREAM_BAD_NAMESPACE_PREFIX) + if not attrs.has_key('to'): return self.terminate_stream(STREAM_IMPROPER_ADDRESSING) + if attrs['to'] not in self._owner.servernames: return self.terminate_stream(STREAM_HOST_UNKNOWN) + self.ourname=attrs['to'].lower() + if self.TYP=='server' and attrs.has_key('version'): + # send features + features=Node('stream:features') + if NS_TLS in self.waiting_features: + features.NT.starttls.setNamespace(NS_TLS) + features.T.starttls.NT.required + if NS_SASL in self.waiting_features: + features.NT.mechanisms.setNamespace(NS_SASL) + for mec in self._owner.SASL.mechanisms: + features.T.mechanisms.NT.mechanism=mec + else: + if NS_BIND in self.waiting_features: features.NT.bind.setNamespace(NS_BIND) + if NS_SESSION in self.waiting_features: features.NT.session.setNamespace(NS_SESSION) + self.sendnow(features) + + def feature(self,feature): + """ Declare some stream feature as activated one. """ + if feature not in self.features: self.features.append(feature) + self.unfeature(feature) + + def unfeature(self,feature): + """ Declare some feature as illegal. Illegal features can not be used. + Example: BIND feature becomes illegal after Non-SASL auth. """ + if feature in self.waiting_features: self.waiting_features.remove(feature) + + def _stream_close(self,unregister=1): + """ Write the closing stream tag and destroy the underlaying socket. Used internally. """ + if self._stream_state>=STREAM__CLOSED: return + self.set_stream_state(STREAM__CLOSING) + self.sendnow('</stream:stream>') + self.set_stream_state(STREAM__CLOSED) + self.push_queue() # decompose queue really since STREAM__CLOSED + self._owner.flush_queues() + if unregister: self._owner.unregistersession(self) + self._destroy_socket() + + def terminate_stream(self,error=None,unregister=1): + """ Notify the peer about stream closure. + Ensure that xmlstream is not brokes - i.e. if the stream isn't opened yet - + open it before closure. + If the error condition is specified than create a stream error and send it along with + closing stream tag. + Emulate receiving 'unavailable' type presence just before stream closure. + """ + if self._stream_state>=STREAM__CLOSING: return + if self._stream_state<STREAM__OPENED: + self.set_stream_state(STREAM__CLOSING) + self._stream_open() + else: + self.set_stream_state(STREAM__CLOSING) + p=Presence(typ='unavailable') + p.setNamespace(NS_CLIENT) + self._dispatch(p,trusted=1) + if error: + if isinstance(error,Node): self.sendnow(error) + else: self.sendnow(ErrorNode(error)) + self._stream_close(unregister=unregister) + if self.slave_session: + self.slave_session.terminate_stream(STREAM_REMOTE_CONNECTION_FAILED) + + def _destroy_socket(self): + """ Break cyclic dependancies to let python's GC free memory right now.""" + self.Stream.dispatch=None + self.Stream.stream_footer_received=None + self.Stream.stream_header_received=None + self.Stream.destroy() + self._sock.close() + self.set_socket_state(SOCKET_DEAD) + + def start_feature(self,f): + """ Declare some feature as "negotiating now" to prevent other features from start negotiating. """ + if self.feature_in_process: raise "Starting feature %s over %s !"%(f,self.feature_in_process) + self.feature_in_process=f + + def stop_feature(self,f): + """ Declare some feature as "negotiated" to allow other features start negotiating. """ + if self.feature_in_process<>f: raise "Stopping feature %s instead of %s !"%(f,self.feature_in_process) + self.feature_in_process=None + + def set_socket_state(self,newstate): + """ Change the underlaying socket state. + Socket starts with SOCKET_UNCONNECTED state + and then proceeds (possibly) to SOCKET_ALIVE + and then to SOCKET_DEAD """ + if self._socket_state<newstate: self._socket_state=newstate + + def set_session_state(self,newstate): + """ Change the session state. + Session starts with SESSION_NOT_AUTHED state + and then comes through + SESSION_AUTHED, SESSION_BOUND, SESSION_OPENED and SESSION_CLOSED states. + """ + if self._session_state<newstate: + if self._session_state<SESSION_AUTHED and \ + newstate>=SESSION_AUTHED: self._stream_pos_queued=self._stream_pos_sent + self._session_state=newstate + + def set_stream_state(self,newstate): + """ Change the underlaying XML stream state + Stream starts with STREAM__NOT_OPENED and then proceeds with + STREAM__OPENED, STREAM__CLOSING and STREAM__CLOSED states. + Note that some features (like TLS and SASL) + requires stream re-start so this state can have non-linear changes. """ + if self._stream_state<newstate: self._stream_state=newstate diff --git a/src/xmpppy-0.5.0rc1/xmpp/simplexml.py b/src/xmpppy-0.5.0rc1/xmpp/simplexml.py new file mode 100644 index 00000000..99383dea --- /dev/null +++ b/src/xmpppy-0.5.0rc1/xmpp/simplexml.py @@ -0,0 +1,485 @@ +## simplexml.py based on Mattew Allum's xmlstream.py +## +## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: simplexml.py,v 1.34 2009/03/03 10:24:02 normanr Exp $ + +"""Simplexml module provides xmpppy library with all needed tools to handle XML nodes and XML streams. +I'm personally using it in many other separate projects. It is designed to be as standalone as possible.""" + +import xml.parsers.expat + +def XMLescape(txt): + """Returns provided string with symbols & < > " replaced by their respective XML entities.""" + # replace also FORM FEED and ESC, because they are not valid XML chars + return txt.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """).replace(u'\x0C', "").replace(u'\x1B', "") + +ENCODING='utf-8' +def ustr(what): + """Converts object "what" to unicode string using it's own __str__ method if accessible or unicode method otherwise.""" + if isinstance(what, unicode): return what + try: r=what.__str__() + except AttributeError: r=str(what) + if not isinstance(r, unicode): return unicode(r,ENCODING) + return r + +class Node(object): + """ Node class describes syntax of separate XML Node. It have a constructor that permits node creation + from set of "namespace name", attributes and payload of text strings and other nodes. + It does not natively support building node from text string and uses NodeBuilder class for that purpose. + After creation node can be mangled in many ways so it can be completely changed. + Also node can be serialised into string in one of two modes: default (where the textual representation + of node describes it exactly) and "fancy" - with whitespace added to make indentation and thus make + result more readable by human. + + Node class have attribute FORCE_NODE_RECREATION that is defaults to False thus enabling fast node + replication from the some other node. The drawback of the fast way is that new node shares some + info with the "original" node that is changing the one node may influence the other. Though it is + rarely needed (in xmpppy it is never needed at all since I'm usually never using original node after + replication (and using replication only to move upwards on the classes tree). + """ + FORCE_NODE_RECREATION=0 + def __init__(self, tag=None, attrs={}, payload=[], parent=None, nsp=None, node_built=False, node=None): + """ Takes "tag" argument as the name of node (prepended by namespace, if needed and separated from it + by a space), attrs dictionary as the set of arguments, payload list as the set of textual strings + and child nodes that this node carries within itself and "parent" argument that is another node + that this one will be the child of. Also the __init__ can be provided with "node" argument that is + either a text string containing exactly one node or another Node instance to begin with. If both + "node" and other arguments is provided then the node initially created as replica of "node" + provided and then modified to be compliant with other arguments.""" + if node: + if self.FORCE_NODE_RECREATION and isinstance(node, Node): + node=str(node) + if not isinstance(node, Node): + node=NodeBuilder(node,self) + node_built = True + else: + self.name,self.namespace,self.attrs,self.data,self.kids,self.parent,self.nsd = node.name,node.namespace,{},[],[],node.parent,{} + for key in node.attrs.keys(): self.attrs[key]=node.attrs[key] + for data in node.data: self.data.append(data) + for kid in node.kids: self.kids.append(kid) + for k,v in node.nsd.items(): self.nsd[k] = v + else: self.name,self.namespace,self.attrs,self.data,self.kids,self.parent,self.nsd = 'tag','',{},[],[],None,{} + if parent: + self.parent = parent + self.nsp_cache = {} + if nsp: + for k,v in nsp.items(): self.nsp_cache[k] = v + for attr,val in attrs.items(): + if attr == 'xmlns': + self.nsd[u''] = val + elif attr.startswith('xmlns:'): + self.nsd[attr[6:]] = val + self.attrs[attr]=attrs[attr] + if tag: + if node_built: + pfx,self.name = (['']+tag.split(':'))[-2:] + self.namespace = self.lookup_nsp(pfx) + else: + if ' ' in tag: + self.namespace,self.name = tag.split() + else: + self.name = tag + if isinstance(payload, basestring): payload=[payload] + for i in payload: + if isinstance(i, Node): self.addChild(node=i) + else: self.data.append(ustr(i)) + + def lookup_nsp(self,pfx=''): + ns = self.nsd.get(pfx,None) + if ns is None: + ns = self.nsp_cache.get(pfx,None) + if ns is None: + if self.parent: + ns = self.parent.lookup_nsp(pfx) + self.nsp_cache[pfx] = ns + else: + return 'http://www.gajim.org/xmlns/undeclared' + return ns + + def __str__(self,fancy=0): + """ Method used to dump node into textual representation. + if "fancy" argument is set to True produces indented output for readability.""" + s = (fancy-1) * 2 * ' ' + "<" + self.name + if self.namespace: + if not self.parent or self.parent.namespace!=self.namespace: + if 'xmlns' not in self.attrs: + s = s + ' xmlns="%s"'%self.namespace + for key in self.attrs.keys(): + val = ustr(self.attrs[key]) + s = s + ' %s="%s"' % ( key, XMLescape(val) ) + s = s + ">" + cnt = 0 + if self.kids: + if fancy: s = s + "\n" + for a in self.kids: + if not fancy and (len(self.data)-1)>=cnt: s=s+XMLescape(self.data[cnt]) + elif (len(self.data)-1)>=cnt: s=s+XMLescape(self.data[cnt].strip()) + if isinstance(a, Node): + s = s + a.__str__(fancy and fancy+1) + elif a: + s = s + a.__str__() + cnt=cnt+1 + if not fancy and (len(self.data)-1) >= cnt: s = s + XMLescape(self.data[cnt]) + elif (len(self.data)-1) >= cnt: s = s + XMLescape(self.data[cnt].strip()) + if not self.kids and s.endswith('>'): + s=s[:-1]+' />' + if fancy: s = s + "\n" + else: + if fancy and not self.data: s = s + (fancy-1) * 2 * ' ' + s = s + "</" + self.name + ">" + if fancy: s = s + "\n" + return s + def getCDATA(self): + """ Serialise node, dropping all tags and leaving CDATA intact. + That is effectively kills all formatiing, leaving only text were contained in XML. + """ + s = "" + cnt = 0 + if self.kids: + for a in self.kids: + s=s+self.data[cnt] + if a: s = s + a.getCDATA() + cnt=cnt+1 + if (len(self.data)-1) >= cnt: s = s + self.data[cnt] + return s + def addChild(self, name=None, attrs={}, payload=[], namespace=None, node=None): + """ If "node" argument is provided, adds it as child node. Else creates new node from + the other arguments' values and adds it as well.""" + if 'xmlns' in attrs: + raise AttributeError("Use namespace=x instead of attrs={'xmlns':x}") + if node: + newnode=node + node.parent = self + else: newnode=Node(tag=name, parent=self, attrs=attrs, payload=payload) + if namespace: + newnode.setNamespace(namespace) + self.kids.append(newnode) + self.data.append(u'') + return newnode + def addData(self, data): + """ Adds some CDATA to node. """ + self.data.append(ustr(data)) + self.kids.append(None) + def clearData(self): + """ Removes all CDATA from the node. """ + self.data=[] + def delAttr(self, key): + """ Deletes an attribute "key" """ + del self.attrs[key] + def delChild(self, node, attrs={}): + """ Deletes the "node" from the node's childs list, if "node" is an instance. + Else deletes the first node that have specified name and (optionally) attributes. """ + if not isinstance(node, Node): node=self.getTag(node,attrs) + self.kids[self.kids.index(node)]=None + return node + def getAttrs(self): + """ Returns all node's attributes as dictionary. """ + return self.attrs + def getAttr(self, key): + """ Returns value of specified attribute. """ + try: return self.attrs[key] + except: return None + def getChildren(self): + """ Returns all node's child nodes as list. """ + return self.kids + def getData(self): + """ Returns all node CDATA as string (concatenated). """ + return ''.join(self.data) + def getName(self): + """ Returns the name of node """ + return self.name + def getNamespace(self): + """ Returns the namespace of node """ + return self.namespace + def getParent(self): + """ Returns the parent of node (if present). """ + return self.parent + def getPayload(self): + """ Return the payload of node i.e. list of child nodes and CDATA entries. + F.e. for "<node>text1<nodea/><nodeb/> text2</node>" will be returned list: + ['text1', <nodea instance>, <nodeb instance>, ' text2']. """ + ret=[] + for i in range(max(len(self.data),len(self.kids))): + if i < len(self.data) and self.data[i]: ret.append(self.data[i]) + if i < len(self.kids) and self.kids[i]: ret.append(self.kids[i]) + return ret + def getTag(self, name, attrs={}, namespace=None): + """ Filters all child nodes using specified arguments as filter. + Returns the first found or None if not found. """ + return self.getTags(name, attrs, namespace, one=1) + def getTagAttr(self,tag,attr): + """ Returns attribute value of the child with specified name (or None if no such attribute).""" + try: return self.getTag(tag).attrs[attr] + except: return None + def getTagData(self,tag): + """ Returns cocatenated CDATA of the child with specified name.""" + try: return self.getTag(tag).getData() + except: return None + def getTags(self, name, attrs={}, namespace=None, one=0): + """ Filters all child nodes using specified arguments as filter. + Returns the list of nodes found. """ + nodes=[] + for node in self.kids: + if not node: continue + if namespace and namespace!=node.getNamespace(): continue + if node.getName() == name: + for key in attrs.keys(): + if key not in node.attrs or node.attrs[key]!=attrs[key]: break + else: nodes.append(node) + if one and nodes: return nodes[0] + if not one: return nodes + + def iterTags(self, name, attrs={}, namespace=None): + """ Iterate over all children using specified arguments as filter. """ + for node in self.kids: + if not node: continue + if namespace is not None and namespace!=node.getNamespace(): continue + if node.getName() == name: + for key in attrs.keys(): + if key not in node.attrs or \ + node.attrs[key]!=attrs[key]: break + else: + yield node + + def setAttr(self, key, val): + """ Sets attribute "key" with the value "val". """ + self.attrs[key]=val + def setData(self, data): + """ Sets node's CDATA to provided string. Resets all previous CDATA!""" + self.data=[ustr(data)] + def setName(self,val): + """ Changes the node name. """ + self.name = val + def setNamespace(self, namespace): + """ Changes the node namespace. """ + self.namespace=namespace + def setParent(self, node): + """ Sets node's parent to "node". WARNING: do not checks if the parent already present + and not removes the node from the list of childs of previous parent. """ + self.parent = node + def setPayload(self,payload,add=0): + """ Sets node payload according to the list specified. WARNING: completely replaces all node's + previous content. If you wish just to add child or CDATA - use addData or addChild methods. """ + if isinstance(payload, basestring): payload=[payload] + if add: self.kids+=payload + else: self.kids=payload + def setTag(self, name, attrs={}, namespace=None): + """ Same as getTag but if the node with specified namespace/attributes not found, creates such + node and returns it. """ + node=self.getTags(name, attrs, namespace=namespace, one=1) + if node: return node + else: return self.addChild(name, attrs, namespace=namespace) + def setTagAttr(self,tag,attr,val): + """ Creates new node (if not already present) with name "tag" + and sets it's attribute "attr" to value "val". """ + try: self.getTag(tag).attrs[attr]=val + except: self.addChild(tag,attrs={attr:val}) + def setTagData(self,tag,val,attrs={}): + """ Creates new node (if not already present) with name "tag" and (optionally) attributes "attrs" + and sets it's CDATA to string "val". """ + try: self.getTag(tag,attrs).setData(ustr(val)) + except: self.addChild(tag,attrs,payload=[ustr(val)]) + def has_attr(self,key): + """ Checks if node have attribute "key".""" + return key in self.attrs + def __getitem__(self,item): + """ Returns node's attribute "item" value. """ + return self.getAttr(item) + def __setitem__(self,item,val): + """ Sets node's attribute "item" value. """ + return self.setAttr(item,val) + def __delitem__(self,item): + """ Deletes node's attribute "item". """ + return self.delAttr(item) + def __getattr__(self,attr): + """ Reduce memory usage caused by T/NT classes - use memory only when needed. """ + if attr=='T': + self.T=T(self) + return self.T + if attr=='NT': + self.NT=NT(self) + return self.NT + raise AttributeError + +class T: + """ Auxiliary class used to quick access to node's child nodes. """ + def __init__(self,node): self.__dict__['node']=node + def __getattr__(self,attr): return self.node.getTag(attr) + def __setattr__(self,attr,val): + if isinstance(val,Node): Node.__init__(self.node.setTag(attr),node=val) + else: return self.node.setTagData(attr,val) + def __delattr__(self,attr): return self.node.delChild(attr) + +class NT(T): + """ Auxiliary class used to quick create node's child nodes. """ + def __getattr__(self,attr): return self.node.addChild(attr) + def __setattr__(self,attr,val): + if isinstance(val,Node): self.node.addChild(attr,node=val) + else: return self.node.addChild(attr,payload=[val]) + +DBG_NODEBUILDER = 'nodebuilder' +class NodeBuilder: + """ Builds a Node class minidom from data parsed to it. This class used for two purposes: + 1. Creation an XML Node from a textual representation. F.e. reading a config file. See an XML2Node method. + 2. Handling an incoming XML stream. This is done by mangling + the __dispatch_depth parameter and redefining the dispatch method. + You do not need to use this class directly if you do not designing your own XML handler.""" + def __init__(self,data=None,initial_node=None): + """ Takes two optional parameters: "data" and "initial_node". + By default class initialised with empty Node class instance. + Though, if "initial_node" is provided it used as "starting point". + You can think about it as of "node upgrade". + "data" (if provided) feeded to parser immidiatedly after instance init. + """ + self.DEBUG(DBG_NODEBUILDER, "Preparing to handle incoming XML stream.", 'start') + self._parser = xml.parsers.expat.ParserCreate() + self._parser.StartElementHandler = self.starttag + self._parser.EndElementHandler = self.endtag + self._parser.CharacterDataHandler = self.handle_cdata + self._parser.StartNamespaceDeclHandler = self.handle_namespace_start + self._parser.buffer_text = True + self.Parse = self._parser.Parse + + self.__depth = 0 + self.__last_depth = 0 + self.__max_depth = 0 + self._dispatch_depth = 1 + self._document_attrs = None + self._document_nsp = None + self._mini_dom=initial_node + self.last_is_data = 1 + self._ptr=None + self.data_buffer = None + self.streamError = '' + if data: + self._parser.Parse(data,1) + + def check_data_buffer(self): + if self.data_buffer: + self._ptr.data.append(''.join(self.data_buffer)) + del self.data_buffer[:] + self.data_buffer = None + + def destroy(self): + """ Method used to allow class instance to be garbage-collected. """ + self.check_data_buffer() + self._parser.StartElementHandler = None + self._parser.EndElementHandler = None + self._parser.CharacterDataHandler = None + self._parser.StartNamespaceDeclHandler = None + + def starttag(self, tag, attrs): + """XML Parser callback. Used internally""" + self.check_data_buffer() + self._inc_depth() + self.DEBUG(DBG_NODEBUILDER, "DEPTH -> %i , tag -> %s, attrs -> %s" % (self.__depth, tag, `attrs`), 'down') + if self.__depth == self._dispatch_depth: + if not self._mini_dom : + self._mini_dom = Node(tag=tag, attrs=attrs, nsp = self._document_nsp, node_built=True) + else: + Node.__init__(self._mini_dom,tag=tag, attrs=attrs, nsp = self._document_nsp, node_built=True) + self._ptr = self._mini_dom + elif self.__depth > self._dispatch_depth: + self._ptr.kids.append(Node(tag=tag,parent=self._ptr,attrs=attrs, node_built=True)) + self._ptr = self._ptr.kids[-1] + if self.__depth == 1: + self._document_attrs = {} + self._document_nsp = {} + nsp, name = (['']+tag.split(':'))[-2:] + for attr,val in attrs.items(): + if attr == 'xmlns': + self._document_nsp[u''] = val + elif attr.startswith('xmlns:'): + self._document_nsp[attr[6:]] = val + else: + self._document_attrs[attr] = val + ns = self._document_nsp.get(nsp, 'http://www.gajim.org/xmlns/undeclared-root') + try: + self.stream_header_received(ns, name, attrs) + except ValueError, e: + self._document_attrs = None + raise ValueError(str(e)) + if not self.last_is_data and self._ptr.parent: + self._ptr.parent.data.append('') + self.last_is_data = 0 + + def endtag(self, tag ): + """XML Parser callback. Used internally""" + self.DEBUG(DBG_NODEBUILDER, "DEPTH -> %i , tag -> %s" % (self.__depth, tag), 'up') + self.check_data_buffer() + if self.__depth == self._dispatch_depth: + if self._mini_dom.getName() == 'error': + self.streamError = self._mini_dom.getChildren()[0].getName() + self.dispatch(self._mini_dom) + elif self.__depth > self._dispatch_depth: + self._ptr = self._ptr.parent + else: + self.DEBUG(DBG_NODEBUILDER, "Got higher than dispatch level. Stream terminated?", 'stop') + self._dec_depth() + self.last_is_data = 0 + if self.__depth == 0: self.stream_footer_received() + + def handle_cdata(self, data): + """XML Parser callback. Used internally""" + self.DEBUG(DBG_NODEBUILDER, data, 'data') + if self.last_is_data: + if self.data_buffer: + self.data_buffer.append(data) + elif self._ptr: + self.data_buffer = [data] + self.last_is_data = 1 + + def handle_namespace_start(self, prefix, uri): + """XML Parser callback. Used internally""" + self.check_data_buffer() + + def DEBUG(self, level, text, comment=None): + """ Gets all NodeBuilder walking events. Can be used for debugging if redefined.""" + def getDom(self): + """ Returns just built Node. """ + self.check_data_buffer() + return self._mini_dom + def dispatch(self,stanza): + """ Gets called when the NodeBuilder reaches some level of depth on it's way up with the built + node as argument. Can be redefined to convert incoming XML stanzas to program events. """ + def stream_header_received(self,ns,tag,attrs): + """ Method called when stream just opened. """ + self.check_data_buffer() + def stream_footer_received(self): + """ Method called when stream just closed. """ + self.check_data_buffer() + + def has_received_endtag(self, level=0): + """ Return True if at least one end tag was seen (at level) """ + return self.__depth <= level and self.__max_depth > level + + def _inc_depth(self): + self.__last_depth = self.__depth + self.__depth += 1 + self.__max_depth = max(self.__depth, self.__max_depth) + + def _dec_depth(self): + self.__last_depth = self.__depth + self.__depth -= 1 + +def XML2Node(xml): + """ Converts supplied textual string into XML node. Handy f.e. for reading configuration file. + Raises xml.parser.expat.parsererror if provided string is not well-formed XML. """ + return NodeBuilder(xml).getDom() + +def BadXML2Node(xml): + """ Converts supplied textual string into XML node. Survives if xml data is cutted half way round. + I.e. "<html>some text <br>some more text". Will raise xml.parser.expat.parsererror on misplaced + tags though. F.e. "<b>some text <br>some more text</b>" will not work.""" + return NodeBuilder(xml).getDom() diff --git a/src/xmpppy-0.5.0rc1/xmpp/transports.py b/src/xmpppy-0.5.0rc1/xmpp/transports.py new file mode 100644 index 00000000..0e3eec90 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/xmpp/transports.py @@ -0,0 +1,339 @@ +## transports.py +## +## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: transports.py,v 1.35 2009/04/07 08:34:09 snakeru Exp $ + +""" +This module contains the low-level implementations of xmpppy connect methods or +(in other words) transports for xmpp-stanzas. +Currently here is three transports: +direct TCP connect - TCPsocket class +proxied TCP connect - HTTPPROXYsocket class (CONNECT proxies) +TLS connection - TLS class. Can be used for SSL connections also. + +Transports are stackable so you - f.e. TLS use HTPPROXYsocket or TCPsocket as more low-level transport. + +Also exception 'error' is defined to allow capture of this module specific exceptions. +""" + +import socket,select,base64,dispatcher,sys +from simplexml import ustr +from client import PlugIn +from protocol import * + +# determine which DNS resolution library is available +HAVE_DNSPYTHON = False +HAVE_PYDNS = False +try: + import dns.resolver # http://dnspython.org/ + HAVE_DNSPYTHON = True +except ImportError: + try: + import DNS # http://pydns.sf.net/ + HAVE_PYDNS = True + except ImportError: + pass + +DATA_RECEIVED='DATA RECEIVED' +DATA_SENT='DATA SENT' + +class error: + """An exception to be raised in case of low-level errors in methods of 'transports' module.""" + def __init__(self,comment): + """Cache the descriptive string""" + self._comment=comment + + def __str__(self): + """Serialise exception into pre-cached descriptive string.""" + return self._comment + +BUFLEN=1024 +class TCPsocket(PlugIn): + """ This class defines direct TCP connection method. """ + def __init__(self, server=None, use_srv=True): + """ Cache connection point 'server'. 'server' is the tuple of (host, port) + absolutely the same as standard tcp socket uses. However library will lookup for + ('_xmpp-client._tcp.' + host) SRV record in DNS and connect to the found (if it is) + server instead + """ + PlugIn.__init__(self) + self.DBG_LINE='socket' + self._exported_methods=[self.send,self.disconnect] + self._server, self.use_srv = server, use_srv + + def srv_lookup(self, server): + " SRV resolver. Takes server=(host, port) as argument. Returns new (host, port) pair " + if HAVE_DNSPYTHON or HAVE_PYDNS: + host, port = server + possible_queries = ['_xmpp-client._tcp.' + host] + + for query in possible_queries: + try: + if HAVE_DNSPYTHON: + answers = [x for x in dns.resolver.query(query, 'SRV')] + if answers: + host = str(answers[0].target) + port = int(answers[0].port) + break + elif HAVE_PYDNS: + # ensure we haven't cached an old configuration + DNS.DiscoverNameServers() + response = DNS.Request().req(query, qtype='SRV') + answers = response.answers + if len(answers) > 0: + # ignore the priority and weight for now + _, _, port, host = answers[0]['data'] + del _ + port = int(port) + break + except: + self.DEBUG('An error occurred while looking up %s' % query, 'warn') + server = (host, port) + else: + self.DEBUG("Could not load one of the supported DNS libraries (dnspython or pydns). SRV records will not be queried and you may need to set custom hostname/port for some servers to be accessible.\n",'warn') + # end of SRV resolver + return server + + def plugin(self, owner): + """ Fire up connection. Return non-empty string on success. + Also registers self.disconnected method in the owner's dispatcher. + Called internally. """ + if not self._server: self._server=(self._owner.Server,5222) + if self.use_srv: server=self.srv_lookup(self._server) + else: server=self._server + if not self.connect(server): return + self._owner.Connection=self + self._owner.RegisterDisconnectHandler(self.disconnected) + return 'ok' + + def getHost(self): + """ Return the 'host' value that is connection is [will be] made to.""" + return self._server[0] + def getPort(self): + """ Return the 'port' value that is connection is [will be] made to.""" + return self._server[1] + + def connect(self,server=None): + """ Try to connect to the given host/port. Does not lookup for SRV record. + Returns non-empty string on success. """ + try: + if not server: server=self._server + self._sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._sock.connect((server[0], int(server[1]))) + self._send=self._sock.sendall + self._recv=self._sock.recv + self.DEBUG("Successfully connected to remote host %s"%`server`,'start') + return 'ok' + except socket.error, (errno, strerror): + self.DEBUG("Failed to connect to remote host %s: %s (%s)"%(`server`, strerror, errno),'error') + except: pass + + def plugout(self): + """ Disconnect from the remote server and unregister self.disconnected method from + the owner's dispatcher. """ + self._sock.close() + if self._owner.__dict__.has_key('Connection'): + del self._owner.Connection + self._owner.UnregisterDisconnectHandler(self.disconnected) + + def receive(self): + """ Reads all pending incoming data. + In case of disconnection calls owner's disconnected() method and then raises IOError exception.""" + try: received = self._recv(BUFLEN) + except socket.sslerror,e: + self._seen_data=0 + if e[0]==socket.SSL_ERROR_WANT_READ: return '' + if e[0]==socket.SSL_ERROR_WANT_WRITE: return '' + self.DEBUG('Socket error while receiving data','error') + sys.exc_clear() + self._owner.disconnected() + raise IOError("Disconnected from server") + except: received = '' + + while self.pending_data(0): + try: add = self._recv(BUFLEN) + except: add='' + received +=add + if not add: break + + if len(received): # length of 0 means disconnect + self._seen_data=1 + self.DEBUG(received,'got') + if hasattr(self._owner, 'Dispatcher'): + self._owner.Dispatcher.Event('', DATA_RECEIVED, received) + else: + self.DEBUG('Socket error while receiving data','error') + self._owner.disconnected() + raise IOError("Disconnected from server") + return received + + def send(self,raw_data): + """ Writes raw outgoing data. Blocks until done. + If supplied data is unicode string, encodes it to utf-8 before send.""" + if type(raw_data)==type(u''): raw_data = raw_data.encode('utf-8') + elif type(raw_data)<>type(''): raw_data = ustr(raw_data).encode('utf-8') + try: + self._send(raw_data) + # Avoid printing messages that are empty keepalive packets. + if raw_data.strip(): + self.DEBUG(raw_data,'sent') + if hasattr(self._owner, 'Dispatcher'): # HTTPPROXYsocket will send data before we have a Dispatcher + self._owner.Dispatcher.Event('', DATA_SENT, raw_data) + except: + self.DEBUG("Socket error while sending data",'error') + self._owner.disconnected() + + def pending_data(self,timeout=0): + """ Returns true if there is a data ready to be read. """ + return select.select([self._sock],[],[],timeout)[0] + + def disconnect(self): + """ Closes the socket. """ + self.DEBUG("Closing socket",'stop') + self._sock.close() + + def disconnected(self): + """ Called when a Network Error or disconnection occurs. + Designed to be overidden. """ + self.DEBUG("Socket operation failed",'error') + +DBG_CONNECT_PROXY='CONNECTproxy' +class HTTPPROXYsocket(TCPsocket): + """ HTTP (CONNECT) proxy connection class. Uses TCPsocket as the base class + redefines only connect method. Allows to use HTTP proxies like squid with + (optionally) simple authentication (using login and password). """ + def __init__(self,proxy,server,use_srv=True): + """ Caches proxy and target addresses. + 'proxy' argument is a dictionary with mandatory keys 'host' and 'port' (proxy address) + and optional keys 'user' and 'password' to use for authentication. + 'server' argument is a tuple of host and port - just like TCPsocket uses. """ + TCPsocket.__init__(self,server,use_srv) + self.DBG_LINE=DBG_CONNECT_PROXY + self._proxy=proxy + + def plugin(self, owner): + """ Starts connection. Used interally. Returns non-empty string on success.""" + owner.debug_flags.append(DBG_CONNECT_PROXY) + return TCPsocket.plugin(self,owner) + + def connect(self,dupe=None): + """ Starts connection. Connects to proxy, supplies login and password to it + (if were specified while creating instance). Instructs proxy to make + connection to the target server. Returns non-empty sting on success. """ + if not TCPsocket.connect(self,(self._proxy['host'],self._proxy['port'])): return + self.DEBUG("Proxy server contacted, performing authentification",'start') + connector = ['CONNECT %s:%s HTTP/1.0'%self._server, + 'Proxy-Connection: Keep-Alive', + 'Pragma: no-cache', + 'Host: %s:%s'%self._server, + 'User-Agent: HTTPPROXYsocket/v0.1'] + if self._proxy.has_key('user') and self._proxy.has_key('password'): + credentials = '%s:%s'%(self._proxy['user'],self._proxy['password']) + credentials = base64.encodestring(credentials).strip() + connector.append('Proxy-Authorization: Basic '+credentials) + connector.append('\r\n') + self.send('\r\n'.join(connector)) + try: reply = self.receive().replace('\r','') + except IOError: + self.DEBUG('Proxy suddenly disconnected','error') + self._owner.disconnected() + return + try: proto,code,desc=reply.split('\n')[0].split(' ',2) + except: raise error('Invalid proxy reply') + if code<>'200': + self.DEBUG('Invalid proxy reply: %s %s %s'%(proto,code,desc),'error') + self._owner.disconnected() + return + while reply.find('\n\n') == -1: + try: reply += self.receive().replace('\r','') + except IOError: + self.DEBUG('Proxy suddenly disconnected','error') + self._owner.disconnected() + return + self.DEBUG("Authentification successfull. Jabber server contacted.",'ok') + return 'ok' + + def DEBUG(self,text,severity): + """Overwrites DEBUG tag to allow debug output be presented as "CONNECTproxy".""" + return self._owner.DEBUG(DBG_CONNECT_PROXY,text,severity) + +class TLS(PlugIn): + """ TLS connection used to encrypts already estabilished tcp connection.""" + def PlugIn(self,owner,now=0): + """ If the 'now' argument is true then starts using encryption immidiatedly. + If 'now' in false then starts encryption as soon as TLS feature is + declared by the server (if it were already declared - it is ok). + """ + if owner.__dict__.has_key('TLS'): return # Already enabled. + PlugIn.PlugIn(self,owner) + DBG_LINE='TLS' + if now: return self._startSSL() + if self._owner.Dispatcher.Stream.features: + try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) + except NodeProcessed: pass + else: self._owner.RegisterHandlerOnce('features',self.FeaturesHandler,xmlns=NS_STREAMS) + self.starttls=None + + def plugout(self,now=0): + """ Unregisters TLS handler's from owner's dispatcher. Take note that encription + can not be stopped once started. You can only break the connection and start over.""" + self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) + self._owner.UnregisterHandler('proceed',self.StartTLSHandler,xmlns=NS_TLS) + self._owner.UnregisterHandler('failure',self.StartTLSHandler,xmlns=NS_TLS) + + def FeaturesHandler(self, conn, feats): + """ Used to analyse server <features/> tag for TLS support. + If TLS is supported starts the encryption negotiation. Used internally""" + if not feats.getTag('starttls',namespace=NS_TLS): + self.DEBUG("TLS unsupported by remote server.",'warn') + return + self.DEBUG("TLS supported by remote server. Requesting TLS start.",'ok') + self._owner.RegisterHandlerOnce('proceed',self.StartTLSHandler,xmlns=NS_TLS) + self._owner.RegisterHandlerOnce('failure',self.StartTLSHandler,xmlns=NS_TLS) + self._owner.Connection.send('<starttls xmlns="%s"/>'%NS_TLS) + raise NodeProcessed + + def pending_data(self,timeout=0): + """ Returns true if there possible is a data ready to be read. """ + return self._tcpsock._seen_data or select.select([self._tcpsock._sock],[],[],timeout)[0] + + def _startSSL(self): + """ Immidiatedly switch socket to TLS mode. Used internally.""" + """ Here we should switch pending_data to hint mode.""" + tcpsock=self._owner.Connection + tcpsock._sslObj = socket.ssl(tcpsock._sock, None, None) + tcpsock._sslIssuer = tcpsock._sslObj.issuer() + tcpsock._sslServer = tcpsock._sslObj.server() + tcpsock._recv = tcpsock._sslObj.read + tcpsock._send = tcpsock._sslObj.write + + tcpsock._seen_data=1 + self._tcpsock=tcpsock + tcpsock.pending_data=self.pending_data + tcpsock._sock.setblocking(0) + + self.starttls='success' + + def StartTLSHandler(self, conn, starttls): + """ Handle server reply if TLS is allowed to process. Behaves accordingly. + Used internally.""" + if starttls.getNamespace()<>NS_TLS: return + self.starttls=starttls.getName() + if self.starttls=='failure': + self.DEBUG("Got starttls response: "+self.starttls,'error') + return + self.DEBUG("Got starttls proceed response. Switching to TLS/SSL...",'ok') + self._startSSL() + self._owner.Dispatcher.PlugOut() + dispatcher.Dispatcher().PlugIn(self._owner) |