771 lines
25 KiB
Python
771 lines
25 KiB
Python
|
|
# -------------------------------------------------------------
|
|
# samclasses.py: Lower-level SAM classes, for internal use.
|
|
# -------------------------------------------------------------
|
|
|
|
"""
|
|
Lower-level SAM API, interfaces with SAM Bridge.
|
|
|
|
For internal use only.
|
|
|
|
Use the higher level i2p.socket module for your own programs.
|
|
|
|
For details on SAM, see "Simple Anonymous Messaging (SAM) v1.0,"
|
|
as published by jrandom.
|
|
|
|
Class Overview:
|
|
|
|
- SAMTerminal: Message sender/reader, talks to SAM Bridge.
|
|
- StringBuffer: Queue for character data.
|
|
- BaseSession: SAM session classes are derived from this.
|
|
- StreamSession: SAM stream session class, threadsafe, high level.
|
|
- DatagramSession: SAM datagram session, threadsafe, high level.
|
|
- RawSession: SAM raw session, threadsafe, high level.
|
|
|
|
Note that a 'None' timeout is an infinite timeout: it
|
|
blocks forever if necessary.
|
|
|
|
Todo:
|
|
- Error handling is a huge mess. Neaten it up.
|
|
Subclass a ErrorMixin class, then use set_error(e),
|
|
check_error(), get_error().
|
|
- Streams are a huge mess. Neaten them up.
|
|
- This whole interface is a tad confusing. Neaten it up.
|
|
"""
|
|
|
|
# ---------------------------------------------------------
|
|
# Imports
|
|
# ---------------------------------------------------------
|
|
|
|
import Queue, traceback, random, sys, shlex
|
|
import thread, threading, time, string
|
|
|
|
# ---------------------------------------------------------
|
|
# Import i2p and i2p.socket (for defaults and errors)
|
|
# ---------------------------------------------------------
|
|
|
|
import i2p
|
|
import i2p.socket
|
|
from i2p.pylib import socket as pysocket # Import Python socket
|
|
|
|
# ---------------------------------------------------------
|
|
# Functions
|
|
# ---------------------------------------------------------
|
|
|
|
def sleep(): time.sleep(0.01) # Sleep between thread polls
|
|
|
|
log = False # Logging flag. Logs to ./log.txt.
|
|
|
|
# -----------------------------------------------------
|
|
# SAMTerminal
|
|
# -----------------------------------------------------
|
|
|
|
class SAMTerminal:
|
|
"""Message-by-message communication with SAM through a single
|
|
pysocket. _on_* messages are dispatched to msgobj."""
|
|
|
|
def __init__(self, addr, msgobj):
|
|
try: self.host, self.port = addr.split(':')
|
|
except: raise ValueError('sam port required')
|
|
self.port = int(self.port)
|
|
self.sock=pysocket.socket(pysocket.AF_INET,pysocket.SOCK_STREAM)
|
|
self.msgobj = msgobj
|
|
try:
|
|
self.sock.connect((self.host, self.port))
|
|
except:
|
|
raise i2p.RouterError('could not contact SAM bridge on ' +
|
|
self.host + ':' + str(self.port))
|
|
thread.start_new_thread(self._poll_loop, ())
|
|
self.error = None
|
|
self.lost_error = i2p.RouterError('SAM bridge connection lost')
|
|
|
|
def _poll_loop(self):
|
|
"""Polling loop for incoming messages."""
|
|
try:
|
|
while True:
|
|
# Read until newline
|
|
line = []
|
|
while True:
|
|
try: c = self.sock.recv(1)
|
|
except pysocket.error, ex: self.error = self.lost_error
|
|
if c == '': self.error = self.lost_error
|
|
if self.error != None: return
|
|
if c == '\n': break
|
|
if c != '': line += [c]
|
|
line = ''.join(line)
|
|
if log:
|
|
logf = open('log.txt', 'a')
|
|
logf.write('\n' + line + '\n')
|
|
logf.close()
|
|
(msg, kwargs) = self._samdecode(line)
|
|
# Read N bytes if SIZE=N is present.
|
|
if 'SIZE' in kwargs:
|
|
data = []
|
|
remain = int(kwargs['SIZE'])
|
|
while True:
|
|
try: s = self.sock.recv(remain)
|
|
except pysocket.error, ex: self.error = self.lost_error
|
|
if s == '': self.error = self.lost_error
|
|
if self.error != None: return
|
|
if s != '': data += [s]
|
|
remain -= len(s)
|
|
if remain <= 0: break
|
|
data = ''.join(data)
|
|
# Store the read data in kwargs['DATA'].
|
|
kwargs['DATA'] = data
|
|
del kwargs['SIZE']
|
|
# Dispatch the message
|
|
try: self.on_message(msg, kwargs)
|
|
except Exception, e:
|
|
# On exception in on_message, print a warning, keep going.
|
|
print 'Unhandled exception in polling thread.'
|
|
traceback.print_exc()
|
|
|
|
# Don't need to sleep since recv() blocks.
|
|
# End of while loop
|
|
except Exception, e:
|
|
# For other exceptions, print a fatal error and stop polling.
|
|
print 'Fatal exception in polling thread'
|
|
traceback.print_exc(); sys.exit()
|
|
|
|
def _samdecode(self, s):
|
|
"""Given a SAM command, returns (a, b), where a is the string at
|
|
the beginning of the command, and b is a dictionary of name,
|
|
value pairs for the command."""
|
|
(args, kwargs) = ([], {})
|
|
for w in shlex.split(s):
|
|
if '=' in w: kwargs[w.split('=')[0]] = w.split('=')[1]
|
|
else: args += [w]
|
|
return (' '.join(args), kwargs)
|
|
|
|
def check_message(self, kwargs):
|
|
"""Raises an error if kwargs['RESULT'] != 'OK'."""
|
|
if not kwargs.get('RESULT', '') in ['OK', '']:
|
|
raise i2p.socket.NetworkError((kwargs['RESULT'],
|
|
kwargs.get('MESSAGE', '')))
|
|
|
|
def on_message(self, msg, kwargs):
|
|
"""Process a SAM message that was received. Dispatch to
|
|
self._on_MESSAGE_NAME(**kwargs)."""
|
|
name = '_on_' + msg.upper().replace(' ', '_')
|
|
getattr(self.msgobj, name)(**kwargs)
|
|
|
|
def send_message(self, msg):
|
|
"""Send a message to the SAM bridge. A newline will be
|
|
automatically added if none is present."""
|
|
self.check()
|
|
if not '\n' in msg: msg = msg + '\n'
|
|
if log:
|
|
logf = open('log.txt', 'a')
|
|
logf.write('\n' + msg)
|
|
logf.close()
|
|
try: self.sock.sendall(msg)
|
|
except pysocket.error: self.error = self.lost_error
|
|
self.check()
|
|
|
|
def check(self):
|
|
"""Raise an error if terminal was closed, otherwise do
|
|
nothing."""
|
|
if self.error != None: raise self.error
|
|
|
|
def close(self):
|
|
"""Close the SAM terminal."""
|
|
# If data is sent via STREAM SEND, and the socket is closed
|
|
# immediately, the data will be lost. Delay 0.01 s to fix this
|
|
# bug (tested Windows, Linux).
|
|
time.sleep(0.01)
|
|
self.error = i2p.socket.ClosedError()
|
|
self.sock.close()
|
|
|
|
def queue_get(self, q):
|
|
"""Identical to q.get() unless a call to self.check() fails,
|
|
in which case the waiting is cut short with an error."""
|
|
while True:
|
|
try: return q.get_nowait()
|
|
except Queue.Empty: pass
|
|
self.check()
|
|
sleep()
|
|
|
|
|
|
# -------------------------------------------------------
|
|
# StringBuffer: A FIFO for string data.
|
|
# -------------------------------------------------------
|
|
|
|
class Deque:
|
|
"""A double-ended queue."""
|
|
def __init__(self):
|
|
self.a = []
|
|
self.b = []
|
|
def push_last(self, obj):
|
|
"""Append obj to the end of the deque."""
|
|
self.b.append(obj)
|
|
def push_first(self, obj):
|
|
"""Prepend obj to the beginning of the deque."""
|
|
self.a.append(obj)
|
|
def _partition(self):
|
|
if len(self) > 1:
|
|
self.a.reverse()
|
|
all = self.a + self.b
|
|
n = len(all) / 2
|
|
self.a = all[:n]
|
|
self.b = all[n:]
|
|
self.a.reverse()
|
|
def pop_last(self):
|
|
"""Pop an item off the end of the deque, and return it."""
|
|
if not self.b: self._partition()
|
|
try: return self.b.pop()
|
|
except: return self.a.pop()
|
|
def pop_first(self):
|
|
"""Pop an item off the beginning of the deque, and return it."""
|
|
if not self.a: self._partition()
|
|
try: return self.a.pop()
|
|
except: return self.b.pop()
|
|
def __len__(self):
|
|
"""Number of items in the deque."""
|
|
return len(self.b) + len(self.a)
|
|
|
|
class StringBuffer(Deque):
|
|
"""A FIFO for characters. Strings can be efficiently
|
|
appended to the end, and read from the beginning.
|
|
|
|
Example:
|
|
>>> B = StringBuffer('Hello W')
|
|
>>> B.append('orld!')
|
|
>>> B.read(5)
|
|
'Hello'
|
|
>>> B.read()
|
|
'World!'
|
|
"""
|
|
def __init__(self, s=''):
|
|
Deque.__init__(self)
|
|
self.length = 0
|
|
self.append(s)
|
|
def append(self, s):
|
|
"""Append string data to the end of the buffer."""
|
|
n = 128
|
|
for block in [s[i:i+n] for i in range(0,len(s),n)]:
|
|
self.push_last(block)
|
|
self.length += len(s)
|
|
def prepend(self, s):
|
|
"""Prepend string data to the beginning of the buffer."""
|
|
n = 128
|
|
blocks = [s[i:i+n] for i in range(0,len(s),n)]
|
|
blocks.reverse()
|
|
for block in blocks:
|
|
self.push_first(block)
|
|
self.length += len(s)
|
|
def read(self, n=None):
|
|
"""Read n bytes of data (or less if less data available) from the
|
|
beginning of the buffer. The data is removed. If n is
|
|
omitted, read the entire buffer."""
|
|
if n == None or n > len(self): n = len(self)
|
|
destlen = len(self) - n
|
|
ans = []
|
|
while len(self) > destlen:
|
|
ans += [self.pop_first()]
|
|
self.length -= len(ans[-1])
|
|
ans = ''.join(ans)
|
|
self.prepend(ans[n:])
|
|
ans = ans[:n]
|
|
return ans
|
|
def peek(self, n=None):
|
|
"""Like read(), but do not remove the data that is returned."""
|
|
ans = self.read(n)
|
|
self.prepend(ans)
|
|
return ans
|
|
def __len__(self): return self.length
|
|
def __str__(self): return self.peek()
|
|
def __repr__(self): return 'StringBuffer(' + str(self) + ')'
|
|
|
|
# -----------------------------------------------------
|
|
# BaseSession
|
|
# -----------------------------------------------------
|
|
|
|
class BaseSession:
|
|
"""Base session, from which StreamSession, DatagramSession,
|
|
and RawSession are derived."""
|
|
|
|
def __init__(self, addr=''):
|
|
if addr == '': addr = i2p.socket.samaddr
|
|
self.term = SAMTerminal(addr=addr, msgobj=self)
|
|
self.lock = threading.Lock() # Data lock.
|
|
self.closed = False
|
|
self.qhello = Queue.Queue() # Thread messaging, HELLO REPLY.
|
|
self.qnaming = Queue.Queue() # Thread messaging, NAMING REPLY.
|
|
self.qsession = Queue.Queue() # Thread messaging, SESSION STATUS.
|
|
self._hello() # Do handshake with SAM bridge.
|
|
|
|
def _hello(self):
|
|
"""Internal command, handshake with SAM terminal."""
|
|
self.term.send_message('HELLO VERSION MIN=' +
|
|
str(i2p.socket.samver) + ' MAX=' + str(i2p.socket.samver))
|
|
self.term.check_message(self.term.queue_get(self.qhello))
|
|
|
|
def _on_HELLO_REPLY(self, **kwargs):
|
|
"""Internal command, got HELLO REPLY."""
|
|
self.qhello.put(kwargs) # Pass kwargs back to _hello.
|
|
|
|
def _on_SESSION_STATUS(self, **kwargs):
|
|
"""Internal command, got SESSION STATUS."""
|
|
self.qsession.put(kwargs) # Pass kwargs back to main thread.
|
|
|
|
def _namelookup(self, name):
|
|
"""Internal command, does a NAMING LOOKUP query."""
|
|
self.term.send_message('NAMING LOOKUP NAME=' + name)
|
|
# Read back response, check it, and return X in VALUE=X.
|
|
kwargs = self.term.queue_get(self.qnaming)
|
|
self.term.check_message(kwargs)
|
|
return kwargs['VALUE']
|
|
|
|
def _on_NAMING_REPLY(self, **kwargs):
|
|
"""Internal command, got NAMING REPLY."""
|
|
self.qnaming.put(kwargs) # Pass kwargs back to _namelookup.
|
|
|
|
def _set_properties(self):
|
|
"""Internal command, call at end of __init__ to set up
|
|
properties."""
|
|
self.dest = self._namelookup('ME')
|
|
|
|
def close(self):
|
|
"""Close the session."""
|
|
# Synchronize
|
|
self.lock.acquire()
|
|
try:
|
|
# Close the terminal if we're not already closed.
|
|
if not self.closed: self.term.close()
|
|
self.closed = True
|
|
finally: self.lock.release()
|
|
|
|
def _encode_kwargs(self, **kwargs):
|
|
"""Internal command, encode extra kwargs for passing to
|
|
SESSION CREATE."""
|
|
ans = ''
|
|
for k in kwargs:
|
|
if k == 'in_depth':
|
|
ans += ' tunnels.depthInbound=' + \
|
|
str(int(kwargs['in_depth']))
|
|
elif k == 'out_depth':
|
|
ans += ' tunnels.depthOutbound=' + \
|
|
str(int(kwargs['out_depth']))
|
|
else:
|
|
raise ValueError('unexpected keyword argument ' + repr(k))
|
|
return ans
|
|
|
|
# -----------------------------------------------------
|
|
# StreamSession
|
|
# -----------------------------------------------------
|
|
|
|
class StreamSession(BaseSession):
|
|
"""Stream session. All methods are blocking and threadsafe."""
|
|
|
|
def __init__(self, name, addr='', **kwargs):
|
|
if addr == '': addr = i2p.socket.samaddr
|
|
BaseSession.__init__(self, addr)
|
|
self.idmap = {} # Maps id to Stream object.
|
|
self.qaccept = Queue.Queue() # Thread messaging, accept.
|
|
self.name = name
|
|
self.max_accept = 0 # Max queued incoming connections.
|
|
|
|
# Create stream session.
|
|
if name == '':
|
|
name = 'TRANSIENT'
|
|
|
|
# DIRECTION=BOTH (the default) is used because we can't know in
|
|
# advance whether a session will call listen().
|
|
|
|
self.term.send_message('SESSION CREATE STYLE=STREAM' +
|
|
' DESTINATION=' + name + self._encode_kwargs(**kwargs))
|
|
self.term.check_message(self.term.queue_get(self.qsession))
|
|
|
|
self._set_properties()
|
|
|
|
def connect(self, dest, timeout=None):
|
|
"""Create a stream connected to remote destination 'dest'. The
|
|
id is random. If the timeout is exceeded, do NOT raise an
|
|
error; rather, return a Stream object with .didconnect set
|
|
to False."""
|
|
if not isinstance(dest, type('')): raise TypeError
|
|
# Synchronize
|
|
self.lock.acquire()
|
|
try:
|
|
# Pick a positive stream id at random.
|
|
while True:
|
|
# 9/10 probability of success per iteration
|
|
id = random.randrange(1, len(self.idmap) * 10 + 2)
|
|
if not id in self.idmap:
|
|
ans = Stream(self, dest, id, didconnect=False)
|
|
self.idmap[id] = ans
|
|
break
|
|
finally: self.lock.release()
|
|
# Send STREAM CONNECT and wait for reply.
|
|
self.term.send_message('STREAM CONNECT ID=' + str(id) +
|
|
' DESTINATION=' + str(dest))
|
|
|
|
# Now wait until the stream's .didconnect flag is set to True.
|
|
if timeout != None: end = time.clock() + timeout
|
|
while True:
|
|
self.term.check()
|
|
if ans.didconnect: break
|
|
if timeout != None and time.clock() >= end: break
|
|
sleep()
|
|
|
|
return ans
|
|
|
|
def _on_STREAM_STATUS(self, **kwargs):
|
|
"""Internal command, got STREAM STATUS. Unblocks connect."""
|
|
# Store error is needed
|
|
try: self.term.check_message(kwargs)
|
|
except Exception, e:
|
|
try: self.idmap[int(kwargs['ID'])].err = e
|
|
except: pass # Closed too quickly
|
|
|
|
# Now set .didconnect flag to True.
|
|
try: self.idmap[int(kwargs['ID'])].didconnect = True
|
|
except: pass # Closed too quickly
|
|
|
|
def accept(self, timeout=None):
|
|
"""Wait for incoming connection, and return a Stream object
|
|
for it."""
|
|
if self.max_accept <= 0:
|
|
raise i2p.Error('listen(n) must be called before accept ' +
|
|
'(n>=1)')
|
|
if timeout != None: end = time.clock() + timeout
|
|
while True:
|
|
self.term.check()
|
|
# Synchronized
|
|
self.lock.acquire()
|
|
try:
|
|
# Get Stream object if available.
|
|
if self.qaccept.qsize() > 0:
|
|
return self.term.queue_get(self.qaccept)
|
|
finally: self.lock.release()
|
|
if timeout != None and time.clock() >= end: break
|
|
sleep()
|
|
|
|
# Handle timeout and blocking errors
|
|
if timeout == 0.0:
|
|
raise i2p.socket.BlockError('command would have blocked')
|
|
else:
|
|
raise i2p.socket.Timeout('timed out')
|
|
|
|
def listen(self, backlog):
|
|
"""Set maximum number of queued connections."""
|
|
if self.closed: raise sam.ClosedError()
|
|
self.max_accept = backlog
|
|
|
|
def _on_STREAM_CONNECTED(self, **kwargs):
|
|
"""Got STREAM CONNECTED command. This is what accept() commands
|
|
wait for."""
|
|
# Synchronize
|
|
self.lock.acquire()
|
|
try:
|
|
# Drop connection if over maximum size.
|
|
if self.qaccept.qsize() >= self.max_accept:
|
|
self.term.send_message('STREAM CLOSE ID=' +
|
|
str(int(kwargs['ID'])))
|
|
return
|
|
|
|
# Parse, create Stream, and place on self.qaccept.
|
|
self.term.check_message(kwargs)
|
|
# A negative id is chosen for us
|
|
id = int(kwargs['ID'])
|
|
self.idmap[id] = Stream(self, kwargs['DESTINATION'], id)
|
|
# Pass Stream object back to accept.
|
|
self.qaccept.put(self.idmap[id])
|
|
finally: self.lock.release()
|
|
|
|
def _send_stream(self, id, data):
|
|
"""Internal command, send data to stream id. Use Stream.send
|
|
in your code."""
|
|
self.term.send_message('STREAM SEND ID=' + str(id) + ' SIZE=' +
|
|
str(len(data)) + '\n' + data)
|
|
|
|
def _on_STREAM_CLOSED(self, **kwargs):
|
|
"""Got STREAM CLOSED command. Call idmap[id].on_close(e) and
|
|
delete idmap[id]."""
|
|
id = int(kwargs['ID'])
|
|
|
|
# No error is produced for a graceful remote close.
|
|
e = None
|
|
try: self.term.check_message(kwargs)
|
|
except i2p.Error, err: e = err
|
|
|
|
# Synchronize
|
|
self.lock.acquire()
|
|
try:
|
|
# Sent STREAM CLOSE, SAM didn't hear us in time.
|
|
if not id in self.idmap: return
|
|
# Pop id from self.idmap, if available.
|
|
obj = self.idmap[id]
|
|
del self.idmap[id]
|
|
finally: self.lock.release()
|
|
|
|
# Process on_close message.
|
|
obj.on_close(None)
|
|
|
|
def _on_STREAM_RECEIVED(self, **kwargs):
|
|
"""Got STREAM RECEIVED command. Dispatch to
|
|
idmap[id].on_receive(s)."""
|
|
id = int(kwargs['ID'])
|
|
if not id in self.idmap:
|
|
# _on_STREAM_CONNECTED blocks until self.idmap[id] is properly
|
|
# set up. Therefore, we have received a stream packet despite
|
|
# closing the stream immediately after _on_STREAM_CONNECTED
|
|
# (SAM ignored us). So ignore it.
|
|
return
|
|
self.idmap[id].on_receive(kwargs['DATA'])
|
|
|
|
def __len__(self):
|
|
"""Unconnected session; has no read data available."""
|
|
return 0
|
|
|
|
|
|
class Stream:
|
|
"""Receives and sends data for an individual stream."""
|
|
|
|
def __init__(self, parent, remotedest, id, didconnect=True):
|
|
self.parent = parent
|
|
self.buf = StringBuffer()
|
|
self.localdest = parent.dest
|
|
self.remotedest = remotedest
|
|
self.id = id
|
|
# Data lock. Allow multiple acquire()s by same thread
|
|
self.lock = threading.RLock()
|
|
self.closed = False
|
|
# Error message, on STREAM STATUS, or on STREAM CLOSED.
|
|
self.err = None
|
|
# Whether stream got a STREAM CONNECTED message
|
|
self.didconnect = didconnect
|
|
|
|
def send(self, s):
|
|
"""Sends the string s, blocking if necessary."""
|
|
id = self.id
|
|
if self.closed or id == None:
|
|
if self.err != None: raise self.err
|
|
raise i2p.socket.ClosedError('stream closed')
|
|
if len(s) == 0: return
|
|
nmax = 32768
|
|
for block in [s[i:i+nmax] for i in range(0,len(s),nmax)]:
|
|
self.parent._send_stream(id, block)
|
|
|
|
def recv(self, n, timeout=None, peek=False, waitall=False):
|
|
"""Reads up to n bytes in a manner identical to socket.recv.
|
|
Blocks for up to timeout seconds if n > 0 and no data is
|
|
available (timeout=None means wait forever). If still no data
|
|
is available, raises BlockError or Timeout. For a closed
|
|
stream, recv will read the data stored in the buffer until
|
|
EOF, at which point the read data will be truncated. If peek
|
|
is True, the data is not removed. If waitall is True, reads
|
|
exactly n bytes, or raises BlockError or Timeout as
|
|
appropriate. Returns data."""
|
|
|
|
if n < 0: raise ValueError
|
|
if n == 0: return ''
|
|
|
|
minlen = 1
|
|
if waitall: minlen = n
|
|
|
|
if timeout != None: end = time.clock() + timeout
|
|
while True:
|
|
# Synchronized check and read until data available.
|
|
self.parent.term.check()
|
|
self.lock.acquire()
|
|
try:
|
|
if len(self.buf) >= minlen:
|
|
if peek: return self.buf.peek(n)
|
|
else: return self.buf.read(n)
|
|
# Graceful close: return as much data as possible
|
|
# (up to n bytes).
|
|
if self.closed and self.err == None: return self.buf.read(n)
|
|
# Ungraceful close: raise an error.
|
|
if self.err != None: raise self.err
|
|
finally: self.lock.release()
|
|
if timeout != None and time.clock() >= end: break
|
|
sleep()
|
|
|
|
# Handle timeout and blocking error
|
|
if timeout == 0.0:
|
|
raise i2p.socket.BlockError('command would have blocked')
|
|
else:
|
|
raise i2p.socket.Timeout('timed out')
|
|
|
|
def __len__(self):
|
|
"""Current length of read buffer."""
|
|
return len(self.buf)
|
|
|
|
def close(self):
|
|
"""Close the stream. Threadsafe."""
|
|
# Synchronize self.parent.
|
|
self.parent.lock.acquire()
|
|
try:
|
|
if not self.closed:
|
|
self.closed = True
|
|
id = self.id
|
|
# Set self.id to None, so we don't close a new stream by
|
|
# accident.
|
|
self.id = None
|
|
if not id in self.parent.idmap: return
|
|
self.parent.term.send_message('STREAM CLOSE ID=' + str(id))
|
|
# No error is produced for a locally closed stream
|
|
self.on_close(None)
|
|
del self.parent.idmap[id]
|
|
finally: self.parent.lock.release()
|
|
|
|
def on_receive(self, s):
|
|
# Synchronize
|
|
self.lock.acquire()
|
|
try:
|
|
self.buf.append(s)
|
|
finally: self.lock.release()
|
|
|
|
def on_close(self, e):
|
|
self.closed = True
|
|
self.err = e
|
|
|
|
def __del__(self):
|
|
self.close()
|
|
|
|
# -----------------------------------------------------
|
|
# DatagramSession
|
|
# -----------------------------------------------------
|
|
|
|
class DatagramSession(BaseSession):
|
|
"""Datagram session. All methods are blocking and threadsafe."""
|
|
|
|
def __init__(self, name, addr='', **kwargs):
|
|
if addr == '': addr = i2p.socket.samaddr
|
|
BaseSession.__init__(self, addr)
|
|
self.buf = Deque() # FIFO of incoming packets.
|
|
self.name = name
|
|
|
|
# Create datagram session
|
|
if name == '': name = 'TRANSIENT'
|
|
self.term.send_message('SESSION CREATE STYLE=DATAGRAM ' +
|
|
'DESTINATION=' + name + self._encode_kwargs(**kwargs))
|
|
self.term.check_message(self.term.queue_get(self.qsession))
|
|
|
|
self._set_properties()
|
|
|
|
def _on_DATAGRAM_RECEIVED(self, **kwargs):
|
|
"""Internal method, got DATAGRAM RECEIVED."""
|
|
# Synchronized
|
|
self.lock.acquire()
|
|
try:
|
|
self.buf.push_last((kwargs['DATA'], kwargs['DESTINATION']))
|
|
finally: self.lock.release()
|
|
|
|
def send(self, s, dest):
|
|
"""Send packet with contents s to given destination."""
|
|
# Raise error if packet is too large.
|
|
if len(s) > i2p.socket.MAX_DGRAM:
|
|
raise ValueError('packets must have length <= ' +
|
|
str(i2p.socket.MAX_DGRAM) + ' bytes')
|
|
self.term.send_message('DATAGRAM SEND DESTINATION=' + dest +
|
|
' SIZE=' + str(len(s)) + '\n' + s)
|
|
|
|
def recv(self, timeout=None, peek=False):
|
|
"""Get a single packet. Blocks for up to timeout seconds if
|
|
n > 0 and no packet is available (timeout=None means wait
|
|
forever). If still no packet is available, raises BlockError
|
|
or Timeout. Returns the pair (data, address). If peek is
|
|
True, the data is not removed."""
|
|
if timeout != None: end = time.clock() + timeout
|
|
while True:
|
|
self.term.check()
|
|
# Synchronized check and read until data available.
|
|
self.lock.acquire()
|
|
try:
|
|
if len(self.buf) > 0:
|
|
if peek:
|
|
ans = self.buf.pop_first()
|
|
self.buf.push_first(ans)
|
|
return ans
|
|
else:
|
|
return self.buf.pop_first()
|
|
finally: self.lock.release()
|
|
if timeout != None and time.clock() >= end: break
|
|
sleep()
|
|
|
|
# Handle timeout and blocking error
|
|
if timeout == 0.0:
|
|
raise i2p.socket.BlockError('command would have blocked')
|
|
else:
|
|
raise i2p.socket.Timeout('timed out')
|
|
|
|
def __len__(self):
|
|
"""Number of packets in read buffer."""
|
|
return len(self.buf)
|
|
|
|
# -----------------------------------------------------
|
|
# RawSession
|
|
# -----------------------------------------------------
|
|
|
|
class RawSession(BaseSession):
|
|
"""Raw session. All methods are blocking and threadsafe."""
|
|
|
|
def __init__(self, name, addr='', **kwargs):
|
|
if addr == '': addr = i2p.socket.samaddr
|
|
BaseSession.__init__(self, addr)
|
|
self.buf = Deque() # FIFO of incoming packets.
|
|
self.name = name
|
|
|
|
# Create raw session
|
|
if name == '': name = 'TRANSIENT'
|
|
self.term.send_message('SESSION CREATE STYLE=RAW DESTINATION=' +
|
|
name + self._encode_kwargs(**kwargs))
|
|
self.term.check_message(self.term.queue_get(self.qsession))
|
|
|
|
self._set_properties()
|
|
|
|
def _on_RAW_RECEIVED(self, **kwargs):
|
|
"""Internal method, got RAW RECEIVED."""
|
|
# Synchronized
|
|
self.lock.acquire()
|
|
try:
|
|
self.buf.push_last((kwargs['DATA'], ''))
|
|
finally: self.lock.release()
|
|
|
|
def send(self, s, dest):
|
|
"""Send packet with contents s to given destination."""
|
|
# Raise error if packet is too big
|
|
if len(s) > i2p.socket.MAX_RAW:
|
|
raise ValueError('packets must have length <= ' +
|
|
str(i2p.socket.MAX_RAW) + ' bytes')
|
|
self.term.send_message('RAW SEND DESTINATION=' + dest +
|
|
' SIZE=' + str(len(s)) + '\n' + s)
|
|
|
|
def recv(self, timeout=None, peek=False):
|
|
"""Identical to DatagramSocket.recv. The from address is an
|
|
empty string."""
|
|
if timeout != None: end = time.clock() + timeout
|
|
while True:
|
|
self.term.check()
|
|
# Synchronized check and read until data available.
|
|
self.lock.acquire()
|
|
try:
|
|
if len(self.buf) > 0:
|
|
if peek:
|
|
ans = self.buf.pop_first()
|
|
self.buf.push_first(ans)
|
|
return ans
|
|
else:
|
|
return self.buf.pop_first()
|
|
finally: self.lock.release()
|
|
if timeout != None and time.clock() >= end: break
|
|
sleep()
|
|
|
|
# Handle timeout and blocking error
|
|
if timeout == 0.0:
|
|
raise i2p.socket.BlockError('command would have blocked')
|
|
else:
|
|
raise i2p.socket.Timeout('timed out')
|
|
|
|
def __len__(self):
|
|
"""Number of packets in read buffer."""
|
|
return len(self.buf)
|
|
|
|
|
|
# -----------------------------------------------------
|
|
# End of file
|
|
# -----------------------------------------------------
|