diff --git a/apps/sam/code.leo b/apps/sam/code.leo
new file mode 100644
index 0000000000000000000000000000000000000000..52cd3703f650031d685bb6ca560d6c4162e2b373
--- /dev/null
+++ b/apps/sam/code.leo
@@ -0,0 +1,3430 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<leo_file>
+<leo_header file_format="2" tnodes="0" max_tnode_index="216" clone_windows="0"/>
+<globals body_outline_ratio="0.37624999999999997">
+	<global_window_position top="155" left="108" height="585" width="890"/>
+	<global_log_window_position top="0" left="0" height="0" width="0"/>
+</globals>
+<preferences>
+</preferences>
+<find_panel_settings>
+	<find_string></find_string>
+	<change_string></change_string>
+</find_panel_settings>
+<vnodes>
+<v t="davidmcnab.041004143447" a="E"><vh>I2P SAM Server and Client</vh>
+<v t="davidmcnab.041004144338" tnodeList="davidmcnab.041004144338,davidmcnab.041004144338.1,davidmcnab.041004144338.2,davidmcnab.041004144338.4,davidmcnab.041004144338.5,davidmcnab.041004144338.6,davidmcnab.041004144338.8,davidmcnab.041004144338.9,davidmcnab.041004144338.10,davidmcnab.041004144338.11,davidmcnab.041004144338.12,davidmcnab.041004144338.13,davidmcnab.041004144338.14,davidmcnab.041004144338.15,davidmcnab.041004144338.17,davidmcnab.041004144338.18,davidmcnab.041004144338.19,davidmcnab.041004144338.20,davidmcnab.041004144338.21,davidmcnab.041004144338.22,davidmcnab.041004144338.23,davidmcnab.041004144338.24,davidmcnab.041004144338.26,davidmcnab.041004144338.27,davidmcnab.041004144338.29,davidmcnab.041004144338.30,davidmcnab.041004144338.31,davidmcnab.041004144338.32,davidmcnab.041004144338.33,davidmcnab.041004144338.34,davidmcnab.041004144338.35,davidmcnab.041004144338.36,davidmcnab.041004144338.37,davidmcnab.041004144338.38,davidmcnab.041004144338.39,davidmcnab.041004144338.40,davidmcnab.041004144338.41,davidmcnab.041004144338.42,davidmcnab.041004144338.43,davidmcnab.041004144338.44,davidmcnab.041004144338.45,davidmcnab.041004144338.46,davidmcnab.041004144338.47,davidmcnab.041004144338.49,davidmcnab.041004144338.50,davidmcnab.041004144338.51,davidmcnab.041004144338.52,davidmcnab.041004144338.53,davidmcnab.041004144338.54,davidmcnab.041004144338.55,davidmcnab.041004144338.56,davidmcnab.041004144338.57,davidmcnab.041004144338.58,davidmcnab.041004144338.59,davidmcnab.041004144338.60,davidmcnab.041004144338.62,davidmcnab.041004144338.63,davidmcnab.041004144338.64,davidmcnab.041004144338.65,davidmcnab.041004144338.66,davidmcnab.041004144338.67,davidmcnab.041004144338.68,davidmcnab.041004144338.69,davidmcnab.041004144338.70,davidmcnab.041004144338.71,davidmcnab.041004144338.72,davidmcnab.041004144338.73,davidmcnab.041004144338.74,davidmcnab.041004144338.75,davidmcnab.041004144338.76,davidmcnab.041004144338.77,davidmcnab.041004144338.78,davidmcnab.041004144338.79,davidmcnab.041004144338.80,davidmcnab.041004144338.81,davidmcnab.041004144338.82,davidmcnab.041004144338.83,davidmcnab.041004144338.84,davidmcnab.041004144338.85,davidmcnab.041004144338.86,davidmcnab.041004144338.87,davidmcnab.041004144338.88,davidmcnab.041004144338.89,davidmcnab.041004144338.90,davidmcnab.041004144338.92,davidmcnab.041004144338.93,davidmcnab.041004144338.94,davidmcnab.041004144338.95,davidmcnab.041004144338.96,davidmcnab.041004144338.97,davidmcnab.041004144338.98,davidmcnab.041004144338.99,davidmcnab.041004144338.100,davidmcnab.041004144338.101,davidmcnab.041004144338.102,davidmcnab.041004144338.103,davidmcnab.041004144338.105,davidmcnab.041004144338.106,davidmcnab.041004144338.107,davidmcnab.041004144338.108,davidmcnab.041004144338.109"><vh>@file jython/src/i2psam.py</vh>
+<v t="davidmcnab.041004144338.1"><vh>imports</vh></v>
+<v t="davidmcnab.041004144338.2"><vh>globals</vh></v>
+<v t="davidmcnab.041004144338.3" a="E"><vh>I2CP Interface Classes</vh>
+<v t="davidmcnab.041004144338.4"><vh>class JavaWrapper</vh></v>
+<v t="davidmcnab.041004144338.5" a="E"><vh>class I2PDestination</vh>
+<v t="davidmcnab.041004144338.6"><vh>__init__</vh></v>
+<v t="davidmcnab.041004144338.7" a="E"><vh>Exporting Methods</vh>
+<v t="davidmcnab.041004144338.8"><vh>toBin</vh></v>
+<v t="davidmcnab.041004144338.9"><vh>toBinFile</vh></v>
+<v t="davidmcnab.041004144338.10"><vh>toBinPrivate</vh></v>
+<v t="davidmcnab.041004144338.11"><vh>toBinFilePrivate</vh></v>
+<v t="davidmcnab.041004144338.12"><vh>toBase64</vh></v>
+<v t="davidmcnab.041004144338.13"><vh>toBase64Private</vh></v>
+<v t="davidmcnab.041004144338.14"><vh>toBase64File</vh></v>
+<v t="davidmcnab.041004144338.15"><vh>toBase64FilePrivate</vh></v>
+</v>
+<v t="davidmcnab.041004144338.16" a="E"><vh>Importing Methods</vh>
+<v t="davidmcnab.041004144338.17"><vh>fromBin</vh></v>
+<v t="davidmcnab.041004144338.18"><vh>fromBinFile</vh></v>
+<v t="davidmcnab.041004144338.19"><vh>fromBinPrivate</vh></v>
+<v t="davidmcnab.041004144338.20"><vh>fromBinFilePrivate</vh></v>
+<v t="davidmcnab.041004144338.21"><vh>fromBase64</vh></v>
+<v t="davidmcnab.041004144338.22"><vh>fromBase64File</vh></v>
+<v t="davidmcnab.041004144338.23"><vh>fromBase64Private</vh></v>
+<v t="davidmcnab.041004144338.24"><vh>fromBase64PrivateFile</vh></v>
+</v>
+<v t="davidmcnab.041004144338.25" a="E"><vh>Signature Methods</vh>
+<v t="davidmcnab.041004144338.26"><vh>sign</vh></v>
+<v t="davidmcnab.041004144338.27"><vh>verify</vh></v>
+</v>
+<v t="davidmcnab.041004144338.28" a="E"><vh>Sanity Methods</vh>
+<v t="davidmcnab.041004144338.29"><vh>hasPrivate</vh></v>
+</v>
+</v>
+<v t="davidmcnab.041004144338.30" a="E"><vh>class I2PClient</vh>
+<v t="davidmcnab.041004144338.31" a="E"><vh>__init__</vh></v>
+<v t="davidmcnab.041004144338.32"><vh>createDestination</vh></v>
+<v t="davidmcnab.041004144338.33"><vh>createSession</vh></v>
+</v>
+<v t="davidmcnab.041004144338.34" a="E"><vh>class I2PSession</vh>
+<v t="davidmcnab.041004144338.35"><vh>attributes</vh></v>
+<v t="davidmcnab.041004144338.36"><vh>__init__</vh></v>
+<v t="davidmcnab.041004144338.37"><vh>sendMessage</vh></v>
+<v t="davidmcnab.041004144338.38"><vh>numMessages</vh></v>
+<v t="davidmcnab.041004144338.39"><vh>getMessage</vh></v>
+<v t="davidmcnab.041004144338.40"><vh>setSessionListener</vh></v>
+<v t="davidmcnab.041004144338.41"><vh>destroySession</vh></v>
+<v t="davidmcnab.041004144338.42" a="E"><vh>CALLBACKS</vh>
+<v t="davidmcnab.041004144338.43"><vh>on_message</vh></v>
+<v t="davidmcnab.041004144338.44"><vh>on_abuse</vh></v>
+<v t="davidmcnab.041004144338.45"><vh>on_disconnected</vh></v>
+<v t="davidmcnab.041004144338.46"><vh>on_error</vh></v>
+</v>
+</v>
+<v t="davidmcnab.041004144338.47"><vh>class I2PSessionListener</vh></v>
+</v>
+<v t="davidmcnab.041004144338.48" a="E"><vh>Streaming Interface Classes</vh>
+<v t="davidmcnab.041004144338.49" a="E"><vh>class I2PSocket</vh>
+<v t="davidmcnab.041004144338.50"><vh>attributes</vh></v>
+<v t="davidmcnab.041004144338.51"><vh>__init__</vh></v>
+<v t="davidmcnab.041004144338.52"><vh>bind</vh></v>
+<v t="davidmcnab.041004144338.53"><vh>listen</vh></v>
+<v t="davidmcnab.041004144338.54"><vh>accept</vh></v>
+<v t="davidmcnab.041004144338.55"><vh>connect</vh></v>
+<v t="davidmcnab.041004144338.56"><vh>recv</vh></v>
+<v t="davidmcnab.041004144338.57"><vh>send</vh></v>
+<v t="davidmcnab.041004144338.58"><vh>available</vh></v>
+<v t="davidmcnab.041004144338.59"><vh>close</vh></v>
+<v t="davidmcnab.041004144338.60"><vh>_createSockmgr</vh></v>
+</v>
+</v>
+<v t="davidmcnab.041004144338.61" a="E"><vh>I2P SAM Server</vh>
+<v t="davidmcnab.041004144338.62" a="E"><vh>class I2PSamServer</vh>
+<v t="davidmcnab.041004144338.63"><vh>attributes</vh></v>
+<v t="davidmcnab.041004144338.64"><vh>__init__</vh></v>
+<v t="davidmcnab.041004144338.65"><vh>run</vh></v>
+<v t="davidmcnab.041004144338.66"><vh>finish_request</vh></v>
+<v t="davidmcnab.041004144338.67"><vh>samAllocId</vh></v>
+</v>
+<v t="davidmcnab.041004144338.68" a="E"><vh>class I2PSamClientHandler</vh>
+<v t="davidmcnab.041004144338.69"><vh>handle</vh></v>
+<v t="davidmcnab.041004144338.70"><vh>on_genkeys</vh></v>
+<v t="davidmcnab.041004144338.71"><vh>on_createsession</vh></v>
+<v t="davidmcnab.041004144338.72"><vh>on_destroysession</vh></v>
+<v t="davidmcnab.041004144338.73"><vh>on_send</vh></v>
+<v t="davidmcnab.041004144338.74"><vh>on_receive</vh></v>
+<v t="davidmcnab.041004144338.75"><vh>on_HELLO</vh></v>
+<v t="davidmcnab.041004144338.76"><vh>on_SESSION</vh></v>
+<v t="davidmcnab.041004144338.77"><vh>on_SESSION_CREATE</vh></v>
+<v t="davidmcnab.041004144338.78"><vh>on_STREAM</vh></v>
+<v t="davidmcnab.041004144338.79"><vh>on_DATAGRAM</vh></v>
+<v t="davidmcnab.041004144338.80"><vh>on_RAW</vh></v>
+<v t="davidmcnab.041004144338.81"><vh>on_NAMING</vh></v>
+<v t="davidmcnab.041004144338.82"><vh>on_DEST</vh></v>
+<v t="davidmcnab.041004144338.83"><vh>on_message</vh></v>
+<v t="davidmcnab.041004144338.84"><vh>threadSocketListener</vh></v>
+<v t="davidmcnab.041004144338.85"><vh>samParse</vh></v>
+<v t="davidmcnab.041004144338.86"><vh>samSend</vh></v>
+<v t="davidmcnab.041004144338.87"><vh>samCreateArgsList</vh></v>
+<v t="davidmcnab.041004144338.88"><vh>_sendbytes</vh></v>
+<v t="davidmcnab.041004144338.89"><vh>_recvbytes</vh></v>
+</v>
+</v>
+<v t="davidmcnab.041004144338.90"><vh>Exceptions</vh></v>
+<v t="davidmcnab.041004144338.91" a="E"><vh>Functions</vh>
+<v t="davidmcnab.041004144338.92"><vh>shahash</vh></v>
+<v t="davidmcnab.041004144338.93"><vh>base64enc</vh></v>
+<v t="davidmcnab.041004144338.94"><vh>base64dec</vh></v>
+<v t="davidmcnab.041004144338.95"><vh>str2bytearray</vh></v>
+<v t="davidmcnab.041004144338.96"><vh>bytearray2str</vh></v>
+<v t="davidmcnab.041004144338.97"><vh>byteoutstream2str</vh></v>
+<v t="davidmcnab.041004144338.98"><vh>dict2props</vh></v>
+<v t="davidmcnab.041004144338.99"><vh>takeKey</vh></v>
+<v t="davidmcnab.041004144338.100"><vh>log</vh></v>
+<v t="davidmcnab.041004144338.101"><vh>logException</vh></v>
+<v t="davidmcnab.041004144338.102"><vh>usage</vh></v>
+<v t="davidmcnab.041004144338.103"><vh>main</vh></v>
+</v>
+<v t="davidmcnab.041004144338.104" a="E"><vh>Tests</vh>
+<v t="davidmcnab.041004144338.105" tnodeList="davidmcnab.041004144338.105"><vh>testdests</vh></v>
+<v t="davidmcnab.041004144338.106"><vh>testsigs</vh></v>
+<v t="davidmcnab.041004144338.107"><vh>testsession</vh></v>
+<v t="davidmcnab.041004144338.108"><vh>testsocket</vh></v>
+</v>
+<v t="davidmcnab.041004144338.109"><vh>MAINLINE</vh></v>
+</v>
+<v t="davidmcnab.041004144551" a="EV" tnodeList="davidmcnab.041004144551,davidmcnab.041004144551.1,davidmcnab.041004144551.2,davidmcnab.041004144551.3,davidmcnab.041004144551.4,davidmcnab.041004144551.5,davidmcnab.041004144551.6,davidmcnab.041004144551.7,davidmcnab.041004144551.8,davidmcnab.041004144551.9,davidmcnab.041004144551.10,davidmcnab.041004144551.12,davidmcnab.041004144551.13,davidmcnab.041004144551.14,davidmcnab.041004144551.15,davidmcnab.041004144551.16,davidmcnab.041004144551.17,davidmcnab.041004144551.18,davidmcnab.041004144551.19,davidmcnab.041004144551.20,davidmcnab.041004144551.21,davidmcnab.041004144551.22,davidmcnab.041004144551.23,davidmcnab.041004144551.24,davidmcnab.041004144551.26,davidmcnab.041004144551.27,davidmcnab.041004144551.28,davidmcnab.041004144551.29,davidmcnab.041004144551.30,davidmcnab.041004144551.31,davidmcnab.041004144551.32,davidmcnab.041004144551.33,davidmcnab.041004144551.35,davidmcnab.041004144551.36,davidmcnab.041004144551.37,davidmcnab.041004144551.38,davidmcnab.041004144551.39,davidmcnab.041004144551.40,davidmcnab.041004144551.41,davidmcnab.041004144551.42,davidmcnab.041004144551.43,davidmcnab.041004144551.45,davidmcnab.041004144551.46,davidmcnab.041004144551.47,davidmcnab.041004144551.48,davidmcnab.041004144551.49,davidmcnab.041004144551.50,davidmcnab.041004144551.51,davidmcnab.041004144551.52"><vh>@file python/src/i2psamclient.py</vh>
+<v t="davidmcnab.041004144551.1"><vh>imports</vh></v>
+<v t="davidmcnab.041004144551.2"><vh>globals</vh></v>
+<v t="davidmcnab.041004144551.3"><vh>exceptions</vh></v>
+<v t="davidmcnab.041004144551.4" a="E"><vh>class I2PSamClient</vh>
+<v t="davidmcnab.041004144551.5"><vh>attributes</vh></v>
+<v t="davidmcnab.041004144551.6"><vh>__init__</vh></v>
+<v t="davidmcnab.041004144551.7"><vh>createSession</vh></v>
+<v t="davidmcnab.041004144551.8"><vh>destroySession</vh></v>
+<v t="davidmcnab.041004144551.9"><vh>send</vh></v>
+<v t="davidmcnab.041004144551.10"><vh>receive</vh></v>
+<v t="davidmcnab.041004144551.11" a="E"><vh>SAM methods</vh>
+<v t="davidmcnab.041004144551.12"><vh>samHello</vh></v>
+<v t="davidmcnab.041004144551.13"><vh>samSessionCreate</vh></v>
+<v t="davidmcnab.041004144551.14"><vh>samDestGenerate</vh></v>
+<v t="davidmcnab.041004144551.15"><vh>samRawSend</vh></v>
+<v t="davidmcnab.041004144551.16"><vh>samRawCheck</vh></v>
+<v t="davidmcnab.041004144551.17"><vh>samRawReceive</vh></v>
+<v t="davidmcnab.041004144551.18"><vh>samDatagramSend</vh></v>
+<v t="davidmcnab.041004144551.19"><vh>samDatagramCheck</vh></v>
+<v t="davidmcnab.041004144551.20"><vh>samDatagramReceive</vh></v>
+<v t="davidmcnab.041004144551.21"><vh>samNamingLookup</vh></v>
+<v t="davidmcnab.041004144551.22"><vh>samParse</vh></v>
+<v t="davidmcnab.041004144551.23"><vh>samSend</vh></v>
+<v t="davidmcnab.041004144551.24"><vh>samCreateArgsList</vh></v>
+</v>
+<v t="davidmcnab.041004144551.25" a="E"><vh>Receiver Side</vh>
+<v t="davidmcnab.041004144551.26"><vh>threadRx</vh></v>
+<v t="davidmcnab.041004144551.27"><vh>on_HELLO</vh></v>
+<v t="davidmcnab.041004144551.28"><vh>on_SESSION</vh></v>
+<v t="davidmcnab.041004144551.29"><vh>on_STREAM</vh></v>
+<v t="davidmcnab.041004144551.30"><vh>on_DATAGRAM</vh></v>
+<v t="davidmcnab.041004144551.31"><vh>on_RAW</vh></v>
+<v t="davidmcnab.041004144551.32"><vh>on_NAMING</vh></v>
+<v t="davidmcnab.041004144551.33"><vh>on_DEST</vh></v>
+</v>
+<v t="davidmcnab.041004144551.34" a="E"><vh>Utility Methods</vh>
+<v t="davidmcnab.041004144551.35"><vh>_recvline</vh></v>
+<v t="davidmcnab.041004144551.36"><vh>_recvbytes</vh></v>
+<v t="davidmcnab.041004144551.37"><vh>_sendbytes</vh></v>
+<v t="davidmcnab.041004144551.38"><vh>_sendline</vh></v>
+</v>
+</v>
+<v t="davidmcnab.041004144551.39" a="E"><vh>class I2PRemoteSession</vh>
+<v t="davidmcnab.041004144551.40"><vh>__init__</vh></v>
+<v t="davidmcnab.041004144551.41"><vh>send</vh></v>
+<v t="davidmcnab.041004144551.42"><vh>recv</vh></v>
+<v t="davidmcnab.041004144551.43"><vh>destroy</vh></v>
+</v>
+<v t="davidmcnab.041004144551.44" a="E"><vh>Functions</vh>
+<v t="davidmcnab.041004144551.45"><vh>log</vh></v>
+<v t="davidmcnab.041004144551.46"><vh>logException</vh></v>
+<v t="davidmcnab.041004144551.47"><vh>demoNAMING</vh></v>
+<v t="davidmcnab.041004144551.48"><vh>demoRAW</vh></v>
+<v t="davidmcnab.041004144551.49"><vh>demoDATAGRAM</vh></v>
+<v t="davidmcnab.041004144551.50"><vh>demoSTREAM</vh></v>
+<v t="davidmcnab.041004144551.51"><vh>demo</vh></v>
+</v>
+<v t="davidmcnab.041004144551.52"><vh>MAINLINE</vh></v>
+</v>
+</v>
+</vnodes>
+<tnodes>
+<t tx="davidmcnab.041004143447"></t>
+<t tx="davidmcnab.041004144338">@first #!/usr/bin/env jython
+r"""
+Implements I2P SAM Server. (refer U{http://drupal.i2p.net/node/view/144})
+
+Also contains useful classes for jython programs,
+which wrap the I2P java classes into more python-compatible
+paradigms.
+
+If you run this module (or the i2psam.jar file created from it)
+without arguments, it'll run an I2P SAM server bridge, listening
+on port 7656.
+
+The file i2psamclient.py contains python client classes and a
+demo program.
+
+Latest vers of this file is available from U{http://www.freenet.org.nz/i2p/i2psam.py}
+Latest epydoc-generated doco at U{http://www.freenet.org.nz/i2p/i2pjyDoc}
+
+The i2psam.jar file is built from this module with the following
+command (requires jython and java 1.4.x+ to be installed)::
+
+  CLASSPATH=/path/to/i2p.jar:/path/to/mstreaming.jar \
+          jythonc -jar i2psam.jar --all -A net.invisiblenet i2psam.py
+
+"""
+
+@others
+
+
+</t>
+<t tx="davidmcnab.041004144338.1"># python imports
+import sys, os, time, Queue, thread, threading, StringIO, traceback, getopt
+from SocketServer import ThreadingTCPServer, StreamRequestHandler
+
+# java imports
+import java
+
+# i2p-specific imports
+import net.i2p
+import net.i2p.client # to shut up epydoc
+
+# shut up java with a few more imports
+import net.i2p.client.streaming
+import net.i2p.crypto
+import net.i2p.data
+import net.i2p.client.I2PClient
+import net.i2p.client.I2PClientFactory
+import net.i2p.client.naming
+#import net.i2p.client.I2PSessionListener
+
+# handy shorthand refs
+i2p = net.i2p
+jI2PClient = i2p.client.I2PClient
+
+# import my own helper hack module
+#import I2PHelper
+
+</t>
+<t tx="davidmcnab.041004144338.2">clientFactory = i2p.client.I2PClientFactory
+
+#i2phelper = I2PHelper()
+
+PROP_RELIABILITY_BEST_EFFORT = i2p.client.I2PClient.PROP_RELIABILITY_BEST_EFFORT
+PROP_RELIABILITY_GUARANTEED = i2p.client.I2PClient.PROP_RELIABILITY_GUARANTEED
+
+version = "0.1.0"
+
+# host/port that our socketserver listens on
+i2psamhost = "127.0.0.1"
+i2psamport = 7656
+
+# host/port that I2P's I2CP listens on
+i2cpHost = "127.0.0.1"
+i2cpPort = 7654
+
+#print "i2cpPort=%s" % repr(i2cpPort)
+
+# ------------------------------------------
+# logging settings
+
+# 1=v.quiet, 2=normal, 3=verbose, 4=debug, 5=painful
+verbosity = 5
+
+# change to a filename to log there instead
+logfile = sys.stdout
+
+# when set to 1, and when logfile != sys.stdout, log msgs are written
+# both to logfile and console stdout
+log2console = 1
+
+# don't touch this!
+loglock = threading.Lock()
+
+
+</t>
+<t tx="davidmcnab.041004144338.3"></t>
+<t tx="davidmcnab.041004144338.4">class JavaWrapper:
+    """
+    Wraps a java object as attribute '_item', and forwards
+    __getattr__ to it.
+    
+    All the classes here derive from this
+    """
+    def __init__(self, item):
+        self._item = item
+    
+    def __getattr__(self, attr):
+        return getattr(self._item, attr)
+    
+
+</t>
+<t tx="davidmcnab.041004144338.5">class I2PDestination(JavaWrapper):
+    """
+    Wraps java I2P destination objects, with a big difference - these
+    objects store the private parts.
+    """
+    @others
+
+</t>
+<t tx="davidmcnab.041004144338.6">def __init__(self, **kw):
+    """
+    Versatile constructor
+    
+    Keywords (choose only one option):
+        - (none) - create a whole new dest
+        - dest, private - wrap an existing I2P java dest with private stream
+          (private is a byte array)
+        - bin - reconstitute a public-only dest from a binary string
+        - binfile - reconstitute public-only from a binary file
+        - binprivate - reconsistitute private dest from binary string
+        - binfileprivate - reconsistitute private dest from binary file pathname
+        - base64 - reconstitute public-only from base64 string
+        - base64file - reconstitute public-only from file containing base64
+        - base64private - reconstitute private from string containing base64
+        - base64fileprivate - reconstitute private from file containing base64
+
+    also:
+        - client - a java net.i2p.client.I2PClient object
+          (avoids need for temporary client object when creating new dests)
+    """
+    dest = i2p.data.Destination()
+    JavaWrapper.__init__(self, dest)
+    self._private = None
+
+    if kw.has_key('dest'):
+        self._item = kw['dest']
+        if kw.has_key('private'):
+            self._private = kw['private']
+
+    elif kw.has_key('bin'):
+        self.fromBin(kw['bin'])
+
+    elif kw.has_key('binfile'):
+        self.fromBinFilePrivate(kw['binfile'])
+
+    elif kw.has_key('binprivate'):
+        self.fromBinPrivate(kw['binprivate'])
+
+    elif kw.has_key('binfileprivate'):
+        self.fromBinFilePrivate(kw['binfileprivate'])
+
+    elif kw.has_key('base64'):
+        self.fromBase64(kw['base64'])
+
+    elif kw.has_key('base64file'):
+        self.fromBase64File(kw['base64file'])
+    
+    elif kw.has_key('base64private'):
+        self.fromBase64Private(kw['base64private'])
+
+    elif kw.has_key('base64fileprivate'):
+        self.fromBase64FilePrivate(kw['base64fileprivate'])
+
+    else:
+        # create a whole new one, with a temporary client object (if needed)
+        if kw.has_key('client'):
+            client = kw['client']
+        else:
+            client = clientFactory.createClient()
+        bytestream = java.io.ByteArrayOutputStream()
+        self._item = client.createDestination(bytestream)
+        self._private = bytestream.toByteArray()
+
+</t>
+<t tx="davidmcnab.041004144338.7"></t>
+<t tx="davidmcnab.041004144338.8">def toBin(self):
+    """
+    Returns a binary string of dest
+    """
+    return bytearray2str(self.toByteArray())
+
+</t>
+<t tx="davidmcnab.041004144338.9">def toBinFile(self, path):
+    """
+    Writes out public binary to a file
+    """
+    f = open(path, "wb")
+    f.write(self.toBin())
+    f.flush()
+    f.close()
+
+</t>
+<t tx="davidmcnab.041004144338.10">def toBinPrivate(self):
+    """
+    Returns the private key string as binary
+    """
+    if self._private == None:
+        raise NoPrivateKey
+    return bytearray2str(self._private)
+
+</t>
+<t tx="davidmcnab.041004144338.11">def toBinFilePrivate(self, path):
+    """
+    Writes out a binary file with the dest info
+    """
+    f = open(path, "wb")
+    f.write(self.toBinPrivate())
+    f.flush()
+    f.close()
+
+</t>
+<t tx="davidmcnab.041004144338.12">def toBase64(self):
+    """
+    Returns base64 string of public part
+    """
+    return self._item.toBase64()
+
+</t>
+<t tx="davidmcnab.041004144338.13">def toBase64Private(self):
+    """
+    Exports dest as base64, including private stuff
+    """
+    if self._private == None:
+        raise NoPrivateKey
+    return i2p.data.Base64.encode(self._private)
+
+</t>
+<t tx="davidmcnab.041004144338.14">def toBase64File(self, path):
+    """
+    Exports dest to file as base64
+    """
+    f = open(path, "wb")
+    f.write(self.toBase64())
+    f.flush()
+    f.close()
+
+</t>
+<t tx="davidmcnab.041004144338.15">def toBase64FilePrivate(self, path):
+    """
+    Writes out a base64 file with the private dest info
+    """
+    f = open(path, "wb")
+    f.write(self.toBase64Private())
+    f.flush()
+    f.close()
+
+</t>
+<t tx="davidmcnab.041004144338.16"></t>
+<t tx="davidmcnab.041004144338.17">def fromBin(self, bin):
+    """
+    Loads this dest from a binary string
+    """
+    self._item.fromByteArray(str2bytearray(bin))
+    self._private = None
+
+</t>
+<t tx="davidmcnab.041004144338.18">def fromBinFile(self, path):
+    """
+    Loads public part from file containing binary
+    """
+    f = open(path, "rb")
+    self.fromBin(f.read())
+    f.close()
+
+</t>
+<t tx="davidmcnab.041004144338.19">def fromBinPrivate(self, s):
+    """
+    Loads this dest object from a base64 private key string
+    """
+    bytes = str2bytearray(s)
+    self._private = bytes
+    stream = java.io.ByteArrayInputStream(bytes)
+    self._item.readBytes(stream)
+
+</t>
+<t tx="davidmcnab.041004144338.20">def fromBinFilePrivate(self, path):
+    """
+    Loads this dest object, given the pathname of a file containing
+    a binary destkey
+    """
+    self.fromBinPrivate(open(path, "rb").read())
+
+</t>
+<t tx="davidmcnab.041004144338.21">def fromBase64(self, b64):
+    """
+    Loads this dest from a base64 string
+    """
+    self._item.fromBase64(b64)
+    self._private = None
+
+</t>
+<t tx="davidmcnab.041004144338.22">def fromBase64File(self, path):
+    """
+    Loads public part from file containing base64
+    """
+    f = open(path, "rb")
+    self.fromBase64(f.read())
+    f.close()
+
+</t>
+<t tx="davidmcnab.041004144338.23">def fromBase64Private(self, s):
+    """
+    Loads this dest object from a base64 private key string
+    """
+    bytes = i2p.data.Base64.decode(s)
+    self._private = bytes
+    stream = java.io.ByteArrayInputStream(bytes)
+    self._item.readBytes(stream)
+
+</t>
+<t tx="davidmcnab.041004144338.24">def fromBase64FilePrivate(self, path):
+    """
+    Loads this dest from a base64 file containing private key
+    """
+    self.fromBase64Private(open(path, "rb").read())
+
+</t>
+<t tx="davidmcnab.041004144338.25"></t>
+<t tx="davidmcnab.041004144338.26">def sign(self, s):
+    """
+    Signs a string using this dest's priv key
+    """
+    # get byte stream
+    bytes = str2bytearray(s)
+
+    # stream up our private bytes
+    stream = java.io.ByteArrayInputStream(self._private)
+
+    # temporary dest object
+    d = i2p.data.Destination()
+
+    # suck the public part off the stream
+    d.readBytes(stream)
+
+    # temporary private key object
+    privkey = i2p.data.PrivateKey()
+    privkey.readBytes(stream)
+    
+    # now we should just have the signing key portion left in the stream
+    signingkey = i2p.data.SigningPrivateKey()
+    signingkey.readBytes(stream)
+    
+    # create DSA engine
+    dsa = i2p.crypto.DSAEngine()
+    
+    sig = dsa.sign(bytes, signingkey)
+
+    rawsig = bytearray2str(sig.getData())
+
+    return rawsig
+
+</t>
+<t tx="davidmcnab.041004144338.27">def verify(self, s, sig):
+    """
+    Verifies a string against this dest, to test if it was actually
+    signed by whoever has the dest privkey
+    """
+    # get byte stream from data
+    databytes = str2bytearray(s)
+
+    # get signature stream from sig
+    sigstream = java.io.ByteArrayInputStream(str2bytearray(sig))
+
+    # make a signature object
+    signature = i2p.data.Signature()
+    signature.readBytes(sigstream)
+
+    # get signature verify key
+    pubkey = self.getSigningPublicKey()
+
+	#log(4, "databytes=%s, pubkey=%s" % (repr(databytes), repr(pubkey)))
+    
+    # now get a verification
+    dsa = i2p.crypto.DSAEngine()
+    result = dsa.verifySignature(signature, databytes, pubkey)
+
+    return result
+
+
+
+</t>
+<t tx="davidmcnab.041004144338.28"></t>
+<t tx="davidmcnab.041004144338.29">def hasPrivate(self):
+    """
+    Returns True if this dest has private parts, False if not
+    """
+
+    if self._private:
+        return 1
+    else:
+        return 0
+</t>
+<t tx="davidmcnab.041004144338.30">class I2PClient(JavaWrapper):
+    """
+    jython-comfortable wrapper for java I2P client class
+    """
+    @others
+
+</t>
+<t tx="davidmcnab.041004144338.31">def __init__(self, **kw):
+    """
+    I2PClient constructor
+    
+    No args or keywords as yet
+    """
+    client = clientFactory.createClient()
+    JavaWrapper.__init__(self, client)
+
+</t>
+<t tx="davidmcnab.041004144338.32">def createDestination(self, **kw):
+    """
+    Creates a destination, either a new one, or from a bin or base64 file
+    
+    Keywords:
+        - see L{I2PDestination} constructor
+    """
+    return I2PDestination(**kw)
+
+</t>
+<t tx="davidmcnab.041004144338.33">def createSession(self, dest, sessionClass=None, **kw):
+    """
+    Create a session
+
+    Arguments:
+        - dest - an L{I2PDestination} object which MUST contain a private portion
+        - sessionClass - if given, this should be a subclass
+          of I2PSession. This allows you to implement your own handlers.
+
+    Keywords:
+        - session options (refer javadocs)
+    """
+    if sessionClass is None:
+        sessionClass = I2PSession
+
+    if not dest.hasPrivate():
+        raise NoPrivateKey("Dest object has no private key")
+
+    #print kw
+    #session = self._item.createSession(destStream, dict2props(kw))
+    session = sessionClass(client=self, dest=dest, **kw)
+    return session
+    #return sessionClass(session=session)
+
+</t>
+<t tx="davidmcnab.041004144338.34">class I2PSession(JavaWrapper):
+    """
+    Wraps an I2P client session
+
+    You can subclass this, overriding the on_* handler callbacks,
+    and pass it as an argument to I2PClient.createSession
+
+    In the default 'on_message' callback, message retrieval is
+    synchronous - inbound messages get written to an internal queue,
+    which you can checked with numMessages() and retrieved from via
+    getMessage(). You may override on_message() if you
+    want to handle incoming messages asynchronously yourself.
+
+    Note - as far as I can tell, this class should be thread-safe.
+    """
+    @others
+</t>
+<t tx="davidmcnab.041004144338.35">host = i2cpHost
+port = i2cpPort
+</t>
+<t tx="davidmcnab.041004144338.36">def __init__(self, **kw):
+    """
+    I2PSession constructor
+
+    Keywords:
+        - either:
+            - session - a java i2p session object
+        - or:
+            - client - an L{I2PClient} object
+            - dest - an L{I2PDestination} object
+    Also:
+        - listener - an L{I2PSessionListener} object.
+
+    Router-level options:
+        - reliability - one of 'guaranteed' and 'besteffort' (default 'besteffort')
+        - host - host on which router is running
+        - port - port on which router is listening
+    """
+    #
+    # grab options destined for java class
+    #
+    options = {}
+
+    reliability = takeKey(kw, 'reliability', 'besteffort')
+    if reliability == 'guaranteed':
+        reliability = jI2PClient.PROP_RELIABILITY_GUARANTEED
+    else:
+        reliability = jI2PClient.PROP_RELIABILITY_BEST_EFFORT
+    options[jI2PClient.PROP_RELIABILITY] = reliability
+
+    host = takeKey(kw, 'host', self.host)
+    options[jI2PClient.PROP_TCP_HOST] = host
+
+    port = takeKey(kw, 'port', self.port)
+    options[jI2PClient.PROP_TCP_PORT] = str(port)
+
+    if kw.has_key('reliability'):
+        reliability = kw['reliability']
+
+    if kw.has_key('listener'):
+        listener = kw['listener']
+        del kw['listener']
+    else:
+        listener = I2PSessionListener()
+
+    #print options
+
+    #
+    # other keywords handled locally
+    #
+    if kw.has_key('session'):
+        session = kw['session']
+        del kw['session']
+        JavaWrapper.__init__(self, session)
+    elif kw.has_key('client') and kw.has_key('dest'):
+        client = kw['client']
+        dest = kw['dest']
+        del kw['client']
+        del kw['dest']
+        destStream = java.io.ByteArrayInputStream(dest._private)
+        session = self._item = client._item.createSession(destStream, dict2props(options))
+        #client.createSession(dest, dict2props(options))
+    else:
+        raise Exception("implementation incomplete")
+
+    # set up a listener
+    self.setSessionListener(listener)
+
+    # set up a queue for inbound msgs
+    self.qInbound = Queue.Queue()
+    self.lockInbound = threading.Lock()
+    self.nInboundMessages = 0
+
+    self.lockOutbound = threading.Lock()
+
+
+
+</t>
+<t tx="davidmcnab.041004144338.37">def sendMessage(self, dest, payload):
+    """
+    Sends a message to another dest
+    
+    Arguments:
+        - dest - an L{I2PDestination} object
+        - payload - a string to send
+    """
+    dest = dest._item
+    payload = str2bytearray(payload)
+    self.lockOutbound.acquire()
+    try:
+        res = self._item.sendMessage(dest, payload)
+    except:
+        self.lockOutbound.release()
+        raise
+    self.lockOutbound.release()
+    return res
+</t>
+<t tx="davidmcnab.041004144338.38">def numMessages(self):
+    """
+    Returns the number of unretrieved inbound messages
+    """
+    self.lockInbound.acquire()
+    n = self.nInboundMessages
+    self.lockInbound.release()
+    return n
+</t>
+<t tx="davidmcnab.041004144338.39">def getMessage(self, blocking=1):
+    """
+    Returns the next available inbound message.
+    
+    If blocking is set to 1 (default), blocks
+    till another message comes in.
+    
+    If blocking is set to 0, returns None if there
+    are no available messages.
+    """
+    if blocking:
+        msg = self.qInbound.get()
+        #print "getMessage: acquiring lock"
+        self.lockInbound.acquire()
+        #print "getMessage: got lock"
+        self.nInboundMessages -= 1
+    else:
+        #print "getMessage: acquiring lock"
+        self.lockInbound.acquire()
+        #print "getMessage: got lock"
+        if self.nInboundMessages &gt; 0:
+            msg = self.qInbound.get()
+            self.nInboundMessages -= 1
+        else:
+            msg = None
+    self.lockInbound.release()
+    #print "getMessage: released lock"
+    return msg
+
+</t>
+<t tx="davidmcnab.041004144338.40">def setSessionListener(self, listener):
+    """
+    Designates an L{I2PSessionListener} object to listen to this session
+    """
+    self.listener = listener
+    listener.addSession(self)
+    self._item.setSessionListener(listener)
+
+
+</t>
+<t tx="davidmcnab.041004144338.41">def destroySession(self):
+    """
+    Destroys an existing session
+
+    Note that due to a jython quirk, calls to destroySession might
+    trigger a TypeError relating to arg mismatch - we ignore such
+    errors here because by the time the exception happens, the
+    session has already been successfully closed
+    """
+    try:
+        self._item.destroySession()
+    except TypeError:
+        pass
+
+</t>
+<t tx="davidmcnab.041004144338.42">#
+# handler methods which you should override
+#
+
+@others
+</t>
+<t tx="davidmcnab.041004144338.43">def on_message(self, msg):
+    """
+    Callback for when a message arrives.
+
+    Appends the message to the inbound queue, which you can check
+    with the numMessages() method, and read with getMessage()
+
+    You should override this if you want to handle inbound messages
+    asynchronously.
+    
+    Arguments:
+        - msg - a string that was sent by peer
+    """
+    #print "on_message: msg=%s" % msg
+    self.lockInbound.acquire()
+    #print "on_message: got lock"
+    self.qInbound.put(msg)
+    self.nInboundMessages += 1
+    self.lockInbound.release()
+    #print "on_message: released lock"
+
+</t>
+<t tx="davidmcnab.041004144338.44">def on_abuse(self, severity):
+    """
+    Callback indicating abuse is happening
+    
+    Arguments:
+        - severity - an int of abuse level, 1-100
+    """
+    print "on_abuse: severity=%s" % severity
+
+</t>
+<t tx="davidmcnab.041004144338.45">def on_disconnected(self):
+    """
+    Callback indicating remote peer disconnected
+    """
+    print "on_disconnected"
+
+</t>
+<t tx="davidmcnab.041004144338.46">def on_error(self, message, error):
+    """
+    Callback indicating an error occurred
+    """
+    print "on_error: message=%s error=%s" % (message, error)
+
+</t>
+<t tx="davidmcnab.041004144338.47">class I2PSessionListener(i2p.client.I2PSessionListener):
+    """
+    Wraps a java i2p.client.I2PSessionListener object
+    """
+    def __init__(self, *sessions):
+        self.sessions = list(sessions)
+
+    def addSession(self, session):
+        """
+        Adds an L{I2PSession} object to the list of sessions to listen on
+        
+        Note - you must also invoke the session's setSessionListener() method
+        (see I2PSession.setSessionListener)
+        """
+        if session not in self.sessions:
+            self.sessions.append(session)
+    
+    def delSession(self, session):
+        """
+        Stop listening to a given session
+        """
+        if session in self.sessions:
+            del self.sessions.index[session]
+
+    def messageAvailable(self, session, msgId, size):
+        """
+        Callback from java::
+            public void messageAvailable(
+                I2PSession session,
+                int msgId,
+                long size)
+        """
+        #print "listener - messageAvailable"
+
+        # try to find session in our sessions table
+        sessions = filter(lambda s, session=session: s._item == session, self.sessions)
+        if sessions:
+            #print "compare to self.session-&gt;%s" % (session == self.session._item)
+
+            # found a matching session - retrieve it
+            session = sessions[0]
+
+            # retrieve message and pass to callback
+            msg = session.receiveMessage(msgId)
+            msgStr = bytearray2str(msg)
+            session.on_message(msgStr)
+        else:
+            print "messageAvailable: unknown session=%s msgId=%s size=%s" % (session, msgId, size)
+
+    def reportAbuse(self, session, severity):
+        """
+        Callback from java::
+            public void reportAbuse(
+                I2PSession session,
+                int severity)
+        """
+        if self.session:
+            self.session.on_abuse(severity)
+        else:
+            print "reportAbuse: unknown session=%s severity=%s" % (session, severity)
+    
+    def disconnected(self, session):
+        """
+        Callback from java::
+            public void disconnected(I2PSession session)
+        """
+        if self.session:
+            self.session.on_disconnected()
+        else:
+            print "disconnected: unknown session=%s" % session
+
+    def errorOccurred(session, message, error):
+        """
+        Callback from java::
+            public void errorOccurred(
+                I2PSession session,
+                java.lang.String message,
+                java.lang.Throwable error)
+        """
+        if self.session:
+            self.session.on_error(message, error)
+        else:
+            print "errorOccurred: message=%s error=%s" % (message, error)
+
+</t>
+<t tx="davidmcnab.041004144338.48"></t>
+<t tx="davidmcnab.041004144338.49">class I2PSocket:
+    """
+    Wraps I2P streaming API into a form resembling python sockets
+    """
+    @others
+</t>
+<t tx="davidmcnab.041004144338.50">host = i2cpHost
+port = i2cpPort
+
+</t>
+<t tx="davidmcnab.041004144338.51">def __init__(self, dest=None, **kw):
+    """
+    Create an I2P streaming socket
+
+    Arguments:
+        - dest - a private destination to associate with this socket
+
+    Keywords:
+        - host - hostname on which i2cp is listening (default self.host)
+        - port - port on which i2cp listens (default self.port)
+
+    Internally used keywords (used for wrapping an accept()ed connection):
+        - dest
+        - remdest
+        - sock
+        - instream
+        - outstream
+    """
+    # set up null attribs
+    self.sockmgr = None
+    self.instream = None
+    self.outstream = None
+    self.sock = None
+    self._connected = 0
+    self._blocking = 1
+
+    # save dest (or lack thereof)
+    self.dest = dest
+
+    if kw.has_key('sock') \
+            and kw.has_key('dest') \
+            and kw.has_key('remdest') \
+            and kw.has_key('instream') \
+            and kw.has_key('outstream'):
+        # wrapping an accept()'ed connection
+        self.sock = kw['sock']
+        self.dest = kw['dest']
+        self.remdest = kw['remdest']
+        self.instream = kw['instream']
+        self.outstream = kw['outstream']
+    else:
+        # process keywords
+        self.host = kw.get('host', self.host)
+        self.port = int(kw.get('port', self.port))
+
+        # we need a factory, don't we?
+        self.sockmgrFact = i2p.client.streaming.I2PSocketManagerFactory()
+</t>
+<t tx="davidmcnab.041004144338.52">def bind(self, dest=None):
+    """
+    'binds' the socket to a dest
+
+    dest is an I2PDestination object, which you may specify in the constructor
+    instead of here. However, we give you the option of specifying here for
+    some semantic compatibility with python sockets.
+    """
+    if dest is not None:
+        self.dest = dest
+    elif not self.dest:
+        # create new dest, client should interrogate it at some time
+        self.dest = Destination()
+</t>
+<t tx="davidmcnab.041004144338.53">def listen(self, *args, **kw):
+    """
+    Sets up the object to receive connections
+    """
+    # sanity checks
+    if self.sockmgr:
+        raise I2PSocketError(".sockmgr already present - have you already called listen?")
+    if not self.dest:
+        raise I2PSocketError("socket is not bound to a destination")
+    
+    # create the socket manager
+    self._createSockmgr()
+    </t>
+<t tx="davidmcnab.041004144338.54">def accept(self):
+    """
+    Waits for incoming connections, and returns a new I2PSocket object
+    with the connection
+    """
+    # sanity check
+    if not self.sockmgr:
+        raise I2PSocketError(".listen() has not been called on this socket")
+
+    # accept a conn and get its streams
+    sock = self.sockmgr.getServerSocket().accept()
+    instream = sock.getInputStream()
+    outstream = sock.getOutputStream()
+    remdest = I2PDestination(dest=sock.getPeerDestination())
+
+    # wrap it and return it
+    sockobj = I2PSocket(dest=self.dest,
+                        remdest=remdest,
+                        sock=sock,
+                        instream=instream,
+                        outstream=outstream)
+    self._connected = 1
+    return sockobj
+
+</t>
+<t tx="davidmcnab.041004144338.55">def connect(self, remdest):
+    """
+    Connects to a remote destination
+    """
+    # sanity check
+    if self.sockmgr:
+        raise I2PSocketError(".sockmgr already present - have you already called listen/connect?")
+
+    # create whole new dest if none was provided to constructor
+    if self.dest is None:
+        self.dest = I2PDestination()
+
+    # create the socket manager
+    self._createSockmgr()
+
+    # do the connect
+    #print "remdest._item = %s" % repr(remdest._item)
+
+    opts = net.i2p.client.streaming.I2PSocketOptions()
+    try:
+        self.sock = self.sockmgr.connect(remdest._item, opts)
+        self.remdest = remdest
+    except:
+        logException(2, "apparent exception, continuing...")
+    self.instream = self.sock.getInputStream()
+    self.outstream = self.sock.getOutputStream()
+    self._connected = 1
+</t>
+<t tx="davidmcnab.041004144338.56">def recv(self, nbytes):
+    """
+    Reads nbytes of data from socket
+    """
+    # sanity check
+    if not self.instream:
+        raise I2PSocketError("Socket is not connected")
+    
+    # for want of better methods, read bytewise
+    chars = []
+    while nbytes &gt; 0:
+        byte = self.instream.read()
+        if byte &lt; 0:
+            break # got all we're gonna get
+        char = chr(byte)
+        chars.append(char)
+        #print "read: got a byte %s (%s)" % (byte, repr(char))
+        nbytes -= 1
+        
+    # got it all
+    buf = "".join(chars)
+    #print "recv: buf=%s" % repr(buf)
+    return buf
+
+
+</t>
+<t tx="davidmcnab.041004144338.57">def send(self, buf):
+    """
+    Sends buf thru socket
+    """
+    # sanity check
+    if not self.outstream:
+        raise I2PSocketError("Socket is not connected")
+
+    # and write it out
+    #print "send: writing '%s' to outstream..." % repr(buf)
+    outstream = self.outstream
+    for c in buf:
+        outstream.write(ord(c))
+
+    # flush just in case
+    #print "send: flushing..."
+    self.outstream.flush()
+
+    #print "send: done"
+</t>
+<t tx="davidmcnab.041004144338.58">def available(self):
+    """
+    Returns the number of bytes available for recv()
+    """
+    return self.sock.available()
+
+</t>
+<t tx="davidmcnab.041004144338.59">def close(self):
+    """
+    Closes the socket
+    """
+    # sanity check
+    #if not self._connected:
+    #    raise I2PSocketError("Socket is not connected")
+
+    # shut up everything
+    try:
+        self.instream.close()
+    except:
+        pass
+    try:
+        self.outstream.close()
+    except:
+        pass
+    try:
+        self.sock.close()
+    except:
+        pass
+</t>
+<t tx="davidmcnab.041004144338.60">def _createSockmgr(self):
+
+    #options = {jI2PClient.PROP_TCP_HOST: self.host,
+    #           jI2PClient.PROP_TCP_PORT: self.port}
+    options = {}
+    props = dict2props(options)
+
+    # get a java stream thing from dest
+    stream = java.io.ByteArrayInputStream(self.dest._private)
+    
+    # create socket manager thing
+    self.sockmgr = self.sockmgrFact.createManager(stream, self.host, self.port, props)
+</t>
+<t tx="davidmcnab.041004144338.61"></t>
+<t tx="davidmcnab.041004144338.62">class I2PSamServer(ThreadingTCPServer):
+    """
+    A server which makes I2CP available via a socket
+    """
+	@others
+</t>
+<t tx="davidmcnab.041004144338.63">host = i2psamhost
+port = i2psamport
+
+i2cphost = i2cpHost
+i2cpport = i2cpPort
+
+version = version
+
+
+</t>
+<t tx="davidmcnab.041004144338.64">def __init__(self, i2pclient=None, **kw):
+    """
+    Create the client listener object
+    
+    Arguments:
+        - i2pclient - an I2PClient object - optional - if not
+          given, one will be created
+    
+    Keywords:
+        - host - host to listen on for client conns (default self.host ('127.0.0.1')
+        - port - port to listen on for client conns (default self.port (7656)
+        - i2cphost - host to talk to i2cp on (default self.i2cphost ('127.0.0.1'))
+        - i2cpport - port to talk to i2cp on (default self.i2cphost ('127.0.0.1'))
+    """
+
+    # create an I2PClient object if none given
+    if i2pclient is None:
+        i2pclient = I2PClient()
+    self.i2pclient = i2pclient
+
+    # get optional host/port for client and i2cp
+    self.host = kw.get('host', self.host)
+    self.port = int(kw.get('port', self.port))
+    self.i2cphost = kw.get('i2cphost', self.i2cphost)
+    self.i2cpport = int(kw.get('i2cpport', self.i2cpport))
+
+    # create record of current sessions, and a lock for it
+    self.sessions = {}
+    self.sessionsLock = threading.Lock()
+    self.streams = {}
+    self.streamsLock = threading.Lock()
+    self.samNextId = 1
+    self.samNextIdLock = threading.Lock()
+
+    # and create the server
+    try:
+        ThreadingTCPServer.__init__(
+            self, 
+            (self.host, self.port),
+            I2PSamClientHandler)
+    except:
+        log(4, "crashed with host=%s, port=%s" % (self.host, self.port))
+        raise
+
+</t>
+<t tx="davidmcnab.041004144338.65">def run(self):
+    """
+    Run the SAM server.
+
+    when connections come in, they are automatically
+    accepted, and an L{I2PClientHandler} object created,
+    and its L{handle} method invoked.
+    """
+    log(4, "Listening for client requests on %s:%s" % (self.host, self.port))
+    self.serve_forever()
+
+
+</t>
+<t tx="davidmcnab.041004144338.66">def finish_request(self, request, client_address):
+    """Finish one request by instantiating RequestHandlerClass."""
+    try:
+        self.RequestHandlerClass(request, client_address, self)
+    except:
+        pass
+    log(3, "Client session terminated")
+</t>
+<t tx="davidmcnab.041004144338.67">def samAllocId(self):
+    """
+    Allocates a new unique id as required by SAM protocol
+    """
+    self.samNextIdLock.acquire()
+    id = self.samNextId
+    self.samNextId += 1
+    self.samNextIdLock.release()
+    return id
+</t>
+<t tx="davidmcnab.041004144338.68">class I2PSamClientHandler(StreamRequestHandler):
+    r"""
+    Manages a single socket connection from a client.
+    
+    When a client connects to the SAM server, the I2PSamServer
+    object creates an instance of this class, and invokes its
+    handle method. See L{handle}.
+
+    Note that if a client terminates its connection to the server, the server
+    will destroy all current connections initiated by that client
+    
+    Size values are decimal
+    Connection is persistent
+    """
+	@others</t>
+<t tx="davidmcnab.041004144338.69">def handle(self):
+    """
+    Reads command/data messages from SAM Client, executes these,
+    and sends back responses.
+    
+    Plants callback hooks into I2PSession objects, so that when
+    data arrives via I2P, it can be immediately sent to the client.
+    """
+    self.localsessions = {}
+    self.globalsessions = self.server.sessions
+
+    self.localstreams = {} # keyed by sam stream id
+    self.globalstreams = self.server.streams
+
+    self.samSessionIsOpen = 0
+    self.samSessionStyle = ''
+
+    # need a local sending lock
+    self.sendLock = threading.Lock()
+
+    log(5, "Got req from %s" % repr(self.client_address))
+
+    try:
+        self.namingService = i2p.client.naming.HostsTxtNamingService()
+    except:
+        logException(2, "Failed to create naming service object")
+
+    try:
+        while 1:
+            # get req
+            req = self.rfile.readline().strip()
+            flds = [s.strip() for s in req.split(" ")]
+            cmd = flds[0]
+            if cmd in ['HELLO', 'SESSION', 'STREAM', 'DATAGRAM', 'RAW', 'NAMING', 'DEST']:
+                topic, subtopic, args = self.samParse(flds)
+                method = getattr(self, "on_"+cmd, None)
+                method(topic, subtopic, args)
+            else:
+                method = getattr(self, "on_"+cmd, None)
+                if method:
+                    method(flds)
+                else:
+                    # bad shit
+                    self.wfile.write("error unknown command '%s'\n" % cmd)
+
+    except IOError:
+        log(3, "Client connection terminated")
+    except ValueError:
+        pass
+    except:
+        logException(4, "Client req handler crashed")
+        self.wfile.write("error\n")
+
+    # clean up sessions
+    for dest in self.localsessions.keys():
+        if dest in self.globalsessions.keys():
+            log(4, "forgetting global dest %s" % dest[:30])
+            del self.globalsessions[dest]
+
+    self.finish()
+    #thread.exit()
+
+</t>
+<t tx="davidmcnab.041004144338.70">def on_genkeys(self, flds):
+
+    log(4, "entered")
+
+    server = self.server
+    client = server.i2pclient
+    globalsessions = server.sessions
+    sessionsLock = server.sessionsLock
+
+    read = self.rfile.read
+    readline = self.rfile.readline
+    write = self.wfile.write
+    flush = self.wfile.flush
+
+    # genkeys
+    try:
+        dest = I2PDestination()
+        priv = dest.toBase64Private()
+        pub = dest.toBase64()
+        write("ok %s %s\n" % (pub, priv))
+    except:
+        write("error exception\n")
+</t>
+<t tx="davidmcnab.041004144338.71">def on_createsession(self, flds):
+
+    log(4, "entered")
+
+    server = self.server
+    client = server.i2pclient
+    globalsessions = server.sessions
+    sessionsLock = server.sessionsLock
+
+    read = self.rfile.read
+    readline = self.rfile.readline
+    write = self.wfile.write
+    flush = self.wfile.flush
+
+    sessionsLock.acquire()
+
+    try:
+        b64priv = flds[1]
+
+        # spit if someone else already has this dest
+        if b64priv in globalsessions.keys():
+            write("error dest in use\n")
+        elif b64priv in self.localsessions.keys():
+            # duh, already open locally, treat as ok
+            write("ok\n")
+        else:
+            # whole new session - set it up
+            dest = I2PDestination(base64private=b64priv)
+            log(4, "Creating session on dest '%s'" % b64priv[:40])
+            session = client.createSession(dest)
+            log(4, "Connecting session on dest '%s'" % b64priv[:40])
+            session.connect()
+            log(4, "Session on dest '%s' now live" % b64priv[:40])
+            
+            # and remember it
+            self.localsessions[b64priv] = session
+            globalsessions[b64priv] = session
+            
+            # and tell the client the good news
+            write("ok\n")
+    except:
+        logException(4, "createsession fail")
+        write("error exception\n")
+
+    sessionsLock.release()
+</t>
+<t tx="davidmcnab.041004144338.72">def on_destroysession(self, flds):
+
+    log(4, "entered")
+
+    server = self.server
+    client = server.i2pclient
+    globalsessions = server.sessions
+    sessionsLock = server.sessionsLock
+
+    read = self.rfile.read
+    readline = self.rfile.readline
+    write = self.wfile.write
+    flush = self.wfile.flush
+
+    sessionsLock.acquire()
+
+    try:
+        b64priv = flds[1]
+        
+        # spit if session not known
+        if not globalsessions.has_key(b64priv):
+            # no such session presently exists anywhere
+            write("error nosuchsession\n")
+        elif not self.localsessions.has_key(b64priv):
+            # session exists, but another client owns it
+            write("error notyoursession\n")
+        else:
+            # session exists and we own it
+            session = self.localsessions[b64priv]
+            del self.localsessions[b64priv]
+            del globalsessions[b64priv]
+            try:
+                session.destroySession()
+                write("ok\n")
+            except:
+                raise
+    except:
+        logException(4, "destroy session failed")
+        write("error exception\n")
+
+    sessionsLock.release()
+
+    log(4, "done")
+
+</t>
+<t tx="davidmcnab.041004144338.73">def on_send(self, flds):
+
+    #log(4, "entered: %s" % repr(flds))
+    log(4, "entered")
+
+    server = self.server
+    client = server.i2pclient
+    globalsessions = server.sessions
+    sessionsLock = server.sessionsLock
+
+    read = self.rfile.read
+    readline = self.rfile.readline
+    write = self.wfile.write
+    flush = self.wfile.flush
+
+    sessionsLock.acquire()
+
+    session = None
+    try:
+        size = int(flds[1])
+        b64priv = flds[2]
+        b64peer = flds[3]
+        msg = self._recvbytes(size)
+
+        # spit if session not known
+        if not globalsessions.has_key(b64priv):
+            # no such session presently exists anywhere
+            log(4, "no such session")
+            write("error nosuchsession\n")
+        elif not self.localsessions.has_key(b64priv):
+            # session exists, but another client owns it
+            write("error notyoursession\n")
+        else:
+            session = self.localsessions[b64priv]
+    except:
+        logException(2, "Send exception")
+        write("error exception on send command\n")
+
+    sessionsLock.release()
+
+    if not session:
+        return
+    
+    # now get/instantiate the remote dest
+    try:
+        peerDest = I2PDestination(base64=b64peer)
+    except:
+        peerDest = None
+        logException(2, "Send: bad remote dest")
+        write("error bad remote dest\n")
+    if not peerDest:
+        return
+
+    # and do the send
+    try:
+        res = session.sendMessage(peerDest, msg)
+    except:
+        logException(2, "Send: failed")
+        write("error exception on send\n")
+        res = None
+
+    if res is None:
+        return
+
+    # report result
+    if res:
+        write("ok\n")
+    else:
+        write("error send failed\n")
+
+    log(4, "done")
+
+</t>
+<t tx="davidmcnab.041004144338.74">def on_receive(self, flds):
+
+    log(4, "entered")
+
+    server = self.server
+    client = server.i2pclient
+    globalsessions = server.sessions
+    sessionsLock = server.sessionsLock
+
+    read = self.rfile.read
+    readline = self.rfile.readline
+    write = self.wfile.write
+    flush = self.wfile.flush
+
+    sessionsLock.acquire()
+
+    session = None
+    try:
+        b64priv = flds[1]
+
+        # spit if session not known
+        if not globalsessions.has_key(b64priv):
+            # no such session presently exists anywhere
+            write("error nosuchsession\n")
+        elif not self.localsessions.has_key(b64priv):
+            # session exists, but another client owns it
+            write("error notyoursession\n")
+        else:
+            session = self.localsessions[b64priv]
+    except:
+        logException(4, "receive command error")
+        write("error exception on receive command\n")
+    sessionsLock.release()
+
+    if not session:
+        log(4, "no session matching privdest %s" % b64priv[:30])
+        return
+    
+    # does this session have any received data?
+    if session.numMessages() &gt; 0:
+        msg = session.getMessage()
+        write("ok %s\n%s" % (len(msg), msg))
+    else:
+        write("ok 0\n")
+
+    log(4, "done")
+
+    return
+
+</t>
+<t tx="davidmcnab.041004144338.75">def on_HELLO(self, topic, subtopic, args):
+    """
+    Responds to client PING
+    """
+    log(4, "entered")
+    self.samSend("HELLO", "PONG")
+    log(4, "responded to HELLO")
+
+</t>
+<t tx="davidmcnab.041004144338.76">def on_SESSION(self, topic, subtopic, args):
+
+    log(4, "entered")
+
+    server = self.server
+    client = server.i2pclient
+    globalsessions = server.sessions
+    localsessions = self.localsessions
+    sessionsLock = server.sessionsLock
+
+    read = self.rfile.read
+    readline = self.rfile.readline
+    write = self.wfile.write
+    flush = self.wfile.flush
+
+    if subtopic == 'CREATE':
+        
+        if self.samSessionIsOpen:
+            self.samSend("SESSION", "STATUS",
+                         RESULT="I2P_ERROR",
+                         MESSAGE="Session_already_created",
+                         )
+            return
+
+        # get/validate STYLE arg
+        style = self.samSessionStyle = args.get('STYLE', None)
+        if style is None:
+            self.samSend("SESSION", "STATUS",
+                         RESULT="I2P_ERROR",
+                         MESSAGE="Missing_STYLE_argument",
+                         )
+            return
+        elif style not in ['STREAM', 'DATAGRAM', 'RAW']:
+            self.samSend("SESSION", "STATUS",
+                         RESULT="I2P_ERROR",
+                         MESSAGE="Invalid_STYLE_argument_'%s'" % style,
+                         )
+            return
+
+        # get/validate DESTINATION arg
+        dest = args.get('DESTINATION', None)
+        if dest == 'TRANSIENT':
+            # create new temporary dest
+            dest = self.samDest = I2PDestination()
+            destb64 = dest.toBase64Private()
+        else:
+            # make sure dest isn't globally or locally known
+            if dest in globalsessions.keys() or dest in localsessions.keys():
+                self.samSend("SESSION", "STATUS",
+                             RESULT="DUPLICATED_DEST",
+                             MESSAGE="Destination_'%s...'_already_in_use" % dest[:20],
+                             )
+                return
+
+            # try to reconstitute dest from given base64
+            try:
+                destb64 = dest
+                dest = I2PDestination(base64private=dest)
+            except:
+                self.samSend("SESSION", "STATUS",
+                             RESULT="INVALID_KEY",
+                             MESSAGE="Bad_destination_base64_string_'%s...'" % destb64[:20],
+                             )
+                return
+
+        # got valid dest now
+        self.dest = dest
+        self.samDestPub = dest.toBase64()
+
+        if style in ['RAW', 'DATAGRAM']:
+
+            if style == 'DATAGRAM':
+                # we need to know how big binary pub dests and sigs
+                self.samDestPubBin = dest.toBin()
+                self.samDestPubBinLen = len(self.samDestPubBin)
+                self.samSigLen = len(self.dest.sign("nothing"))
+                
+                log(4, "binary pub dests are %s bytes, sigs are %s bytes" % (
+                    self.samDestPubBinLen, self.samSigLen))
+
+            i2cpHost = args.get('I2CP.HOST', server.i2cphost)
+            i2cpPort = int(args.get('I2CP.PORT', server.i2cpport))
+
+            # both these styles require an I2PSession object
+            session = client.createSession(dest, host=i2cpHost, port=i2cpPort)
+            
+            # plug in our inbound message handler
+            session.on_message = self.on_message
+
+            log(4, "Connecting session on dest '%s'" % destb64[:40])
+            try:
+                session.connect()
+            except net.i2p.client.I2PSessionException:
+                self.samSend("SESSION", "STATUS",
+                             RESULT="I2P_ERROR",
+                             MESSAGE="Failed_to_connect_to_i2cp_port",
+                             )
+                logException(3, "Failed to connect I2PSession")
+                return
+                
+            log(4, "Session on dest '%s' now live" % destb64[:40])
+            
+            # and remember it
+            localsessions[destb64] = session
+            globalsessions[destb64] = session
+            self.samSession = session
+
+        else: # STREAM
+            # no need to create session object, because we're using streaming api
+
+            # but we do need to mark it as being in use
+            localsessions[destb64] = globalsessions[destb64] = None
+
+            # make a local socket
+            sock = self.samSock = I2PSocket(dest)
+
+            # and we also need to fire up a socket listener
+            thread.start_new_thread(self.threadSocketListener, (sock, dest))
+
+        # finally, we can reply with the good news
+        self.samSend("SESSION", "STATUS",
+                     RESULT="OK",
+                     )
+
+    else: # subtopic != CREATE
+        self.samSend("SESSION", "STATUS",
+                     RESULT="I2P_ERROR",
+                     MESSAGE="Invalid_command_'SESSION_%s'" % subtopic,
+                     )
+        return
+
+</t>
+<t tx="davidmcnab.041004144338.77">def on_SESSION_CREATE(self, topic, subtopic, args):
+
+    log(4, "entered")
+
+    server = self.server
+    client = server.i2pclient
+    globalsessions = server.sessions
+    localsessions = self.localsessions
+    sessionsLock = server.sessionsLock
+
+    read = self.rfile.read
+    readline = self.rfile.readline
+    write = self.wfile.write
+    flush = self.wfile.flush
+
+</t>
+<t tx="davidmcnab.041004144338.78">def on_STREAM(self, topic, subtopic, args):
+
+    log(4, "entered")
+
+    server = self.server
+    client = server.i2pclient
+    globalsessions = server.sessions
+    sessionsLock = server.sessionsLock
+
+    read = self.rfile.read
+    readline = self.rfile.readline
+    write = self.wfile.write
+    flush = self.wfile.flush
+
+    if subtopic == 'CONNECT':
+        # who are we connecting to again?
+        remdest = I2PDestionation(b64=args['DESTINATION'])
+        id = args['ID']
+    
+        try:
+            self.samSock.connect(remdest)
+            self.samSend("STREAM", "STATUS",
+                         RESULT='OK',
+                         ID=id,
+                         )
+        except:
+            self.samSend("STREAM", "STATUS",
+                         RESULT='I2P_ERROR',
+                         MESSAGE='exception on connect',
+                         )
+
+</t>
+<t tx="davidmcnab.041004144338.79">def on_DATAGRAM(self, topic, subtopic, args):
+    r"""
+    DATAGRAM SEND
+    DESTINATION=$base64key
+    SIZE=$numBytes\n[$numBytes of data]
+
+    All datagram messages have a signature/hash header, formatted as:
+        - sender's binary public dest
+        - S(H(sender_bin_pubdest + recipient_bin_pubdest + msg))
+    """
+    log(4, "entered")
+
+    # at this stage of things, we don't know how to handle anything except SEND
+    if subtopic != 'SEND':
+        log(3, "Got illegal subtopic '%s' in DATAGRAM command" % subtopic)
+        return
+
+    # get the details
+    peerdestb64 = args['DESTINATION']
+    peerdest = I2PDestination(base64=peerdestb64)
+    peerdestBin = base64dec(peerdestb64)
+    data = args['DATA']
+
+    # make up the header
+    log(4, "samDestPubBin (%s) %s" % (type(self.samDestPubBin), repr(self.samDestPubBin)))
+    log(4, "peerdestBin (%s) %s" % (type(peerdestBin), repr(peerdestBin)))
+    log(4, "data (%s) %s" % (type(data), repr(data)))
+
+    hashed = shahash(self.samDestPubBin + peerdestBin + data)
+    log(4, "hashed=%s" % repr(hashed))
+
+    sig = self.dest.sign(hashed)
+    log(4, "sig=%s" % repr(sig))
+    hdr = self.samDestPubBin + sig
+    
+    # send the thing
+    self.samSession.sendMessage(peerdest, hdr + data)
+
+</t>
+<t tx="davidmcnab.041004144338.80">def on_RAW(self, topic, subtopic, args):
+    r"""
+    RAW SEND
+    DESTINATION=$base64key
+    SIZE=$numBytes\n[$numBytes of data]
+    """
+    log(4, "entered")
+
+    # at this stage of things, we don't know how to handle anything except SEND
+    if subtopic != 'SEND':
+        return
+
+    # get the details
+    peerdest = I2PDestination(base64=args['DESTINATION'])
+    msg = args['DATA']
+
+    # send the thing
+    self.samSession.sendMessage(peerdest, msg)
+</t>
+<t tx="davidmcnab.041004144338.81">def on_NAMING(self, topic, subtopic, args):
+
+    log(4, "entered: %s %s %s" % (repr(topic), repr(subtopic), repr(args)))
+
+    # at this stage of things, we don't know how to handle anything except LOOKUP
+    if subtopic != 'LOOKUP':
+        return
+
+    # get the details
+    host = args['NAME']
+
+    log(4, "looking up host %s" % host)
+    
+    # try to lookup
+    jdest = self.namingService.lookup(host)
+
+    if not jdest:
+        log(4, "host %s not found" % host)
+        self.samSend("NAMING", "REPLY",
+                     RESULT="KEY_NOT_FOUND",
+                     NAME=host,
+                     )
+        return
+
+    try:
+        b64 = I2PDestination(dest=jdest).toBase64()
+        self.samSend("NAMING", "REPLY",
+                     RESULT="OK",
+                     NAME=host,
+                     VALUE=b64,
+                     )
+        log(4, "host %s found and valid key returned" % host)
+        return
+    except:
+        log(4, "host %s found but key invalid" % host)
+        self.samSend("NAMING", "REPLY",
+                     RESULT="INVALID_KEY",
+                     NAME=host,
+                     )
+
+</t>
+<t tx="davidmcnab.041004144338.82">def on_DEST(self, topic, subtopic, args):
+
+    log(4, "Generating dest")
+
+    dest = I2PDestination()
+    priv = dest.toBase64Private()
+    pub = dest.toBase64()
+
+    log(4, "Sending dest to client")
+
+    self.samSend("DEST", "REPLY", PUB=pub, PRIV=priv)
+
+    log(4, "done")
+</t>
+<t tx="davidmcnab.041004144338.83">def on_message(self, msg):
+    """
+    This callback gets plugged into the I2PSession object,
+    so we can asychronously notify our client when stuff arrives
+    """
+    if self.samSessionStyle == 'RAW':
+        self.samSend("RAW", "RECEIVE", msg)
+
+    elif self.samSessionStyle == 'DATAGRAM':
+        # ain't so simple, we gotta rip and validate the header
+        remdestBin = msg[:self.samDestPubBinLen]
+        log(4, "remdestBin=%s" % repr(remdestBin))
+
+        sig = msg[self.samDestPubBinLen:self.samDestPubBinLen+self.samSigLen]
+        log(4, "sig=%s" % repr(sig))
+
+        data = msg[self.samDestPubBinLen+self.samSigLen:]
+        log(4, "data=%s" % repr(data))
+        
+        # now try to verify
+        hashed = shahash(remdestBin + self.samDestPubBin + data)
+        log(4, "hashed=%s" % repr(hashed))
+
+        remdest = I2PDestination(bin=remdestBin)
+        if remdest.verify(hashed, sig):
+            # fine - very good, pass it on
+            log(4, "sig from peer is valid")
+            self.samSend("DATAGRAM", "RECEIVE", data,
+                         DESTINATION=remdest.toBase64(),
+                         )
+        else:
+            log(4, "DATAGRAM sig from peer is invalid")
+</t>
+<t tx="davidmcnab.041004144338.84">def threadSocketListener(self, sock, dest):
+    """
+    Listens for incoming socket connections, and
+    notifies the client accordingly
+    """
+    destb64 = dest.toBase64()
+
+    log(4, "Listening for connections to %s..." % destb64[:40])
+    while 1:
+        newsock = sock.accept()
+        
+        # need an id, negative
+        id = - self.server.samAllocId()
+
+        # register it in local and global streams
+        self.localstreams[id] = self.globalstreams[id] = newsock
+        
+        # who is connected to us?
+        remdest = newsock.remdest
+        remdest_b64 = remdest.toBase64()
+        
+        # and notify the client
+        self.samSend("STREAM", "CONNECTED",
+                     DESTINATION=remdest_b64,
+                     ID=id)
+</t>
+<t tx="davidmcnab.041004144338.85">def samParse(self, flds):
+    """
+    carves up a SAM command, returns it as a 3-tuple:
+        - cmd - command string
+        - subcmd - subcommand string
+        - dargs - dict of args
+    """
+    cmd = flds[0]
+    subcmd = flds[1]
+    args = flds[2:]
+    
+    dargs = {}
+    for arg in args:
+        try:
+            name, val = arg.split("=", 1)
+        except:
+            logException(3, "failed to process %s" % repr(arg))
+            raise
+        dargs[name] = val
+
+    # read and add data if any
+    if dargs.has_key('SIZE'):
+        size = dargs['SIZE'] = int(dargs['SIZE'])
+        dargs['DATA'] = self._recvbytes(size)
+
+    #log(4, "\n".join([cmd+" "+subcmd] + [("%s=%s (...)" % (k,v[:40])) for k,v in dargs.items()]))
+    log(4, "\n".join([cmd+" "+subcmd] + [("%s=%s (...)" % (k,v)) for k,v in dargs.items()]))
+
+    return cmd, subcmd, dargs
+
+
+
+</t>
+<t tx="davidmcnab.041004144338.86">def samSend(self, topic, subtopic, data=None, **kw):
+    """
+    Sends a SAM message (reply?) back to client
+    
+    Arguments:
+        - topic - the first word in the reply, eg 'STREAM'
+        - subtopic - the second word of the reply, eg 'CONNECTED'
+        - data - a string of raw data to send back (optional)
+    Keywords:
+        - extra 'name=value' items to pass back.
+    
+    Notes:
+        1. SIZE is not required. If sending back data, it will
+           be sized and a SIZE arg inserted automatically.
+        2. a dict of values can be passed to the 'args' keyword, in lieu
+           of direct keywords. This allows for cases where arg names would
+           cause python syntax clashes, eg 'tunnels.depthInbound'
+    """
+    items = [topic, subtopic]
+
+    # stick in SIZE if needed
+    if data is not None:
+        kw['SIZE'] = str(len(data))
+    else:
+        data = '' # for later
+
+    self.samCreateArgsList(kw, items)
+    
+    # and whack it together
+    buf = " ".join(items) + '\n' + data
+
+    # and ship it
+    self.sendLock.acquire()
+    try:
+        self._sendbytes(buf)
+    except:
+        self.sendLock.release()
+        raise
+    self.sendLock.release()
+
+</t>
+<t tx="davidmcnab.041004144338.87">def samCreateArgsList(self, kw1, lst):
+    for k,v in kw1.items():
+        if k == 'args':
+            self.samCreateArgsList(v, lst)
+        else:
+            lst.append("=".join([str(k), str(v)]))
+</t>
+<t tx="davidmcnab.041004144338.88">def _sendbytes(self, raw):
+
+    self.wfile.write(raw)
+    self.wfile.flush()
+</t>
+<t tx="davidmcnab.041004144338.89">def _recvbytes(self, count):
+    """
+    Does a guaranteed read of n bytes
+    """
+    read = self.rfile.read
+
+    chunks = []
+    needed = count
+    while needed &gt; 0:
+        chunk = read(needed)
+        chunklen = len(chunk)
+        needed -= chunklen
+        chunks.append(chunk)
+    raw = "".join(chunks)
+
+    # done
+    return raw
+
+</t>
+<t tx="davidmcnab.041004144338.90">class NoPrivateKey(Exception):
+    """Destination object has no private key"""
+
+class I2PSocketError(Exception):
+    """Error working with I2PSocket objects"""
+</t>
+<t tx="davidmcnab.041004144338.91"></t>
+<t tx="davidmcnab.041004144338.92">def shahash(s):
+    """
+    Calculates SHA Hash of a string, as a string, using
+    I2P hashing facility
+    """
+    h = net.i2p.crypto.SHA256Generator().calculateHash(s)
+    h = bytearray2str(h.getData())
+    return h
+</t>
+<t tx="davidmcnab.041004144338.93">def base64enc(s):
+    return net.i2p.data.Base64.encode(s)
+</t>
+<t tx="davidmcnab.041004144338.94">def base64dec(s):
+    return bytearray2str(net.i2p.data.Base64.decode(s))
+
+</t>
+<t tx="davidmcnab.041004144338.95">def str2bytearray(s):
+    """
+    Convenience - converts python string to java-friendly byte array
+    """
+    a = []
+    for c in s:
+        n = ord(c)
+        if n &gt;= 128:
+            n = n - 256
+        a.append(n)
+    return a
+
+</t>
+<t tx="davidmcnab.041004144338.96">def bytearray2str(a):
+    """
+    Convenience - converts java-friendly byte array to python string
+    """
+    chars = []
+    for n in a:
+        if n &lt; 0:
+            n += 256
+        chars.append(chr(n))
+    return "".join(chars)
+
+</t>
+<t tx="davidmcnab.041004144338.97">def byteoutstream2str(bs):
+    """
+    Convenience - converts java-friendly byteoutputstream to python string
+    """
+    chars = []
+    while 1:
+        c = bs.read()
+        if c &gt;= 0:
+            chars.append(chr(c))
+        else:
+            break
+    return "".join(chars)
+
+</t>
+<t tx="davidmcnab.041004144338.98">def dict2props(d):
+    """
+    Converts a python dict d into a java.util.Properties object
+    """
+    props = java.util.Properties()
+    for k,v in d.items():
+        props[k] = str(v)
+    return props
+
+
+</t>
+<t tx="davidmcnab.041004144338.99">def takeKey(somedict, keyname, default=None):
+    """
+    Utility function to destructively read a key from a given dict.
+    Same as the dict's 'takeKey' method, except that the key (if found)
+    sill be deleted from the dictionary.
+    """
+    if somedict.has_key(keyname):
+        val = somedict[keyname]
+        del somedict[keyname]
+    else:
+        val = default
+    return val
+</t>
+<t tx="davidmcnab.041004144338.100">def log(level, msg, nPrev=0):
+
+    # ignore messages that are too trivial for chosen verbosity
+    if level &gt; verbosity:
+        return
+
+    loglock.acquire()
+    try:
+        # rip the stack
+        caller = traceback.extract_stack()[-(2+nPrev)]
+        path, line, func = caller[:3]
+        path = os.path.split(path)[1]
+        full = "%s:%s:%s():\n* %s" % (
+            path,
+            line,
+            func,
+            msg.replace("\n", "\n   + "))
+        now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
+        msg = "%s %s\n" % (now, full)
+    
+        if logfile == sys.stdout:
+            print msg
+        else:
+            file(logfile, "a").write(msg+"\n")
+    except:
+        s = StringIO.StringIO()
+        traceback.print_exc(file=s)
+        print s.getvalue()
+        print "Logger crashed"
+    loglock.release()</t>
+<t tx="davidmcnab.041004144338.101">def logException(level, msg=''):
+    s = StringIO.StringIO()
+    traceback.print_exc(file=s)
+    log(level, "%s\n%s" % (s.getvalue(), msg), 1)
+</t>
+<t tx="davidmcnab.041004144338.102">def usage(detailed=0):
+    
+    print "Usage: %s &lt;options&gt; [&lt;command&gt;]" % sys.argv[0]
+    if not detailed:
+        print "Run with '-h' to get detailed help"
+        sys.exit(0)
+
+    print "I2PSAM is a bridge that allows I2P client programs to access the"
+    print "I2P network by talking over a plaintext socket connection."
+    print "References:"
+    print "   - http://www.freenet.org.nz/i2p - source, doco, downloadables"
+    print "   - http://drupal.i2p.net/node/view/144 - I2P SAM specification"
+    print
+    print "Options:"
+    print "  -h, -?, --help        - display this help"
+    print "  -v, --version         - print program version"
+    print "  -V, --verbosity=n     - set verbosity to n, default 2, 1==quiet, 4==noisy"
+    print "  -H, --listenhost=host - specify host to listen on for client connections"
+    print "  -P, --listenport=port - port to listen on for client connections"
+    print "      --i2cphost=host   - hostname of I2P router's I2CP interface"
+    print "      --i2cpport=port   - port of I2P router's I2CP interface"
+    print
+    print "Commands:"
+    print "     (run with no commands to launch SAM server)"
+    print "     samserver - runs as a SAM server"
+    print "     test - run a suite of self-tests"
+    print
+    
+    sys.exit(0)
+
+
+
+</t>
+<t tx="davidmcnab.041004144338.103">def main():
+
+    argv = sys.argv
+    argc = len(argv)
+
+    try:
+        opts, args = getopt.getopt(sys.argv[1:],
+                                   "h?vV:H:P:",
+                                   ['help', 'version', 'verbosity=',
+                                    'listenhost=', 'listenport=',
+                                    'i2cphost=', 'i2cpport=',
+                                    ])
+    except:
+        traceback.print_exc(file=sys.stdout)
+        usage("You entered an invalid option")
+
+    cmd = 'samserver'
+
+    # we prolly should pass all these parms in constructor call, but
+    # what the heck!
+    #global verbosity, i2psamhost, i2psamport, i2cpHost, i2cpPort
+    
+    serveropts = {}
+
+    for opt, val in opts:
+        if opt in ['-h', '-?', '--help']:
+            usage(1)
+        elif opt in ['-v', '--version']:
+            print "I2P SAM version %s" % version
+            sys.exit(0)
+        elif opt in ['-V', '--verbosity']:
+            serveropts['verbosity'] = int(val)
+        elif opt in ['-H', '--listenhost']:
+            serveropts['host'] = val
+        elif opt in ['-P', '--listenport']:
+            serveropts['port'] = int(val)
+        elif opt in ['--i2cphost']:
+            serveropts['i2cphost'] = val
+        elif opt in ['--i2cpport']:
+            serveropts['i2cpport'] = int(val)
+        else:
+            usage(0)
+
+    if len(args) == 0:
+        cmd = 'samserver'
+    else:
+        cmd = args[0]
+
+    if cmd == 'samserver':
+
+        log(2, "Running I2P SAM Server...")
+        server = I2PSamServer(**serveropts)
+        server.run()
+
+    elif cmd == 'test':
+        
+        print "RUNNING I2P Jython TESTS"
+        testsigs()
+        testdests()
+        testsession()
+        testsocket()
+
+    else:
+        usage(0)
+</t>
+<t tx="davidmcnab.041004144338.104"></t>
+<t tx="davidmcnab.041004144338.105">def testdests():
+    """
+    Demo function which tests out dest generation and import/export
+    """
+    print
+    print "********************************************"
+    print "Testing I2P destination create/export/import"
+    print "********************************************"
+    print
+
+    print "Generating a destination"
+    d1 = I2PDestination()
+
+    print "Exporting and importing dest1 in several forms"
+
+    print "public binary string..."
+    d1_bin = d1.toBin()
+    d2_bin = I2PDestination(bin=d1_bin)
+
+    print "public binary file..."
+    d1.toBinFile("temp-d1-bin")
+    d2_binfile = I2PDestination(binfile="temp-d1-bin")
+
+    print "private binary string..."
+    d1_binprivate = d1.toBinPrivate()
+    d2_binprivate = I2PDestination(binprivate=d1_binprivate)
+
+    print "private binary file..."
+    d1.toBinFilePrivate("temp-d1-bin-private")
+    d2_binfileprivate = I2PDestination(binfileprivate="temp-d1-bin-private")
+
+    print "public base64 string..."
+    d1_b64 = d1.toBase64()
+    d2_b64 = I2PDestination(base64=d1_b64)
+
+    print "public base64 file..."
+    d1.toBase64File("temp-d1-b64")
+    d2_b64file = I2PDestination(base64file="temp-d1-b64")
+
+    print "private base64 string..."
+    d1_base64private = d1.toBase64Private()
+    d2_b64private = I2PDestination(base64private=d1_base64private)
+
+    print "private base64 file..."
+    d1.toBase64FilePrivate("temp-d1-b64-private")
+    d2_b64fileprivate = I2PDestination(base64fileprivate="temp-d1-b64-private")
+
+    print "All destination creation/import/export tests passed!"
+
+
+</t>
+<t tx="davidmcnab.041004144338.106">def testsigs():
+    global d1, d1pub, d1sig, d1res
+    
+    print
+    print "********************************************"
+    print "Testing I2P dest-based signatures"
+    print "********************************************"
+    print
+    
+    print "Creating dest..."
+    d1 = I2PDestination()
+
+    s_good = "original stuff that we're signing"
+    s_bad = "non-original stuff we're trying to forge"
+    
+    print "Signing some shit against d1..."
+    d1sig = d1.sign(s_good)
+
+    print "Creating public dest d1pub"
+    d1pub = I2PDestination(bin=d1.toBin())
+
+    print "Verifying original data with d1pub"
+    res = d1pub.verify(s_good, d1sig)
+    print "Result: %s (should be 1)" % repr(res)
+    
+    print "Trying to verify on a different string"
+    res1 = d1pub.verify(s_bad, d1sig)
+    print "Result: %s (should be 0)" % repr(res1)
+    
+    if res and not res1:
+        print "signing/verifying test passed"
+    else:
+        print "SIGNING/VERIFYING TEST FAILED"
+
+</t>
+<t tx="davidmcnab.041004144338.107">def testsession():
+
+    global c, d1, d2, s1, s2
+
+    print
+    print "********************************************"
+    print "Testing I2P dest-&gt;dest messaging"
+    print "********************************************"
+    print
+    
+    print "Creating I2P client..."
+    c = I2PClient()
+
+    print "Creating destination d1..."
+    d1 = c.createDestination()
+
+    print "Creating destination d2..."
+    d2 = c.createDestination()
+
+    print "Creating destination d3..."
+    d3 = c.createDestination()
+
+    print "Creating session s1 on dest d1..."
+    s1 = c.createSession(d1, host='localhost', port=7654)
+
+    print "Creating session s2 on dest d2..."
+    s2 = c.createSession(d2)
+
+    print "Connecting session s1..."
+    s1.connect()
+
+    print "Connecting session s2..."
+    s2.connect()
+
+    print "Sending message from s1 to d2..."
+    s1.sendMessage(d2, "Hi there, s2!!")
+
+    print "Retrieving message from s2..."
+    print "got: %s" % repr(s2.getMessage())
+
+    print "Sending second message from s1 to d2..."
+    s1.sendMessage(d2, "Hi there again, s2!!")
+
+    print "Retrieving message from s2..."
+    print "got: %s" % repr(s2.getMessage())
+
+    print "Sending message from s1 to d3 (should take ages then fail)..."
+    res = s1.sendMessage(d3, "This is futile!!")
+    print "result of that send was %s (should have been 0)" % res
+
+    print "Destroying session s1..."
+    s1.destroySession()
+
+    print "Destroying session s2..."
+    s2.destroySession()
+
+    print "session tests passed!"
+</t>
+<t tx="davidmcnab.041004144338.108">def testsocket():
+
+    global d1, d2, s1, s2
+
+    print
+    print "********************************************"
+    print "Testing I2P streaming interface"
+    print "********************************************"
+    print
+    
+    print "Creating destinations..."
+    dServer = I2PDestination()
+    dClient = I2PDestination()
+
+    print "Creating sockets..."
+    sServer = I2PSocket(dServer)
+    sClient = I2PSocket(dClient)
+
+    # server thread which simply reads a line at a time, then echoes
+    # that line back to the client
+    def servThread(s):
+        print "server: binding socket"
+        s.bind()
+        print "server: setting socket to listen"
+        s.listen()
+        print "server: awaiting connection"
+        sock = s.accept()
+        print "server: got connection"
+
+        sock.send("Hello, echoing...\n")
+        buf = ''
+        while 1:
+            c = sock.recv(1)
+            if c == '':
+                sock.close()
+                print "server: socket closed"
+                break
+
+            buf += c
+            if c == '\n':
+                sock.send("SERVER: "+buf)
+                buf = ''
+
+    # client thread which reads lines and prints them to stdout
+    def clientThread(s):
+        buf = ''
+        while 1:
+            c = s.recv(1)
+            if c == '':
+                s.close()
+                print "client: socket closed"
+                break
+            buf += c
+            if c == '\n':
+                print "client: got %s" % repr(buf)
+                buf = ''
+
+    print "launching server thread..."
+    thread.start_new_thread(servThread, (sServer,))
+
+    print "client: trying to connect"
+    sClient.connect(dServer)
+
+    print "client: connected, launching rx thread"
+    thread.start_new_thread(clientThread, (sClient,))
+
+    while 1:
+        line = raw_input("Enter something (q to quit)&gt; ")
+        if line == 'q':
+            print "closing client socket"
+            sClient.close()
+            break
+        sClient.send(line+"\n")
+
+    print "I2PSocket test apparently succeeded"
+
+</t>
+<t tx="davidmcnab.041004144338.109">if __name__ == '__main__':
+    main()
+
+</t>
+<t tx="davidmcnab.041004144551">@first #!/usr/bin/env python
+"""
+Implements a client API for I2CP messaging via SAM
+
+Very simple I2P messaging interface, which should prove easy
+to reimplement in your language of choice
+
+This module can be used from cpython or jython
+
+Run this module without arguments to see a demo in action
+(requires SAM server to be already running)
+"""
+@others
+
+</t>
+<t tx="davidmcnab.041004144551.1">import sys, os, socket, thread, threading, Queue, traceback, StringIO, time
+
+</t>
+<t tx="davidmcnab.041004144551.2"># -----------------------------------------
+# server access settings
+
+i2psamhost = '127.0.0.1'
+i2psamport = 7656
+
+# ------------------------------------------
+# logging settings
+
+# 1=v.quiet, 2=normal, 3=verbose, 4=debug, 5=painful
+verbosity = 5
+
+# change to a filename to log there instead
+logfile = sys.stdout
+
+# when set to 1, and when logfile != sys.stdout, log msgs are written
+# both to logfile and console stdout
+log2console = 1
+
+# don't touch this!
+loglock = threading.Lock()
+
+</t>
+<t tx="davidmcnab.041004144551.3">class I2PServerFail(Exception):
+    """
+    A failure in connecting to the I2CP server
+    """
+
+class I2PCommandFail(Exception):
+    """
+    A failure in an I2CP command
+    """
+    pass
+</t>
+<t tx="davidmcnab.041004144551.4">class I2PSamClient:
+    """
+    Implements a reference client for accessing I2CP via i2psam
+    
+    Connects to i2psam's I2PSamServer, sends commands
+    and receives results
+
+    The primitives should be reasonably self-explanatory
+
+    Usage summary:
+        1. create one or more I2PSamClient instances per process (1 should be fine)
+        2. invoke the L{genkeys} method to create destination keypairs
+        3. create sessions objects via the L{createSession} method
+        4. use these session objects to send and receive data
+        5. destroy the session objects when you're done
+    
+    Refer to the function L{demo} for a simple example
+    """
+    @others
+</t>
+<t tx="davidmcnab.041004144551.5"># server host/port settings exist here in case you might
+# have a reason for overriding in a subclass
+
+host = i2psamhost
+port = i2psamport
+
+i2cpHost = None
+i2cpPort = None
+
+</t>
+<t tx="davidmcnab.041004144551.6">def __init__(self, **kw):
+    """
+    Creates a client connection to i2psam listener
+    
+    Keywords:
+        - host - host to connect to (default 127.0.0.1)
+        - port - port to connect to (default 7656)
+    """
+    # get optional host/port
+    log(4, "entered")
+
+    self.host = kw.get('host', self.host)
+    self.port = int(kw.get('port', self.port))
+
+    self.cmdLock = threading.Lock()
+
+    self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+
+    self.lockHello = threading.Lock()
+    self.sendLock = threading.Lock()
+    self.qNewDests = Queue.Queue()
+    self.qSession = Queue.Queue()
+    self.qDatagrams = Queue.Queue()
+    self.qRawMessages = Queue.Queue()
+    self.namingReplies = {}
+    self.namingCache = {}
+    self.isRunning = 1
+
+    log(4, "trying connection to SAM server...")
+    try:
+        self.sock.connect((self.host, self.port))
+    except:
+        raise I2PServerFail(
+            "Connection to i2psam server failed\n"
+            "(are you sure your I2P router is running, and\n"
+            "listening for I2CP connections on %s:%s?)" % (self.host, self.port)
+            )
+
+    # fire up receiver thread
+    thread.start_new_thread(self.threadRx, ())
+
+    # ping the server
+    try:
+        log(4, "trying to ping SAM server...")
+        self.samHello()
+    except:
+        logException(4, "Exception on handshaking")
+        raise I2PServerFail("Failed to handshake with i2psam server")
+
+    # connected fine
+    log(2, "I2CP Client successfully connected")
+</t>
+<t tx="davidmcnab.041004144551.7">def createSession(self, privdest):
+    """
+    DEPRECATED - use sam* methods instead!
+
+    Creates a session using private destkey
+    """
+    #3. createsession:
+    #    - client-&gt;server:
+    #        - createsession &lt;base64private&gt;\n
+    #    - server-&gt;client:
+    #        - ok\n  OR
+    #        - error[ &lt;reason&gt;]\n
+
+    self.cmdLock.acquire()
+    try:
+        self._sendline("createsession %s" % privdest)
+        respitems = self._recvline().split(" ", 1)
+        if respitems[0] == 'ok':
+            res = None
+        else:
+            res = respitems[1]
+    except:
+        logException(2, "createsession fail")
+        self.cmdLock.release()
+        raise
+
+    self.cmdLock.release()
+
+    if res:
+        raise I2PCommandFail("createsession fail: "+res)
+
+    return I2PRemoteSession(self, privdest)
+
+</t>
+<t tx="davidmcnab.041004144551.8">def destroySession(self, privdest):
+    """
+    DEPRECATED - use sam* methods instead!
+
+    Destrlys a session using private destkey
+    """
+    #4. destroysession:
+    #    - client-&gt;server:
+    #        - destroysession &lt;base64private&gt;\n
+    #    - server-&gt;client:
+    #        - ok\n OR
+    #        - error[ &lt;reason&gt;]\n
+
+    self.cmdLock.acquire()
+    try:
+        self._sendline("destroysession %s" % privdest)
+        respitems = self._recvline().split(" ", 1)
+        if respitems[0] == 'ok':
+            res = None
+        else:
+            res = respitems[1]
+    except:
+        logException(2, "destroysession fail")
+        self.cmdLock.release()
+        raise
+
+    self.cmdLock.release()
+
+    if res:
+        raise I2PCommandFail("destroysession fail: " + res)
+
+    return res
+
+</t>
+<t tx="davidmcnab.041004144551.9">def send(self, privdest, peerdest, msg):
+    """
+    DEPRECATED - use sam* methods instead!
+
+    Sends a block of data from local dest to remote dest
+    """
+    #5. send:
+    #    - client-&gt;server:
+    #        - send &lt;size&gt; &lt;localbase64private&gt; &lt;remotebase64dest&gt;\ndata
+    #    - server-&gt;client:
+    #        - ok\n OR
+    #        - error[ &lt;reason&gt;]\n
+
+    self.cmdLock.acquire()
+    try:
+        self._sendline("send %s %s %s" % (len(msg), privdest, peerdest))
+        self._sendbytes(msg)
+        line = self._recvline()
+        #print "** %s" % line
+        respitems = line.split(" ", 1)
+        if respitems[0] == 'ok':
+            res = None
+        else:
+            res = " ".join(respitems[1:])
+    except:
+        logException(2, "send fail")
+        self.cmdLock.release()
+        raise
+
+    self.cmdLock.release()
+
+    if res:
+        raise I2PCommandFail("send fail: " + res)
+
+    return res
+
+</t>
+<t tx="davidmcnab.041004144551.10">def receive(self, privdest):
+    """
+    DEPRECATED - use sam* methods instead!
+
+    receives a block of data, returning string, or None if no data available
+    """
+    #6. receive:
+    #    - client-&gt;server:
+    #        - receive &lt;localbase64private&gt;\n
+    #    - server-&gt;client:
+    #        - ok &lt;size&gt;\ndata OR
+    #        - error[ &lt;reason&gt;]\n
+
+    self.cmdLock.acquire()
+    try:
+        self._sendline("receive %s" % privdest)
+        respitems = self._recvline().split(" ", 1)
+        if respitems[0] == 'ok':
+            res = None
+            size = int(respitems[1])
+            msg = self._recvbytes(size)
+            res = None
+        else:
+            res = respitems[1]
+    except:
+        logException(2, "receive fail")
+        self.cmdLock.release()
+        raise
+
+    self.cmdLock.release()
+
+    if res:
+        raise I2PCommandFail("destroysession fail: " + res)
+
+    return msg
+</t>
+<t tx="davidmcnab.041004144551.11"></t>
+<t tx="davidmcnab.041004144551.12">def samHello(self):
+    """
+    Sends a quick HELLO PING to SAM server and awaits response
+    Arguments:
+        - none
+
+    Keywords:
+        - none
+    
+    Returns:
+        - nothing (None) if ping sent and pong received, or raises an exception if
+          failed
+    """
+    self.lockHello.acquire()
+    self.samSend("HELLO", "PING")
+    self.lockHello.acquire()
+    self.lockHello.release()
+</t>
+<t tx="davidmcnab.041004144551.13">def samSessionCreate(self, style, dest, **kw):
+    """
+    Creates a SAM session
+    
+    Arguments:
+        - style - one of 'STREAM', 'DATAGRAM' or 'RAW'
+        - dest - base64 private destination
+    
+    Keywords:
+        - i2cphost - hostname for the SAM bridge to contact i2p router on
+        - i2cpport - port for the SAM bridge to contact i2p router on
+    
+    Returns:
+        - 'OK' if session was created successfully, or a tuple
+          (keyword, message) if not
+    """
+    kw1 = dict(kw)
+    kw1['STYLE'] = self.samStyle = style
+    kw1['DESTINATION'] = dest
+
+    # stick in i2cp host/port if specified
+    if kw.has_key('i2cphost'):
+        kw1['I2CP.HOST'] = kw['i2cphost']
+    if kw.has_key('i2cpport'):
+        kw1['I2CP.PORT'] = kw['i2cpport']
+    
+    self.samSend("SESSION", "CREATE",
+                 **kw1)
+    subtopic, args = self.qSession.get()
+
+    if args['RESULT'] == 'OK':
+        return 'OK'
+    else:
+        return (args['RESULT'], args['MESSAGE'])
+</t>
+<t tx="davidmcnab.041004144551.14">def samDestGenerate(self):
+    """
+    Creates a whole new dest and returns an tuple pub, priv as
+    base64 public and private destination keys
+    """
+    self.samSend("DEST", "GENERATE")
+    pub, priv = self.qNewDests.get()
+    return pub, priv
+</t>
+<t tx="davidmcnab.041004144551.15">def samRawSend(self, peerdest, msg):
+    """
+    Sends a raw anon message to another peer
+    
+    peerdest is the public base64 destination key of the peer
+    """
+    self.samSend("RAW", "SEND", msg,
+                 DESTINATION=peerdest,
+                 )
+</t>
+<t tx="davidmcnab.041004144551.16">def samRawCheck(self):
+    """
+    Returns 1 if there are received raw messages available, 0 if not
+    """
+    return not self.qRawMessages.empty()
+</t>
+<t tx="davidmcnab.041004144551.17">def samRawReceive(self, blocking=1):
+    """
+    Returns the next raw message available,
+    blocking if none is available and the blocking arg is set to 0
+
+    If blocking is 0, and no messages are available, returns None.
+    
+    Remember that you can check for availability with
+    the .samRawCheck() method
+    """
+    if not blocking:
+        if self.qRawMessages.empty():
+            return None
+    return self.qRawMessages.get()
+    </t>
+<t tx="davidmcnab.041004144551.18">def samDatagramSend(self, peerdest, msg):
+    """
+    Sends a repliable datagram message to another peer
+
+    peerdest is the public base64 destination key of the peer
+    """
+    self.samSend("DATAGRAM", "SEND", msg,
+                 DESTINATION=peerdest,
+                 )
+</t>
+<t tx="davidmcnab.041004144551.19">def samDatagramCheck(self):
+    """
+    Returns 1 if there are datagram messages received messages available, 0 if not
+    """
+    return not self.qDatagrams.empty()
+</t>
+<t tx="davidmcnab.041004144551.20">def samDatagramReceive(self, blocking=1):
+    """
+    Returns the next datagram message available,
+    blocking if none is available.
+
+    If blocking is set to 0, and no messages are available,
+    returns None.
+    
+    Remember that you can check for availability with
+    the .samRawCheck() method
+    
+    Returns 2-tuple: dest, msg
+    where dest is the base64 destination of the peer from
+    whom the message was received
+    """
+    if not blocking:
+        if self.qDatagrams.empty():
+            return None
+    return self.qDatagrams.get()
+</t>
+<t tx="davidmcnab.041004144551.21">def samNamingLookup(self, host):
+    """
+    Looks up a host in hosts.txt
+    """
+    # try the cache first
+    if self.namingCache.has_key(host):
+        log(4, "found host %s in cache" % host)
+        return self.namingCache[host]
+
+    # make a queue for reply
+    q = self.namingReplies[host] = Queue.Queue()
+    
+    # send off req
+    self.samSend("NAMING", "LOOKUP",
+                 NAME=host,
+                 )
+
+    # get resp
+    resp = q.get()
+
+    result = resp.get('RESULT', 'none')
+    if result == 'OK':
+        log(4, "adding host %s to cache" % host)
+        val = resp['VALUE']
+        self.namingCache[host] = val
+        return val
+    else:
+        raise I2PCommandFail("Error looking up '%s': %s %s" % (
+            host, result, resp.get('MESSAGE', '')))
+
+</t>
+<t tx="davidmcnab.041004144551.22">def samParse(self, flds):
+    """
+    carves up a SAM command, returns it as a 3-tuple:
+        - cmd - command string
+        - subcmd - subcommand string
+        - dargs - dict of args
+    """
+    cmd = flds[0]
+    subcmd = flds[1]
+    args = flds[2:]
+    
+    dargs = {}
+    for arg in args:
+        try:
+            name, val = arg.split("=", 1)
+        except:
+            logException(3, "failed to process %s" % repr(arg))
+            raise
+        dargs[name] = val
+
+    # read and add data if any
+    if dargs.has_key('SIZE'):
+        size = dargs['SIZE'] = int(dargs['SIZE'])
+        dargs['DATA'] = self._recvbytes(size)
+
+    #log(4, "\n".join([cmd+" "+subcmd] + [("%s=%s (...)" % (k,v[:40])) for k,v in dargs.items()]))
+    log(4, "\n".join([cmd+" "+subcmd] + [("%s=%s (...)" % (k,v)) for k,v in dargs.items()]))
+
+    return cmd, subcmd, dargs
+
+
+
+</t>
+<t tx="davidmcnab.041004144551.23">def samSend(self, topic, subtopic, data=None, **kw):
+    """
+    Sends a SAM message (reply?) back to client
+    
+    Arguments:
+        - topic - the first word in the reply, eg 'STREAM'
+        - subtopic - the second word of the reply, eg 'CONNECTED'
+        - data - a string of raw data to send back (optional)
+    Keywords:
+        - extra 'name=value' items to pass back.
+    
+    Notes:
+        1. SIZE is not required. If sending back data, it will
+           be sized and a SIZE arg inserted automatically.
+        2. a dict of values can be passed to the 'args' keyword, in lieu
+           of direct keywords. This allows for cases where arg names would
+           cause python syntax clashes, eg 'tunnels.depthInbound'
+    """
+    items = [topic, subtopic]
+
+    # stick in SIZE if needed
+    if data is not None:
+        kw['SIZE'] = str(len(data))
+    else:
+        data = '' # for later
+
+    self.samCreateArgsList(kw, items)
+    
+    # and whack it together
+    buf = " ".join(items) + '\n' + data
+
+    # and ship it
+    self.sendLock.acquire()
+    try:
+        self._sendbytes(buf)
+    except:
+        self.sendLock.release()
+        raise
+    self.sendLock.release()
+
+</t>
+<t tx="davidmcnab.041004144551.24">def samCreateArgsList(self, kw1, lst):
+    for k,v in kw1.items():
+        if k == 'args':
+            self.samCreateArgsList(v, lst)
+        else:
+            lst.append("=".join([str(k), str(v)]))
+</t>
+<t tx="davidmcnab.041004144551.25"></t>
+<t tx="davidmcnab.041004144551.26">def threadRx(self):
+    """
+    Handles all incoming stuff from SAM, storing in
+    local queues as appropriate
+    """
+    while self.isRunning:
+        try:
+            log(4, "Awaiting next message from server")
+            line = self._recvline()
+            if line == '':
+                log(3, "I2P server socket closed")
+                return
+            flds = line.split(" ")
+            topic, subtopic, args = self.samParse(flds)
+            log(4, "Got %s %s %s" % (topic, subtopic, args))
+            handleMsg = getattr(self, "on_"+topic, None)
+            if handleMsg:
+                handleMsg(topic, subtopic, args)
+            else:
+                log(2, "No handler for '%s' message" % topic)
+        except:
+            #logException(3, "Exception handling %s %s\n%s" % (topic, subtopic, args))
+            logException(3, "Exception handling %s" % repr(line))
+</t>
+<t tx="davidmcnab.041004144551.27">def on_HELLO(self, topic, subtopic, args):
+    """
+    Handles HELLO PONG messages from server
+    """
+    # just wake up the caller
+    log(4, "got HELLO")
+    self.lockHello.release()
+</t>
+<t tx="davidmcnab.041004144551.28">def on_SESSION(self, topic, subtopic, args):
+    """
+    Handles SESSION messages from server
+    """
+    # just stick whatever on the queue and wake up the caller
+    res = subtopic, args
+    self.qSession.put(res)
+</t>
+<t tx="davidmcnab.041004144551.29">def on_STREAM(self, topic, subtopic, args):
+    """
+    Handles STREAM messages from server
+    """
+</t>
+<t tx="davidmcnab.041004144551.30">def on_DATAGRAM(self, topic, subtopic, args):
+    """
+    Handles DATAGRAM messages from server
+    """
+    remdest = args['DESTINATION']
+    data = args['DATA']
+    
+    self.qDatagrams.put((remdest, data))
+</t>
+<t tx="davidmcnab.041004144551.31">def on_RAW(self, topic, subtopic, args):
+    """
+    Handles RAW messages from server
+    """
+    data = args['DATA']
+
+    log(3, "Got anonymous datagram %s" % repr(data))
+    self.qRawMessages.put(data)
+</t>
+<t tx="davidmcnab.041004144551.32">def on_NAMING(self, topic, subtopic, args):
+    """
+    Handles NAMING messages from server
+    """
+    # just find out hostname, and stick it on resp q
+    host = args['NAME']
+    self.namingReplies[host].put(args)
+</t>
+<t tx="davidmcnab.041004144551.33">def on_DEST(self, topic, subtopic, args):
+    """
+    Handles DEST messages from server
+    """
+    pubkey = args['PUB']
+    privkey = args['PRIV']
+    res = pubkey, privkey
+    self.qNewDests.put(res)
+</t>
+<t tx="davidmcnab.041004144551.34"></t>
+<t tx="davidmcnab.041004144551.35">def _recvline(self):
+    """
+    Guaranteed read of a full line
+    """
+    chars = []
+    while 1:
+        c = self.sock.recv(1)
+        if c in ['', '\n']:
+            break
+        chars.append(c)
+    return "".join(chars)
+</t>
+<t tx="davidmcnab.041004144551.36">def _recvbytes(self, num):
+    """
+    Guaranteed read of num bytes
+    """
+    if num &lt;= 0:
+        return ""
+
+    reqd = num
+    chunks = []
+    while reqd &gt; 0:
+        chunk = self.sock.recv(reqd)
+        if not chunk:
+            raise I2PServerFail("Buffer read fail")
+        chunks.append(chunk)
+        reqd -= len(chunk)
+    return "".join(chunks)
+</t>
+<t tx="davidmcnab.041004144551.37">def _sendbytes(self, buf):
+    """
+    Guaranteed complete send of a buffer
+    """
+    reqd = len(buf)
+    while reqd &gt; 0:
+        nsent = self.sock.send(buf)
+        if nsent == 0:
+            raise I2PServerFail("Send to server failed")
+        buf = buf[nsent:]
+        reqd -= nsent
+</t>
+<t tx="davidmcnab.041004144551.38">def _sendline(self, line):
+    """
+    just tacks on a newline and sends
+    """
+    self._sendbytes(line+"\n")
+</t>
+<t tx="davidmcnab.041004144551.39">class I2PRemoteSession:
+    """
+    DEPRECATED
+
+    Wrapper for I2CP connections
+    
+    Do not instantiate this directly - it gets created by
+    I2PSamClient.createSession()
+    """    
+    @others
+</t>
+<t tx="davidmcnab.041004144551.40">def __init__(self, client, dest):
+    """
+    Do not instantiate this directly
+    """
+    self.client = client
+    self.dest = dest
+</t>
+<t tx="davidmcnab.041004144551.41">def send(self, peerdest, msg):
+
+    return self.client.send(self.dest, peerdest, msg)
+</t>
+<t tx="davidmcnab.041004144551.42">def receive(self):
+    
+    return self.client.receive(self.dest)
+</t>
+<t tx="davidmcnab.041004144551.43">def destroy(self):
+    
+    return self.client.destroySession(self.dest)
+
+</t>
+<t tx="davidmcnab.041004144551.44"></t>
+<t tx="davidmcnab.041004144551.45">def log(level, msg, nPrev=0):
+
+    # ignore messages that are too trivial for chosen verbosity
+    if level &gt; verbosity:
+        return
+
+    loglock.acquire()
+    try:
+        # rip the stack
+        caller = traceback.extract_stack()[-(2+nPrev)]
+        path, line, func = caller[:3]
+        path = os.path.split(path)[1]
+        full = "%s:%s:%s():\n* %s" % (
+            path,
+            line,
+            func,
+            msg.replace("\n", "\n   + "))
+        now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
+        msg = "%s %s\n" % (now, full)
+    
+        if logfile == sys.stdout:
+            print msg
+        else:
+            file(logfile, "a").write(msg+"\n")
+    except:
+        s = StringIO.StringIO()
+        traceback.print_exc(file=s)
+        print s.getvalue()
+        print "Logger crashed"
+    loglock.release()</t>
+<t tx="davidmcnab.041004144551.46">def logException(level, msg=''):
+    s = StringIO.StringIO()
+    traceback.print_exc(file=s)
+    log(level, "%s\n%s" % (s.getvalue(), msg), 1)
+</t>
+<t tx="davidmcnab.041004144551.47">def demoNAMING():
+    """
+    Demonstrates the NAMING service
+    """
+    print "Starting SAM NAMING demo..."
+    print
+
+    print "Instantiating client connection..."
+    c0 = I2PSamClient()
+    print "Client connection created"
+
+    for host in ['duck.i2p', 'nonexistent.i2p']:
+        print "Sending query for host '%s'..." % host
+        try:
+            res = c0.samNamingLookup(host)
+            print "query for %s returned:" % host
+            print repr(res)
+        except I2PCommandFail, e:
+            print "got exception: %s" % repr(e.args)
+    
+    print
+    print "---------------------------------"
+    print "NAMING service tests succeeded"
+    print "---------------------------------"
+    print
+
+
+</t>
+<t tx="davidmcnab.041004144551.48">def demoRAW():
+    """
+    Runs a demo of SAM RAW messaging
+    """
+    print "Starting SAM RAW demo..."
+    print
+
+    print "Instantiating client connections..."
+    c1 = I2PSamClient()
+    c2 = I2PSamClient()
+
+    print "Creating dests via SAM"
+    pub1, priv1 = c1.samDestGenerate()
+    pub2, priv2 = c2.samDestGenerate()
+    print "SAM Dests generated ok"
+    
+    print "Creating SAM RAW SESSION on connection c1..."
+    res = c1.samSessionCreate("RAW", priv1)
+    if res != 'OK':
+        print "Failed to create session on connection c1: %s" % repr(res)
+        return
+    print "Session on connection c1 created successfully"
+
+    print "Creating SAM SESSION on connection c2..."
+    res = c2.samSessionCreate("RAW", priv2)
+    if res != 'OK':
+        print "Failed to create session on connection c2: %s" % repr(res)
+        return
+    print "Session on connection c2 created successfully"
+
+    msg = "Hi there!"
+    print "sending from c1 to c2: %s" % repr(msg)
+    c1.samRawSend(pub2, msg)
+
+    print "now try to receive from c2 (will block)..."
+    msg1 = c2.samRawReceive()
+    print "Connection c2 got %s" % repr(msg1)
+
+    print
+    print "---------------------------------"
+    print "RAW data transfer tests succeeded"
+    print "---------------------------------"
+    print
+
+</t>
+<t tx="davidmcnab.041004144551.49">def demoDATAGRAM():
+    """
+    Runs a demo of SAM DATAGRAM messaging
+    """
+    print "Starting SAM DATAGRAM demo..."
+    print
+
+    print "Instantiating 2 more client connections..."
+    c3 = I2PSamClient()
+    c4 = I2PSamClient()
+
+    print "Creating more dests via SAM"
+    pub3, priv3 = c3.samDestGenerate()
+    pub4, priv4 = c4.samDestGenerate()
+
+    print "Creating SAM DATAGRAM SESSION on connection c3..."
+    res = c3.samSessionCreate("DATAGRAM", priv3)
+    if res != 'OK':
+        print "Failed to create DATAGRAM session on connection c3: %s" % repr(res)
+        return
+    print "DATAGRAM Session on connection c3 created successfully"
+
+    print "Creating SAM DATAGRAM SESSION on connection c4..."
+    res = c4.samSessionCreate("DATAGRAM", priv4)
+    if res != 'OK':
+        print "Failed to create DATAGRAM session on connection c4: %s" % repr(res)
+        return
+    print "Session on connection c4 created successfully"
+
+    msg = "Hi there, this is a datagram!"
+    print "sending from c3 to c4: %s" % repr(msg)
+    c3.samDatagramSend(pub4, msg)
+
+    print "now try to receive from c4 (will block)..."
+    remdest, msg1 = c4.samDatagramReceive()
+    print "Connection c4 got %s from %s..." % (repr(msg1), repr(remdest))
+
+
+    print
+    print "--------------------------------------"
+    print "DATAGRAM data transfer tests succeeded"
+    print "--------------------------------------"
+    print
+
+</t>
+<t tx="davidmcnab.041004144551.50">def demoSTREAM():
+    """
+    Runs a demo of SAM STREAM messaging
+    """
+    print "Starting SAM STREAM demo..."
+    print
+
+    print "Instantiating 2 more client connections..."
+    c5 = I2PSamClient()
+    c6 = I2PSamClient()
+
+    print "Creating more dests via SAM"
+    pub5, priv5 = c5.samDestGenerate()
+    pub6, priv6 = c6.samDestGenerate()
+
+    print "Creating SAM STREAM SESSION on connection c3..."
+    res = c5.samSessionCreate("STREAM", priv5)
+    if res != 'OK':
+        print "Failed to create STREAM session on connection c5: %s" % repr(res)
+        return
+    print "STREAM Session on connection c5 created successfully"
+
+    print "Creating SAM STREAM SESSION on connection c6..."
+    res = c6.samSessionCreate("STREAM", priv6)
+    if res != 'OK':
+        print "Failed to create STREAM session on connection c4: %s" % repr(res)
+        return
+    print "STREAM Session on connection c4 created successfully"
+
+    msg = "Hi there, this is a datagram!"
+    print "sending from c5 to c6: %s" % repr(msg)
+    c5.samStreamSend(pub6, msg)
+
+    print "now try to receive from c6 (will block)..."
+    msg1 = c6.samStreamReceive()
+    print "Connection c6 got %s from %s..." % (repr(msg1), repr(remdest))
+
+    print
+    print "--------------------------------------"
+    print "DATAGRAM data transfer tests succeeded"
+    print "--------------------------------------"
+    print
+
+</t>
+<t tx="davidmcnab.041004144551.51">def demo():
+    """
+    This is a simple and straightforward demo of talking to
+    the i2psam server socket via the I2PSamClient class.
+    
+    Read the source, Luke, it's never been so easy...
+    """
+    print
+    print "-----------------------------------------"
+    print "Running i2psamclient demo..."
+    print "-----------------------------------------"
+    print
+
+    demoNAMING()
+    demoRAW()
+    demoDATAGRAM()
+    #demoSTREAM()
+
+    print
+    print "-----------------------------------------"
+    print "Demo Finished"
+    print "-----------------------------------------"
+
+    return
+</t>
+<t tx="davidmcnab.041004144551.52">if __name__ == '__main__':
+
+    demo()
+</t>
+</tnodes>
+</leo_file>