forked from I2P_Developers/i2p.i2p
2428 lines
72 KiB
Python
2428 lines
72 KiB
Python
#!/usr/bin/env jython
|
|
#@+leo-ver=4
|
|
#@+node:@file jython/src/i2psam.py
|
|
#@@first
|
|
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
|
|
#@+node:imports
|
|
# 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
|
|
|
|
#@-node:imports
|
|
#@+node:globals
|
|
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()
|
|
|
|
|
|
#@-node:globals
|
|
#@+node:class JavaWrapper
|
|
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)
|
|
|
|
|
|
#@-node:class JavaWrapper
|
|
#@+node:class I2PDestination
|
|
class I2PDestination(JavaWrapper):
|
|
"""
|
|
Wraps java I2P destination objects, with a big difference - these
|
|
objects store the private parts.
|
|
"""
|
|
#@ @+others
|
|
#@+node:__init__
|
|
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()
|
|
|
|
#@-node:__init__
|
|
#@+node:toBin
|
|
def toBin(self):
|
|
"""
|
|
Returns a binary string of dest
|
|
"""
|
|
return bytearray2str(self.toByteArray())
|
|
|
|
#@-node:toBin
|
|
#@+node:toBinFile
|
|
def toBinFile(self, path):
|
|
"""
|
|
Writes out public binary to a file
|
|
"""
|
|
f = open(path, "wb")
|
|
f.write(self.toBin())
|
|
f.flush()
|
|
f.close()
|
|
|
|
#@-node:toBinFile
|
|
#@+node:toBinPrivate
|
|
def toBinPrivate(self):
|
|
"""
|
|
Returns the private key string as binary
|
|
"""
|
|
if self._private == None:
|
|
raise NoPrivateKey
|
|
return bytearray2str(self._private)
|
|
|
|
#@-node:toBinPrivate
|
|
#@+node:toBinFilePrivate
|
|
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()
|
|
|
|
#@-node:toBinFilePrivate
|
|
#@+node:toBase64
|
|
def toBase64(self):
|
|
"""
|
|
Returns base64 string of public part
|
|
"""
|
|
return self._item.toBase64()
|
|
|
|
#@-node:toBase64
|
|
#@+node:toBase64Private
|
|
def toBase64Private(self):
|
|
"""
|
|
Exports dest as base64, including private stuff
|
|
"""
|
|
if self._private == None:
|
|
raise NoPrivateKey
|
|
return i2p.data.Base64.encode(self._private)
|
|
|
|
#@-node:toBase64Private
|
|
#@+node:toBase64File
|
|
def toBase64File(self, path):
|
|
"""
|
|
Exports dest to file as base64
|
|
"""
|
|
f = open(path, "wb")
|
|
f.write(self.toBase64())
|
|
f.flush()
|
|
f.close()
|
|
|
|
#@-node:toBase64File
|
|
#@+node:toBase64FilePrivate
|
|
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()
|
|
|
|
#@-node:toBase64FilePrivate
|
|
#@+node:fromBin
|
|
def fromBin(self, bin):
|
|
"""
|
|
Loads this dest from a binary string
|
|
"""
|
|
self._item.fromByteArray(str2bytearray(bin))
|
|
self._private = None
|
|
|
|
#@-node:fromBin
|
|
#@+node:fromBinFile
|
|
def fromBinFile(self, path):
|
|
"""
|
|
Loads public part from file containing binary
|
|
"""
|
|
f = open(path, "rb")
|
|
self.fromBin(f.read())
|
|
f.close()
|
|
|
|
#@-node:fromBinFile
|
|
#@+node:fromBinPrivate
|
|
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)
|
|
|
|
#@-node:fromBinPrivate
|
|
#@+node:fromBinFilePrivate
|
|
def fromBinFilePrivate(self, path):
|
|
"""
|
|
Loads this dest object, given the pathname of a file containing
|
|
a binary destkey
|
|
"""
|
|
self.fromBinPrivate(open(path, "rb").read())
|
|
|
|
#@-node:fromBinFilePrivate
|
|
#@+node:fromBase64
|
|
def fromBase64(self, b64):
|
|
"""
|
|
Loads this dest from a base64 string
|
|
"""
|
|
self._item.fromBase64(b64)
|
|
self._private = None
|
|
|
|
#@-node:fromBase64
|
|
#@+node:fromBase64File
|
|
def fromBase64File(self, path):
|
|
"""
|
|
Loads public part from file containing base64
|
|
"""
|
|
f = open(path, "rb")
|
|
self.fromBase64(f.read())
|
|
f.close()
|
|
|
|
#@-node:fromBase64File
|
|
#@+node:fromBase64Private
|
|
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)
|
|
|
|
#@-node:fromBase64Private
|
|
#@+node:fromBase64PrivateFile
|
|
def fromBase64FilePrivate(self, path):
|
|
"""
|
|
Loads this dest from a base64 file containing private key
|
|
"""
|
|
self.fromBase64Private(open(path, "rb").read())
|
|
|
|
#@-node:fromBase64PrivateFile
|
|
#@+node:sign
|
|
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
|
|
|
|
#@-node:sign
|
|
#@+node:verify
|
|
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
|
|
|
|
|
|
|
|
#@-node:verify
|
|
#@+node:hasPrivate
|
|
def hasPrivate(self):
|
|
"""
|
|
Returns True if this dest has private parts, False if not
|
|
"""
|
|
|
|
if self._private:
|
|
return 1
|
|
else:
|
|
return 0
|
|
#@-node:hasPrivate
|
|
#@-others
|
|
|
|
#@-node:class I2PDestination
|
|
#@+node:class I2PClient
|
|
class I2PClient(JavaWrapper):
|
|
"""
|
|
jython-comfortable wrapper for java I2P client class
|
|
"""
|
|
#@ @+others
|
|
#@+node:__init__
|
|
def __init__(self, **kw):
|
|
"""
|
|
I2PClient constructor
|
|
|
|
No args or keywords as yet
|
|
"""
|
|
client = clientFactory.createClient()
|
|
JavaWrapper.__init__(self, client)
|
|
|
|
#@-node:__init__
|
|
#@+node:createDestination
|
|
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)
|
|
|
|
#@-node:createDestination
|
|
#@+node:createSession
|
|
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)
|
|
|
|
#@-node:createSession
|
|
#@-others
|
|
|
|
#@-node:class I2PClient
|
|
#@+node:class I2PSession
|
|
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
|
|
#@+node:attributes
|
|
host = i2cpHost
|
|
port = i2cpPort
|
|
#@-node:attributes
|
|
#@+node:__init__
|
|
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()
|
|
|
|
|
|
|
|
#@-node:__init__
|
|
#@+node:sendMessage
|
|
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
|
|
#@-node:sendMessage
|
|
#@+node:numMessages
|
|
def numMessages(self):
|
|
"""
|
|
Returns the number of unretrieved inbound messages
|
|
"""
|
|
self.lockInbound.acquire()
|
|
n = self.nInboundMessages
|
|
self.lockInbound.release()
|
|
return n
|
|
#@-node:numMessages
|
|
#@+node:getMessage
|
|
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 > 0:
|
|
msg = self.qInbound.get()
|
|
self.nInboundMessages -= 1
|
|
else:
|
|
msg = None
|
|
self.lockInbound.release()
|
|
#print "getMessage: released lock"
|
|
return msg
|
|
|
|
#@-node:getMessage
|
|
#@+node:setSessionListener
|
|
def setSessionListener(self, listener):
|
|
"""
|
|
Designates an L{I2PSessionListener} object to listen to this session
|
|
"""
|
|
self.listener = listener
|
|
listener.addSession(self)
|
|
self._item.setSessionListener(listener)
|
|
|
|
|
|
#@-node:setSessionListener
|
|
#@+node:destroySession
|
|
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
|
|
|
|
#@-node:destroySession
|
|
#@+node:CALLBACKS
|
|
#
|
|
# handler methods which you should override
|
|
#
|
|
|
|
#@+others
|
|
#@+node:on_message
|
|
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"
|
|
|
|
#@-node:on_message
|
|
#@+node:on_abuse
|
|
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
|
|
|
|
#@-node:on_abuse
|
|
#@+node:on_disconnected
|
|
def on_disconnected(self):
|
|
"""
|
|
Callback indicating remote peer disconnected
|
|
"""
|
|
print "on_disconnected"
|
|
|
|
#@-node:on_disconnected
|
|
#@+node:on_error
|
|
def on_error(self, message, error):
|
|
"""
|
|
Callback indicating an error occurred
|
|
"""
|
|
print "on_error: message=%s error=%s" % (message, error)
|
|
|
|
#@-node:on_error
|
|
#@-others
|
|
#@-node:CALLBACKS
|
|
#@-others
|
|
#@-node:class I2PSession
|
|
#@+node:class I2PSessionListener
|
|
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->%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)
|
|
|
|
#@-node:class I2PSessionListener
|
|
#@+node:class I2PSocket
|
|
class I2PSocket:
|
|
"""
|
|
Wraps I2P streaming API into a form resembling python sockets
|
|
"""
|
|
#@ @+others
|
|
#@+node:attributes
|
|
host = i2cpHost
|
|
port = i2cpPort
|
|
|
|
#@-node:attributes
|
|
#@+node:__init__
|
|
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()
|
|
#@-node:__init__
|
|
#@+node:bind
|
|
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()
|
|
#@-node:bind
|
|
#@+node:listen
|
|
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()
|
|
|
|
#@nonl
|
|
#@-node:listen
|
|
#@+node:accept
|
|
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
|
|
|
|
#@-node:accept
|
|
#@+node:connect
|
|
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
|
|
#@-node:connect
|
|
#@+node:recv
|
|
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 > 0:
|
|
byte = self.instream.read()
|
|
if byte < 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
|
|
|
|
|
|
#@-node:recv
|
|
#@+node:send
|
|
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"
|
|
#@-node:send
|
|
#@+node:available
|
|
def available(self):
|
|
"""
|
|
Returns the number of bytes available for recv()
|
|
"""
|
|
return self.sock.available()
|
|
|
|
#@-node:available
|
|
#@+node:close
|
|
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
|
|
#@-node:close
|
|
#@+node:_createSockmgr
|
|
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)
|
|
#@-node:_createSockmgr
|
|
#@-others
|
|
#@-node:class I2PSocket
|
|
#@+node:class I2PSamServer
|
|
class I2PSamServer(ThreadingTCPServer):
|
|
"""
|
|
A server which makes I2CP available via a socket
|
|
"""
|
|
#@ @+others
|
|
#@+node:attributes
|
|
host = i2psamhost
|
|
port = i2psamport
|
|
|
|
i2cphost = i2cpHost
|
|
i2cpport = i2cpPort
|
|
|
|
version = version
|
|
|
|
|
|
#@-node:attributes
|
|
#@+node:__init__
|
|
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
|
|
|
|
#@-node:__init__
|
|
#@+node:run
|
|
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()
|
|
|
|
|
|
#@-node:run
|
|
#@+node:finish_request
|
|
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")
|
|
#@-node:finish_request
|
|
#@+node:samAllocId
|
|
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
|
|
#@-node:samAllocId
|
|
#@-others
|
|
#@-node:class I2PSamServer
|
|
#@+node:class I2PSamClientHandler
|
|
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
|
|
#@+node:handle
|
|
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()
|
|
|
|
#@-node:handle
|
|
#@+node:on_genkeys
|
|
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")
|
|
#@-node:on_genkeys
|
|
#@+node:on_createsession
|
|
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()
|
|
#@-node:on_createsession
|
|
#@+node:on_destroysession
|
|
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")
|
|
|
|
#@-node:on_destroysession
|
|
#@+node:on_send
|
|
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")
|
|
|
|
#@-node:on_send
|
|
#@+node:on_receive
|
|
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() > 0:
|
|
msg = session.getMessage()
|
|
write("ok %s\n%s" % (len(msg), msg))
|
|
else:
|
|
write("ok 0\n")
|
|
|
|
log(4, "done")
|
|
|
|
return
|
|
|
|
#@-node:on_receive
|
|
#@+node:on_HELLO
|
|
def on_HELLO(self, topic, subtopic, args):
|
|
"""
|
|
Responds to client PING
|
|
"""
|
|
log(4, "entered")
|
|
self.samSend("HELLO", "PONG")
|
|
log(4, "responded to HELLO")
|
|
|
|
#@-node:on_HELLO
|
|
#@+node:on_SESSION
|
|
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
|
|
|
|
#@-node:on_SESSION
|
|
#@+node:on_SESSION_CREATE
|
|
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
|
|
|
|
#@-node:on_SESSION_CREATE
|
|
#@+node:on_STREAM
|
|
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',
|
|
)
|
|
|
|
#@-node:on_STREAM
|
|
#@+node:on_DATAGRAM
|
|
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)
|
|
|
|
#@-node:on_DATAGRAM
|
|
#@+node:on_RAW
|
|
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)
|
|
#@-node:on_RAW
|
|
#@+node:on_NAMING
|
|
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,
|
|
)
|
|
|
|
#@-node:on_NAMING
|
|
#@+node:on_DEST
|
|
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")
|
|
#@-node:on_DEST
|
|
#@+node:on_message
|
|
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")
|
|
#@-node:on_message
|
|
#@+node:threadSocketListener
|
|
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)
|
|
#@-node:threadSocketListener
|
|
#@+node:samParse
|
|
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
|
|
|
|
|
|
|
|
#@-node:samParse
|
|
#@+node:samSend
|
|
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()
|
|
|
|
#@-node:samSend
|
|
#@+node:samCreateArgsList
|
|
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)]))
|
|
#@-node:samCreateArgsList
|
|
#@+node:_sendbytes
|
|
def _sendbytes(self, raw):
|
|
|
|
self.wfile.write(raw)
|
|
self.wfile.flush()
|
|
#@-node:_sendbytes
|
|
#@+node:_recvbytes
|
|
def _recvbytes(self, count):
|
|
"""
|
|
Does a guaranteed read of n bytes
|
|
"""
|
|
read = self.rfile.read
|
|
|
|
chunks = []
|
|
needed = count
|
|
while needed > 0:
|
|
chunk = read(needed)
|
|
chunklen = len(chunk)
|
|
needed -= chunklen
|
|
chunks.append(chunk)
|
|
raw = "".join(chunks)
|
|
|
|
# done
|
|
return raw
|
|
|
|
#@-node:_recvbytes
|
|
#@-others
|
|
#@nonl
|
|
#@-node:class I2PSamClientHandler
|
|
#@+node:Exceptions
|
|
class NoPrivateKey(Exception):
|
|
"""Destination object has no private key"""
|
|
|
|
class I2PSocketError(Exception):
|
|
"""Error working with I2PSocket objects"""
|
|
#@-node:Exceptions
|
|
#@+node:shahash
|
|
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
|
|
#@-node:shahash
|
|
#@+node:base64enc
|
|
def base64enc(s):
|
|
return net.i2p.data.Base64.encode(s)
|
|
#@-node:base64enc
|
|
#@+node:base64dec
|
|
def base64dec(s):
|
|
return bytearray2str(net.i2p.data.Base64.decode(s))
|
|
|
|
#@-node:base64dec
|
|
#@+node:str2bytearray
|
|
def str2bytearray(s):
|
|
"""
|
|
Convenience - converts python string to java-friendly byte array
|
|
"""
|
|
a = []
|
|
for c in s:
|
|
n = ord(c)
|
|
if n >= 128:
|
|
n = n - 256
|
|
a.append(n)
|
|
return a
|
|
|
|
#@-node:str2bytearray
|
|
#@+node:bytearray2str
|
|
def bytearray2str(a):
|
|
"""
|
|
Convenience - converts java-friendly byte array to python string
|
|
"""
|
|
chars = []
|
|
for n in a:
|
|
if n < 0:
|
|
n += 256
|
|
chars.append(chr(n))
|
|
return "".join(chars)
|
|
|
|
#@-node:bytearray2str
|
|
#@+node:byteoutstream2str
|
|
def byteoutstream2str(bs):
|
|
"""
|
|
Convenience - converts java-friendly byteoutputstream to python string
|
|
"""
|
|
chars = []
|
|
while 1:
|
|
c = bs.read()
|
|
if c >= 0:
|
|
chars.append(chr(c))
|
|
else:
|
|
break
|
|
return "".join(chars)
|
|
|
|
#@-node:byteoutstream2str
|
|
#@+node:dict2props
|
|
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
|
|
|
|
|
|
#@-node:dict2props
|
|
#@+node:takeKey
|
|
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
|
|
#@-node:takeKey
|
|
#@+node:log
|
|
def log(level, msg, nPrev=0):
|
|
|
|
# ignore messages that are too trivial for chosen verbosity
|
|
if level > 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()
|
|
#@nonl
|
|
#@-node:log
|
|
#@+node:logException
|
|
def logException(level, msg=''):
|
|
s = StringIO.StringIO()
|
|
traceback.print_exc(file=s)
|
|
log(level, "%s\n%s" % (s.getvalue(), msg), 1)
|
|
#@-node:logException
|
|
#@+node:usage
|
|
def usage(detailed=0):
|
|
|
|
print "Usage: %s <options> [<command>]" % 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)
|
|
|
|
|
|
|
|
#@-node:usage
|
|
#@+node:main
|
|
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)
|
|
#@-node:main
|
|
#@+node:testdests
|
|
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!"
|
|
|
|
|
|
#@-node:testdests
|
|
#@+node:testsigs
|
|
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"
|
|
|
|
#@-node:testsigs
|
|
#@+node:testsession
|
|
def testsession():
|
|
|
|
global c, d1, d2, s1, s2
|
|
|
|
print
|
|
print "********************************************"
|
|
print "Testing I2P dest->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!"
|
|
#@-node:testsession
|
|
#@+node:testsocket
|
|
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)> ")
|
|
if line == 'q':
|
|
print "closing client socket"
|
|
sClient.close()
|
|
break
|
|
sClient.send(line+"\n")
|
|
|
|
print "I2PSocket test apparently succeeded"
|
|
|
|
#@-node:testsocket
|
|
#@+node:MAINLINE
|
|
if __name__ == '__main__':
|
|
main()
|
|
|
|
#@-node:MAINLINE
|
|
#@-others
|
|
|
|
|
|
#@-node:@file jython/src/i2psam.py
|
|
#@-leo
|