Initial commit
commit
150197513d
@ -0,0 +1 @@
|
||||
*.pyc
|
@ -0,0 +1,4 @@
|
||||
from reactor import Reactor
|
||||
from baseclient import BaseClient
|
||||
from server import Server
|
||||
from filelike import FileLike
|
@ -0,0 +1,144 @@
|
||||
import socket, ssl, msgpack, tempfile, os
|
||||
from collections import deque
|
||||
from bitstring import BitArray
|
||||
from filelike import FileLike
|
||||
|
||||
class BaseClient:
|
||||
_tempbuff = b""
|
||||
_read_left = 0
|
||||
_read_buff = b""
|
||||
_tempfiles = {}
|
||||
_last_datastream_id = 10
|
||||
_active_datastreams = {}
|
||||
_filelike_counter = 0
|
||||
max_mem = 32 * 1024 * 1024
|
||||
reactor = None
|
||||
|
||||
def __init__(self, host=None, port=None, use_ssl=False, allowed_certs=None, conn=None, source=None, **kwargs):
|
||||
self.objtype = "client"
|
||||
self.sendq = deque([])
|
||||
|
||||
if (host is None or port is None) and (conn is None or source is None):
|
||||
raise Exception("You must specify either a connection and source address, or a hostname and port.")
|
||||
|
||||
if host is not None:
|
||||
# Treat this as a new client
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.ssl = use_ssl
|
||||
self.spawned = False
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
if self.ssl == True:
|
||||
self.stream = ssl.wrap_socket(sock, cert_reqs=ssl.CERT_REQUIRED, ca_certs=allowed_certs)
|
||||
else:
|
||||
self.stream = sock
|
||||
|
||||
self.stream.connect((self.host, self.port))
|
||||
self.event_connected()
|
||||
|
||||
elif conn is not None:
|
||||
# Treat this as a client spawned by a server
|
||||
self.host = source[0]
|
||||
self.port = source[1]
|
||||
self.stream = conn
|
||||
self.spawned = True
|
||||
self.event_connected()
|
||||
|
||||
def _send_chunk(self, chunk):
|
||||
self.stream.send(chunk)
|
||||
|
||||
def _encode_header(self, chunktype, size, channel):
|
||||
header_type = BitArray(uint=chunktype, length=7)
|
||||
header_size = BitArray(uint=size, length=25)
|
||||
header_channel = BitArray(uint=channel, length=24)
|
||||
header = header_type + header_size + header_channel
|
||||
return header.bytes
|
||||
|
||||
def _pack(self, data):
|
||||
return msgpack.packb(data, default=self._encode_pack)
|
||||
|
||||
def _encode_pack(self, obj):
|
||||
if hasattr(obj, "read"):
|
||||
datastream_id = self._create_datastream(obj)
|
||||
|
||||
# Determine the total size of the file
|
||||
current_pos = obj.tell()
|
||||
obj.seek(0, os.SEEK_END)
|
||||
total_size = obj.tell()
|
||||
obj.seek(current_pos)
|
||||
|
||||
obj = {"__type__": "file", "__id__": datastream_id, "__size__": total_size}
|
||||
|
||||
return obj
|
||||
|
||||
def _unpack(self, data):
|
||||
return msgpack.unpackb(data, object_hook=self._decode_unpack)
|
||||
|
||||
def _decode_unpack(self, obj):
|
||||
if "__type__" in obj:
|
||||
if obj['__type__'] == "file":
|
||||
# TODO: Validate item
|
||||
datastream_id = obj['__id__']
|
||||
size = obj['__size__']
|
||||
self._create_tempfile(datastream_id)
|
||||
obj = self._tempfiles[datastream_id]
|
||||
obj._total_size = size
|
||||
|
||||
return obj
|
||||
|
||||
def _create_datastream(self, obj):
|
||||
datastream_id = self._get_datastream_id()
|
||||
self._active_datastreams[datastream_id] = obj
|
||||
#print "Datastream created on ID %d." % datastream_id
|
||||
return datastream_id
|
||||
|
||||
def _get_datastream_id(self):
|
||||
self._last_datastream_id += 1
|
||||
|
||||
if self._last_datastream_id > 10000:
|
||||
self._last_datastream_id = 10
|
||||
|
||||
if self._last_datastream_id in self._active_datastreams:
|
||||
return self._get_datastream_id()
|
||||
|
||||
return self._last_datastream_id
|
||||
|
||||
def _create_tempfile(self, datastream_id):
|
||||
# This creates a temporary file for the specified datastream if it does not already exist.
|
||||
if datastream_id not in self._tempfiles:
|
||||
self._filelike_counter += 1
|
||||
self._tempfiles[datastream_id] = FileLike(tempfile.SpooledTemporaryFile(max_size=self.max_mem), self._filelike_counter)
|
||||
|
||||
def _receive_datastream(self, datastream_id, data):
|
||||
self._create_tempfile(datastream_id)
|
||||
obj = self._tempfiles[datastream_id]
|
||||
obj.write(data)
|
||||
obj._bytes_finished += len(data)
|
||||
self.event_datastream_progress(obj, obj._bytes_finished, obj._total_size)
|
||||
|
||||
def _send_system_message(self, data):
|
||||
encoded = self._pack(data)
|
||||
header = self._encode_header(3, len(encoded), 1)
|
||||
self.sendq.append(header + encoded)
|
||||
|
||||
def send(self, data):
|
||||
encoded = self._pack(data)
|
||||
header = self._encode_header(1, len(encoded), 1)
|
||||
self.sendq.append(header + encoded)
|
||||
|
||||
def event_connected(self):
|
||||
pass
|
||||
|
||||
def event_disconnected(self):
|
||||
pass
|
||||
|
||||
def event_receive(self, data):
|
||||
pass
|
||||
|
||||
def event_datastream_progress(self, stream, finished_bytes, total_bytes):
|
||||
pass
|
||||
|
||||
def event_datastream_finished(self, stream):
|
||||
pass
|
@ -0,0 +1,23 @@
|
||||
import os
|
||||
|
||||
class FileLike:
|
||||
_pos = 0
|
||||
_total_size = 0
|
||||
_bytes_finished = 0
|
||||
|
||||
def __init__(self, original, file_id):
|
||||
self._file = original
|
||||
self.id = file_id
|
||||
|
||||
def write(self, str):
|
||||
return self._file.write(str)
|
||||
|
||||
def read(self, size=-1):
|
||||
self._file.seek(self._pos)
|
||||
data = self._file.read(size)
|
||||
self._pos = self._file.tell()
|
||||
self._file.seek(0, os.SEEK_END)
|
||||
return data
|
||||
|
||||
def tell(self):
|
||||
return self._file.tell()
|
@ -0,0 +1,145 @@
|
||||
from bitstring import BitArray
|
||||
import select, msgpack
|
||||
|
||||
class ReactorException(Exception):
|
||||
pass
|
||||
|
||||
class Reactor:
|
||||
queue = []
|
||||
objmap = {}
|
||||
abort_flag = False
|
||||
|
||||
recv_size = 1024
|
||||
#chunk_size = 512 * 1024
|
||||
chunk_size = 512
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def _process_chunk(self, obj, chunktype, channel, data):
|
||||
if chunktype == 1:
|
||||
# Client message
|
||||
obj.event_receive(obj._unpack(data))
|
||||
elif chunktype == 2:
|
||||
# Datastream chunk
|
||||
obj._receive_datastream(channel, data)
|
||||
elif chunktype == 3:
|
||||
# System message
|
||||
self._process_message(obj, msgpack.unpackb(data))
|
||||
|
||||
def _process_message(self, client, data):
|
||||
if data['type'] == "datastream_finished":
|
||||
datastream_id = data['id']
|
||||
#print "Successfully finished receiving datastream %d." % datastream_id
|
||||
#print client._tempfiles[datastream_id].read()
|
||||
client.event_datastream_finished(client._tempfiles[datastream_id])
|
||||
del client._tempfiles[datastream_id]
|
||||
|
||||
def add(self, obj):
|
||||
try:
|
||||
self.queue.append(obj.stream)
|
||||
self.objmap[obj.stream.fileno()] = obj
|
||||
except AttributeError, e:
|
||||
raise ReactorException("The provided object has no valid stream attached to it.")
|
||||
|
||||
obj.reactor = self
|
||||
|
||||
def cycle(self):
|
||||
readable, writable, error = select.select(self.queue, self.queue, self.queue)
|
||||
|
||||
for stream in readable:
|
||||
fileno = stream.fileno()
|
||||
obj = self.objmap[fileno]
|
||||
|
||||
if obj.objtype == "server":
|
||||
sock, addr = obj.stream.accept()
|
||||
new_client = obj.spawn_client(sock, addr)
|
||||
self.add(new_client)
|
||||
print "Found new client from %s:%d" % addr
|
||||
elif obj.objtype == "client":
|
||||
while True:
|
||||
try:
|
||||
buff = stream.recv(self.recv_size)
|
||||
break
|
||||
except ssl.SSLError, err:
|
||||
if err.args[0] == ssl.SSL_ERROR_WANT_READ:
|
||||
select.select([sock], [], [])
|
||||
elif err.args[0] == ssl.SSL_ERROR_WANT_WRITE:
|
||||
select.select([], [sock], [])
|
||||
else:
|
||||
raise
|
||||
|
||||
if not buff:
|
||||
# The client has ceased to exist - most likely it has closed the connection.
|
||||
del self.objmap[fileno]
|
||||
self.queue.remove(stream)
|
||||
print "Client disconnected: %s:%d" % (obj.host, obj.port)
|
||||
|
||||
buff = obj._tempbuff + buff
|
||||
|
||||
while buff != b"":
|
||||
if obj._read_left > 0:
|
||||
# Continue reading a chunk in progress
|
||||
if obj._read_left < self.recv_size:
|
||||
# All the data we need is in the buffer.
|
||||
obj._read_buff += buff[:obj._read_left]
|
||||
buff = buff[obj._read_left:]
|
||||
obj._read_left = 0
|
||||
|
||||
self._process_chunk(obj, obj._chunktype, obj._channel, obj._read_buff)
|
||||
|
||||
obj._read_buff = b""
|
||||
else:
|
||||
# We need to read more data than is in the buffer.
|
||||
obj._read_buff += buff
|
||||
obj._read_left -= len(buff)
|
||||
buff = b""
|
||||
elif len(buff) >= 7:
|
||||
# Start reading a new chunk
|
||||
header = BitArray(bytes=buff[:7]) # 7 byte chunk header
|
||||
chunktype = header[:7].uint # Bits 0-6 inc
|
||||
chunksize = header[7:32].uint # Bits 7-31 inc
|
||||
channel = header[32:56].uint # Bits 32-55 inc
|
||||
|
||||
buff = buff[7:]
|
||||
obj._read_left = chunksize
|
||||
obj._chunksize = chunksize
|
||||
obj._chunktype = chunktype
|
||||
obj._channel = channel
|
||||
else:
|
||||
# We need more data to do anything meaningful
|
||||
obj._tempbuff = buff
|
||||
break
|
||||
|
||||
for stream in writable:
|
||||
fileno = stream.fileno()
|
||||
obj = self.objmap[fileno]
|
||||
|
||||
if obj.objtype == "client":
|
||||
if len(obj.sendq) > 0:
|
||||
item = obj.sendq.popleft()
|
||||
obj._send_chunk(item)
|
||||
|
||||
if len(obj._active_datastreams) > 0:
|
||||
to_delete = []
|
||||
|
||||
for datastream_id, datastream in obj._active_datastreams.iteritems():
|
||||
data = datastream.read(self.chunk_size)
|
||||
|
||||
if data:
|
||||
header = obj._encode_header(2, len(data), datastream_id)
|
||||
stream.send(header + data)
|
||||
else:
|
||||
# Done with this datastream.
|
||||
obj._send_system_message({"type": "datastream_finished", "id": datastream_id})
|
||||
to_delete.append(datastream_id)
|
||||
|
||||
for datastream_id in to_delete:
|
||||
del obj._active_datastreams[datastream_id]
|
||||
|
||||
def loop(self):
|
||||
while self.abort_flag == False:
|
||||
self.cycle()
|
||||
|
||||
def stop(self):
|
||||
self.abort_flag = True
|
@ -0,0 +1,35 @@
|
||||
import socket, ssl
|
||||
|
||||
class Server:
|
||||
reactor = None
|
||||
|
||||
def __init__(self, interface, port, client_class, use_ssl=False, **kwargs):
|
||||
self.interface = interface
|
||||
self.port = port
|
||||
self.objtype = "server"
|
||||
self.ssl = use_ssl
|
||||
self.client_class = client_class
|
||||
|
||||
if self.ssl == True and (kwargs.haskey('certfile') == False or kwargs.hasfile('keyfile') == False):
|
||||
raise Exception("SSL mode requires both a certificate and a keyfile.")
|
||||
|
||||
try:
|
||||
self.certificate = kwargs['certfile']
|
||||
self.keyfile = kwargs['keyfile']
|
||||
except KeyError, e:
|
||||
pass
|
||||
|
||||
self.stream = socket.socket()
|
||||
self.stream.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.stream.bind((self.interface, self.port))
|
||||
self.stream.listen(5)
|
||||
|
||||
def spawn_client(self, connection, source):
|
||||
if self.ssl == True:
|
||||
new_socket = ssl.wrap_socket(connection, server_side=True, certfile=self.certificate, keyfile=self.keyfile, ssl_version=ssl.PROTOCOL_TLSv1)
|
||||
else:
|
||||
new_socket = connection
|
||||
|
||||
return self.client_class(conn=connection, source=source)
|
||||
|
||||
|
@ -0,0 +1,27 @@
|
||||
import pyreactor
|
||||
|
||||
class TestClient(pyreactor.BaseClient):
|
||||
def event_receive(self, data):
|
||||
print "Received message: %s" % repr(data)
|
||||
|
||||
def event_datastream_progress(self, stream, finished_bytes, total_bytes):
|
||||
print "Completed %d of %d bytes for stream %d." % (finished_bytes, total_bytes, stream.id)
|
||||
|
||||
def event_datastream_finished(self, stream):
|
||||
print "Completed stream %d! Data follows..." % stream.id
|
||||
print "============================================"
|
||||
print stream.read()
|
||||
|
||||
print "============================================"
|
||||
print "Exiting..."
|
||||
self.reactor.stop()
|
||||
|
||||
s = pyreactor.Server("127.0.0.1", 4006, TestClient)
|
||||
c = TestClient(host="127.0.0.1", port=4006)
|
||||
|
||||
c.send({"test": "just sending some test data...", "number": 41, "file": open("test.py", "r")})
|
||||
|
||||
reactor = pyreactor.Reactor()
|
||||
reactor.add(s)
|
||||
reactor.add(c)
|
||||
reactor.loop()
|
Loading…
Reference in New Issue