forked from aewens/abots
Added UI and helper sets, fixed bugs in net socket server, added better comments
This commit is contained in:
parent
54f4eda307
commit
059187881e
48
TODO
48
TODO
|
@ -2,13 +2,16 @@
|
|||
|
||||
### socket_server
|
||||
|
||||
- [ ] Add comments
|
||||
- [ ] Abstract default handler out to another file
|
||||
- [ ] Send heartbeat
|
||||
- [ ] Better handle clients disconnecting
|
||||
- [ ] Remove lookup, use clients instead
|
||||
- [ ] Add alias system for clients to replace using fd
|
||||
- [x] Add comments
|
||||
- [x] Abstract default handler out to another file
|
||||
- [x] Send heartbeat
|
||||
- [x] Better handle clients disconnecting
|
||||
- [x] Remove lookup, use clients instead
|
||||
- [x] Add alias system for clients to replace using fd
|
||||
- [ ] Add support for cryptography
|
||||
- [ ] Add data retention verbs to default handler
|
||||
- [x] Abstract length prefix to message into handler
|
||||
- [x] Fix comments to be more helpful
|
||||
|
||||
### socket_client
|
||||
|
||||
|
@ -17,18 +20,43 @@
|
|||
- [ ] Respond to heartbeat
|
||||
- [ ] Respond to alias from server
|
||||
- [ ] Add support for cryptography
|
||||
- [ ] Add data retention model to default handler
|
||||
- [ ] Abstract length prefix to message into handler
|
||||
|
||||
### socket_to_websocket
|
||||
|
||||
- [ ] Bridges socket_client and websocket_client together
|
||||
|
||||
## crypto
|
||||
|
||||
- [ ] Add crypto set
|
||||
- [ ] In crypto add GPG, Diffie-Hellman, and symmetric & asymmetric crypto
|
||||
- [ ] Add GPG wrapper functions
|
||||
- [ ] Add Diffie-Hellman functions
|
||||
- [ ] Add symmetric & asymmetric crypto functions
|
||||
|
||||
## helpers
|
||||
|
||||
- [ ] Add helpers set
|
||||
- [ ] In helpers add JSON encoding / decoding
|
||||
- [x] Add helpers set
|
||||
- [x] In helpers add JSON encoding / decoding
|
||||
- [ ] Add helper for generating / reading Twitter's snowflake ID format
|
||||
- [ ] Add helpers for running shell commands and getting outputs
|
||||
- [ ] Add wrapper functions to reading / using git repositories
|
||||
|
||||
## db
|
||||
|
||||
- [ ] Add db set
|
||||
- [ ] In db add sqlite wrappers
|
||||
- [ ] In db add sqlite wrappers to create, modify, and delete tables
|
||||
- [ ] In db add sqlite wrappers to query, add, edit, and remove entries
|
||||
|
||||
## ui
|
||||
|
||||
- [x] Add ui set
|
||||
- [ ] Add framework for curses module
|
||||
|
||||
|
||||
## web
|
||||
|
||||
- [ ] Add web set
|
||||
- [ ] Add websocket server compatible with socket_server handlers
|
||||
- [ ] Add websocket client compatible with socket_client handlers
|
||||
- [ ] Add Flask that integrates websocket server and databases from db
|
|
@ -0,0 +1,40 @@
|
|||
"""
|
||||
|
||||
ABOTS: A Bunch Of Tiny Scripts
|
||||
==============================
|
||||
|
||||
The name of this project explains what it is, a bunch of tiny scripts.
|
||||
I find myself thinking of many different projects that all require some core
|
||||
functionality that many other projects can share.
|
||||
However, it must be laid down first before adding the "unique" code that my
|
||||
ideas consist of.
|
||||
The usual approach to this issue is using an existing framework someone else
|
||||
wrote, but then you need to understand how that framework does things and fit
|
||||
your application to fit that mindset.
|
||||
As well, you now have this black box in your application that you do not 100%
|
||||
understand and adds another layer of abstraction that makes debugging issues
|
||||
that much harder (we all make bugs, so do framework devs).
|
||||
|
||||
With that being said, ideologically I do not like using existing frameworks
|
||||
since that deprives me of the opportunity to learn how that particular piece of
|
||||
software works.
|
||||
So ABOTS is my approach of making a shared library of code that I want to use
|
||||
in other projects.
|
||||
Any improvements here can then improve my other projects, as well as give me
|
||||
something small to work on when I am in-between projects that could eventually
|
||||
be useful later on.
|
||||
|
||||
The ideas of these scripts are to be as modular as possible so that they can be
|
||||
used in a variety of different projects with little changes needed.
|
||||
Due to the nature of the project, this will probably not be too useful for
|
||||
other developers who are not me, but it could be useful to see how a particular
|
||||
component of ABOTS works since the project is optimized more towards
|
||||
versitlity and simplicity than being the most efficient way of doing something
|
||||
at the expense of being harder to understand.
|
||||
|
||||
Now that you know what lies here, proceed with caution.
|
||||
You have been warned.
|
||||
|
||||
~aewens
|
||||
|
||||
"""
|
|
@ -0,0 +1 @@
|
|||
from abots.helpers.json import jots, jsto
|
|
@ -0,0 +1,22 @@
|
|||
from json import dumps, loads
|
||||
|
||||
# JSON encoder, converts a python object to a string
|
||||
def jots(self, data, readable=False):
|
||||
kwargs = dict()
|
||||
|
||||
# If readable is set, it pretty prints the JSON to be more human-readable
|
||||
if readable:
|
||||
kwargs["sort_keys"] = True
|
||||
kwargs["indent"] = 4
|
||||
kwargs["separators"] = (",", ":")
|
||||
try:
|
||||
return json.dumps(data, **kwargs)
|
||||
except ValueError as e:
|
||||
return None
|
||||
|
||||
# JSON decoder, converts a string to a python object
|
||||
def jsto(self, data):
|
||||
try:
|
||||
return json.loads(data)
|
||||
except ValueError as e:
|
||||
return None
|
|
@ -1,3 +1,4 @@
|
|||
from abots.net.socket_server import SocketServer
|
||||
from abots.net.socket_client import SocketClient
|
||||
from abots.net.socket_server_handler import SocketServerHandler
|
||||
from abots.net.socket_server_handler import SocketServerHandler
|
||||
from abots.net.socket_client_handler import SocketClientHandler
|
|
@ -1,22 +1,39 @@
|
|||
from abots.net.socket_client_handler import SocketClientHandler as handler
|
||||
|
||||
from struct import pack, unpack
|
||||
from multiprocessing import Process, Queue, JoinableQueue
|
||||
from socket import socket, AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR
|
||||
from ssl import wrap_socket
|
||||
|
||||
"""
|
||||
|
||||
Socket Client
|
||||
=============
|
||||
|
||||
|
||||
|
||||
"""
|
||||
|
||||
class SocketClient(Process):
|
||||
def __init__(self, host, port, buffer_size=4096, end_of_line="\r\n",
|
||||
inbox=JoinableQueue(), outbox=Queue(), handler=lambda x: x):
|
||||
secure=False, inbox=JoinableQueue(), outbox=Queue(), handler=handler,
|
||||
**kwargs):
|
||||
Process.__init__(self)
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.buffer_size = buffer_size
|
||||
self.end_of_line = end_of_line
|
||||
self.handler = handler
|
||||
self.sock = socket(AF_INET, SOCK_STREAM)
|
||||
self.connection = (self.host, self.port)
|
||||
self.running = True
|
||||
self.secure = secure
|
||||
self.inbox = inbox
|
||||
self.outbox = outbox
|
||||
self.handler = handler(self)
|
||||
self.sock = socket(AF_INET, SOCK_STREAM)
|
||||
if self.secure:
|
||||
self.sock = wrap_socket(self.sock, **kwargs)
|
||||
|
||||
self.connection = (self.host, self.port)
|
||||
self.running = True
|
||||
self.error = None
|
||||
|
||||
def _recv_bytes(self, get_bytes, decode=True):
|
||||
|
@ -99,7 +116,7 @@ class SocketClient(Process):
|
|||
if err is not None:
|
||||
print(err)
|
||||
return err
|
||||
print("Ready!")
|
||||
# print("Ready!")
|
||||
while self.running:
|
||||
data = self._get_message()
|
||||
if data is not None:
|
||||
|
|
|
@ -1,210 +1,362 @@
|
|||
from abots.net.socket_server_handler import SocketServerHandler
|
||||
from abots.net.socket_server_handler import SocketServerHandler as handler
|
||||
|
||||
from threading import Thread
|
||||
from struct import pack, unpack
|
||||
from select import select
|
||||
from socket import socket, AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR
|
||||
from multiprocessing import Process, Queue, JoinableQueue
|
||||
from time import time
|
||||
from ssl import wrap_socket
|
||||
|
||||
"""
|
||||
|
||||
net\SocketServer
|
||||
================
|
||||
|
||||
The intent behind this script is to provide a simple interface to start up a
|
||||
TCP socket server in the background, run each of the clients in their own
|
||||
thread, provide a simple system to handle server events, and provide simple
|
||||
functions to send/receive messages from the server.
|
||||
|
||||
"""
|
||||
|
||||
# Inherits Process so that server can be run as a daemon
|
||||
class SocketServer(Process):
|
||||
def __init__(self, host, port, listeners=5, buffer_size=4096,
|
||||
max_message_size=26214400, end_of_line="\r\n", inbox=JoinableQueue(),
|
||||
outbox=Queue(), handler=None):
|
||||
# There are many parameters here, but that is so that any constant used can
|
||||
# be easily tweaked and not remain hard-coded without an easy way to change
|
||||
def __init__(self, host, port, listeners=5, buffer_size=4096, secure=False,
|
||||
max_message_size=-1, end_of_line="\r\n", heartbeat=60,
|
||||
inbox=JoinableQueue(), outbox=Queue(), handler=handler, **kwargs):
|
||||
Process.__init__(self)
|
||||
|
||||
# The connection information for server, the clients will use this to
|
||||
# connect to the server
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
# The number of unaccepted connections that the system will allow
|
||||
# before refusing new connections
|
||||
self.listeners = listeners
|
||||
|
||||
# Size of buffer pulled by `receive_bytes` when not specified
|
||||
self.buffer_size = buffer_size
|
||||
|
||||
# If max_message_size is -1, it allows any message size
|
||||
self.max_message_size = max_message_size
|
||||
|
||||
# Which character(s) will terminate a message
|
||||
self.end_of_line = end_of_line
|
||||
|
||||
# Determines if SSL wrapper is used
|
||||
self.secure = secure
|
||||
|
||||
# How often a heartbeat will be sent to a client
|
||||
self.heartbeat = heartbeat
|
||||
|
||||
# Queues used for sending messages and receiving results using `send`
|
||||
# and `results`
|
||||
self.inbox = inbox
|
||||
self.outbox = outbox
|
||||
self.handler = SocketServerHandler if handler is None else handler
|
||||
|
||||
# An object that determines how the server reacts to events, will use
|
||||
# net\SocketServerHandler if none are specified. Use it as a model for
|
||||
# how other handlers should look / work.
|
||||
self.handler = handler(self)
|
||||
|
||||
# Sets up the socket itself
|
||||
self.sock = socket(AF_INET, SOCK_STREAM)
|
||||
if self.secure:
|
||||
# Note: kwargs is used here to specify any SSL parameters desired
|
||||
self.sock = wrap_socket(self.sock, **kwargs)
|
||||
|
||||
# Will later be set to the file descriptor of the socket on the server
|
||||
# See `_prepare`
|
||||
self.sock_fd = -1
|
||||
self.lookup = list()
|
||||
|
||||
# Will later be set to the alias used for the socket on the server
|
||||
# See `_prepare`
|
||||
self.sock_alias = None
|
||||
|
||||
# List of all sockets involved (both client and server)
|
||||
self.sockets = list()
|
||||
|
||||
# Maps metadata about the clients
|
||||
self.clients = dict()
|
||||
|
||||
# State variable for if the server is running or not. See `run`.
|
||||
self.running = True
|
||||
|
||||
def _close_sock(self, sock):
|
||||
# Sends all messages queued in inbox
|
||||
def _process_inbox(self):
|
||||
while not self.inbox.empty():
|
||||
# In the format" mode, message, args
|
||||
data = self.inbox.get()
|
||||
mode = data[0]
|
||||
# Send to one socket
|
||||
if mode == self.handler.send_verb:
|
||||
client, message, args = data[1:]
|
||||
self.send_message(message, *args)
|
||||
# Broadcast to sockets
|
||||
elif mode == self.handler.broadcast_verb:
|
||||
message, args = data[1:]
|
||||
self.broadcast_message(self.sock, message, *args)
|
||||
self.inbox.task_done()
|
||||
|
||||
# Logic for the client socket running in its own thread
|
||||
def _client_thread(self, sock, alias):
|
||||
last = time()
|
||||
client = self.clients[alias]
|
||||
while self.running:
|
||||
now = time()
|
||||
# Run heartbeat after defined time elapses
|
||||
# This will probably drift somewhat, but this is fine here
|
||||
if now - last >= self.heartbeat:
|
||||
# If the client missed last heartbeat, close client
|
||||
if not client["alive"]:
|
||||
self.handler.close_client(alias)
|
||||
break
|
||||
# The handler must set this to True, this is how a missed
|
||||
# heartbeat is checked later on
|
||||
client["alive"] = False
|
||||
last = now
|
||||
self.handler.send_heartbeat(alias)
|
||||
try:
|
||||
message = self.handler.get_message(sock)
|
||||
# The socket can either be broken or no longer open at all
|
||||
except (BrokenPipeError, OSError) as e:
|
||||
# In this case, the socket most likely died before the
|
||||
# heartbeat caught it
|
||||
self.handler.close_client(alias)
|
||||
break
|
||||
if message is None:
|
||||
continue
|
||||
# Each message returns a status code, exactly which code is
|
||||
# determined by the handler
|
||||
status = self.handler.message(sock, message)
|
||||
# Send status and message received to the outbox queue
|
||||
self.outbox.put((status, message))
|
||||
|
||||
# Prepares socket server before starting it
|
||||
def _prepare(self):
|
||||
self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||
try:
|
||||
self.sock.bind((self.host, self.port))
|
||||
# The socket can either be broken or no longer open at all
|
||||
except (BrokenPipeError, OSError) as e:
|
||||
# This usually means that the port is already in use
|
||||
return e
|
||||
self.sock.listen(self.listeners)
|
||||
|
||||
# Gets the file descriptor of the socket, which is a fallback for a
|
||||
# unique identifier for the sockets when an alias does not work
|
||||
self.sock_fd = self.sock.fileno()
|
||||
sock_address = self.sock.getsockname()
|
||||
sock_host, sock_port = sock_address
|
||||
|
||||
# This may change later, but for now aliases start at @0 and continue
|
||||
# on from there numerically
|
||||
self.sock_alias = "@{}".format(len(self.sockets))
|
||||
self.sockets.append(self.sock)
|
||||
|
||||
# Set metadata about the socket server, the fd and alias are both set
|
||||
# here to make obtaining the other metadata possible with less lookups
|
||||
self.clients[self.sock_fd] = dict()
|
||||
self.clients[self.sock_fd]["fd"] = self.sock_fd
|
||||
self.clients[self.sock_fd]["host"] = sock_host
|
||||
self.clients[self.sock_fd]["port"] = sock_port
|
||||
self.clients[self.sock_fd]["sock"] = self.sock
|
||||
self.clients[self.sock_fd]["alias"] = self.sock_alias
|
||||
|
||||
# Here the alias is just a pointer to the same data, or at least acts
|
||||
# like a pointer given how Python handles dictionaries referencing the
|
||||
# same data
|
||||
self.clients[self.sock_alias] = self.clients[self.sock_fd]
|
||||
return None
|
||||
|
||||
# Closes a connected socket and removes it from the server metadata
|
||||
def close_sock(self, alias):
|
||||
client = self.clients.get(alias, None)
|
||||
if client is None:
|
||||
return None
|
||||
sock = client["sock"]
|
||||
fd = client["fd"]
|
||||
self.sockets.remove(sock)
|
||||
fd = self._get_client_fd(sock)
|
||||
if fd is not None:
|
||||
# While the alias is a pointer, you need to delete both
|
||||
# individually to truly remove the socket from `clients`
|
||||
del self.clients[fd]
|
||||
del self.clients[alias]
|
||||
sock.close()
|
||||
|
||||
def _recv_bytes(self, sock, get_bytes, decode=True):
|
||||
# Receives specified number of bytes from a socket
|
||||
# sock - one of the sockets in sockets
|
||||
# get_bytes - number of bytes to receive from socket
|
||||
# decode - flag if the returned data is binary-to-string decoded
|
||||
def receive_bytes(self, sock, get_bytes, decode=True):
|
||||
data = "".encode()
|
||||
eol = self.end_of_line.encode()
|
||||
if get_bytes > self.max_message_size:
|
||||
# Auto-fail if requested bytes is greater than allowed by server
|
||||
if self.max_message_size > 0 and get_bytes > self.max_message_size:
|
||||
return None
|
||||
attempts = 0
|
||||
while len(data) < get_bytes:
|
||||
if attempts > self.max_message_size / self.buffer_size:
|
||||
break
|
||||
# Automatically break loop to prevent infinite loop
|
||||
if self.max_message_size > 0:
|
||||
if attempts > self.max_message_size / self.buffer_size:
|
||||
break
|
||||
else:
|
||||
attempts = attempts + 1
|
||||
else:
|
||||
attempts = attempts + 1
|
||||
# With max_message_size not set, allow at least twice the
|
||||
# needed iterations to occur before breaking loop
|
||||
if attempts > 2 * (get_bytes / self.buffer_size):
|
||||
break
|
||||
else:
|
||||
attempts = attempts + 1
|
||||
bufsize = get_bytes - len(data)
|
||||
|
||||
# Force bufsize to cap out at buffer_size
|
||||
if bufsize > self.buffer_size:
|
||||
bufsize = self.buffer_size
|
||||
try:
|
||||
packet = sock.recv(bufsize)
|
||||
except OSError:
|
||||
# The socket can either be broken or no longer open at all
|
||||
except (BrokenPipeError, OSError) as e:
|
||||
return None
|
||||
length = len(data) + len(packet)
|
||||
checker = packet if length < get_bytes else packet[:-2]
|
||||
|
||||
# Automatically stop reading message if EOL character sent
|
||||
if eol in checker:
|
||||
packet = packet.split(eol)[0] + eol
|
||||
return data + packet
|
||||
data = data + packet
|
||||
return data.decode() if decode else data
|
||||
|
||||
def _package_message(self, message, *args):
|
||||
formatted = None
|
||||
if len(args) > 0:
|
||||
formatted = message.format(*args) + self.end_of_line
|
||||
else:
|
||||
formatted = message + self.end_of_line
|
||||
packaged = pack(">I", len(formatted)) + formatted.encode()
|
||||
return packaged
|
||||
|
||||
def _get_message_size(self, sock):
|
||||
raw_message_size = self._recv_bytes(sock, 4, False)
|
||||
if not raw_message_size:
|
||||
return None
|
||||
message_size = unpack(">I", raw_message_size)[0]
|
||||
return message_size
|
||||
|
||||
def _get_message(self, sock):
|
||||
message_size = self._get_message_size(sock)
|
||||
if message_size is None:
|
||||
return None
|
||||
elif message_size > self.max_message_size:
|
||||
return None
|
||||
# Packages a message and sends it to socket
|
||||
def send_message(self, sock, message, *args):
|
||||
formatted = self.handler.format_message(message, *args)
|
||||
try:
|
||||
return self._recv_bytes(sock, message_size).strip(self.end_of_line)
|
||||
except OSError:
|
||||
self._close_sock(sock)
|
||||
return None
|
||||
sock.send(formatted)
|
||||
# The socket can either be broken or no longer open at all
|
||||
except (BrokenPipeError, OSError) as e:
|
||||
alias = self.get_client_alias_by_sock(sock)
|
||||
if alias is not None:
|
||||
self.close_sock(alias)
|
||||
|
||||
def _send_message(self, sock, message, *args):
|
||||
packaged = self._package_message(message, *args)
|
||||
try:
|
||||
sock.send(packaged)
|
||||
except BrokenPipeError:
|
||||
self._close_sock(sock)
|
||||
except OSError:
|
||||
self._close_sock(sock)
|
||||
|
||||
def _broadcast_message(self, client_sock, client_message, *args):
|
||||
# Like send_message, but sends to all sockets but the server and the sender
|
||||
def broadcast_message(self, client_sock, client_message, *args):
|
||||
for sock in self.sockets:
|
||||
not_server = sock != self.sock
|
||||
not_client = sock != client_sock
|
||||
if not_server and not_client:
|
||||
self._send_message(sock, client_message, *args)
|
||||
self.send_message(sock, client_message, *args)
|
||||
|
||||
def _get_client_fd(self, client_sock):
|
||||
# Obtains file descriptor of the socket
|
||||
def get_client_fd(self, client_sock):
|
||||
try:
|
||||
# First, try the easy route of just pulling it directly
|
||||
return client_sock.fileno()
|
||||
except OSError:
|
||||
for fd, sock in self.lookup:
|
||||
# The socket can either be broken or no longer open at all
|
||||
except (BrokenPipeError, OSError) as e:
|
||||
# Otherwise, the socket is probably dead and we can try finding it
|
||||
# using brute-force. This sometimes works
|
||||
for fd, sock in self.sockets:
|
||||
if sock != client_sock:
|
||||
continue
|
||||
return fd
|
||||
# If the brute-force option does not work, I cannot think of a good
|
||||
# way to get the fd aside from passing it along everywhere that
|
||||
# sock is also used, which would be extremely tedios. However, if
|
||||
# you have the alias you can skip this entirely and just pull the
|
||||
# fd from `clients` using the alias
|
||||
return None
|
||||
|
||||
def _process_inbox(self):
|
||||
while not self.inbox.empty():
|
||||
data = self.inbox.get()
|
||||
mode = data[0]
|
||||
if mode == "SEND":
|
||||
client, message, args = data[1:]
|
||||
self._send_message(message, *args)
|
||||
elif mode == "BCAST":
|
||||
message, args = data[1:]
|
||||
self._broadcast_message(self.sock, message, *args)
|
||||
self.inbox.task_done()
|
||||
|
||||
def _client_thread(self, sock):
|
||||
while self.running:
|
||||
message = self._get_message(sock)
|
||||
if message is None:
|
||||
continue
|
||||
status = self.handler(self, sock, message)
|
||||
self.outbox.put((status, message))
|
||||
|
||||
def _prepare(self):
|
||||
self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||
try:
|
||||
self.sock.bind((self.host, self.port))
|
||||
except OSError as e:
|
||||
return e
|
||||
self.sock.listen(self.listeners)
|
||||
self.sock_fd = self.sock.fileno()
|
||||
sock_address = self.sock.getsockname()
|
||||
sock_host, sock_port = sock_address
|
||||
self.lookup.append((self.sock_fd, sock_address))
|
||||
self.sockets.append(self.sock)
|
||||
self.clients[self.sock_fd] = dict()
|
||||
self.clients[self.sock_fd]["host"] = sock_host
|
||||
self.clients[self.sock_fd]["port"] = sock_port
|
||||
self.clients[self.sock_fd]["sock"] = self.sock
|
||||
return None
|
||||
# I realize the function name here is long, but for the few times I use
|
||||
# this it makes it clear exactly what magic is going on
|
||||
def get_client_alias_by_sock(self, client_sock):
|
||||
client_fd = self.get_client_fd(client_sock)
|
||||
if client_fd is None:
|
||||
return None
|
||||
return self.clients.get(client_fd, dict()).get("alias", None)
|
||||
|
||||
# Externally called function to send a message to a client
|
||||
def send(self, client, message, *args):
|
||||
self.inbox.put(("SEND", client, message, args))
|
||||
# This queue will be read by `_process_inbox` during the next loop
|
||||
self.inbox.put((self.handler.send_verb, client, message, args))
|
||||
|
||||
# Externally called function to broadcast a message to all clients
|
||||
def broadcast(self, message, *args):
|
||||
self.inbox.put(("BCAST", message, args))
|
||||
# This queue will be read by `_process_inbox` during the next loop
|
||||
self.inbox.put((self.handler.broadcast_verb, message, args))
|
||||
|
||||
def results(self):
|
||||
# Externally called function to iterates over the outbox queue and returns
|
||||
# them as a list in FIFO order
|
||||
def results(self, remove_status=False):
|
||||
messages = list()
|
||||
while not self.outbox.empty():
|
||||
messages.append(self.outbox.get())
|
||||
result = self.outbox.get()
|
||||
# For when you do not care about the status codes
|
||||
if remove_status:
|
||||
status, message = result
|
||||
messages.append(message)
|
||||
else:
|
||||
messages.append(result)
|
||||
return messages
|
||||
|
||||
# The Process function for running the socket server logic loop
|
||||
def run(self):
|
||||
err = self._prepare()
|
||||
if err is not None:
|
||||
print(err)
|
||||
return err
|
||||
print("Server ready!")
|
||||
# print("Server ready!")
|
||||
while self.running:
|
||||
# try:
|
||||
# selection = select(self.sockets, list(), list(), 5)
|
||||
# read_socks, write_socks, err_socks = selection
|
||||
# except OSError as e:
|
||||
# print("Error", e)
|
||||
# continue
|
||||
# for sock in read_socks:
|
||||
# if sock == self.sock:
|
||||
try:
|
||||
# Accept new socket client
|
||||
client_sock, client_address = self.sock.accept()
|
||||
client_sock.settimeout(60)
|
||||
except OSError:
|
||||
# The socket can either be broken or no longer open at all
|
||||
except (BrokenPipeError, OSError) as e:
|
||||
continue
|
||||
|
||||
# Collect the metadata of the client socket
|
||||
client_name = "{}:{}".format(*client_address)
|
||||
client_host, client_port = client_address
|
||||
client_fd = client_sock.fileno()
|
||||
self.lookup.append((client_fd, client_sock))
|
||||
client_alias = "@{}".format(len(self.sockets))
|
||||
|
||||
# Define metadata for client
|
||||
self.sockets.append(client_sock)
|
||||
self.clients[client_fd] = dict()
|
||||
self.clients[client_fd]["fd"] = client_fd
|
||||
self.clients[client_fd]["host"] = client_host
|
||||
self.clients[client_fd]["port"] = client_port
|
||||
self.clients[client_fd]["sock"] = client_sock
|
||||
joined = "ENTER {}".format(client_name)
|
||||
self.outbox.put((1, joined))
|
||||
self._broadcast_message(client_sock, joined)
|
||||
Thread(target=self._client_thread, args=(client_sock,)).start()
|
||||
# else:
|
||||
# message = self._get_message(sock)
|
||||
# if message is None:
|
||||
# continue
|
||||
# status = self.handler(self, sock, message)
|
||||
# self.outbox.put((status, message))
|
||||
self.clients[client_fd]["alias"] = client_alias
|
||||
self.clients[client_fd]["alive"] = True
|
||||
|
||||
# The alias is just a key that points to the same metadata
|
||||
self.clients[client_alias] = self.clients[client_fd]
|
||||
|
||||
# Have handler process new client event
|
||||
status = self.handler.open_client(client_alias)
|
||||
|
||||
# Send status and message received to the outbox queue
|
||||
self.outbox.put((status, message))
|
||||
|
||||
# Spawn new thread for client
|
||||
args = (client_sock, client_alias)
|
||||
Thread(target=self._client_thread, args=args).start()
|
||||
|
||||
# Process messages waiting in inbox queue
|
||||
# This is done at the end in case for some weird reason a message
|
||||
# is sent to the new client in the middle of processing this data
|
||||
# it eliminates the chance of a race condition.
|
||||
self._process_inbox()
|
||||
|
||||
# Stop the socket server
|
||||
def stop(self):
|
||||
self.handler.close_server()
|
||||
self.running = False
|
||||
self.sock.close()
|
|
@ -1,43 +1,166 @@
|
|||
def SocketServerHandler(server, sock, message):
|
||||
print("RAW:", message)
|
||||
if message == "STOP":
|
||||
server._broadcast_message(server.sock, "STOP")
|
||||
server.stop()
|
||||
"""
|
||||
|
||||
Socket Server Handlers
|
||||
======================
|
||||
|
||||
The socket server was made to be versitile where the handler can be swapped out
|
||||
in favor of another handler. This handler is the default provided if one is not
|
||||
passed to get the basic client-server relationship working for either starting
|
||||
with something simple, testing, and/or providing a template to build other
|
||||
handlers from.
|
||||
|
||||
"""
|
||||
|
||||
class SocketServerHandler:
|
||||
def __init__(self, server):
|
||||
self.server = server
|
||||
|
||||
# These are used when processing inbox messages
|
||||
self.send_verb = "SEND"
|
||||
self.broadcast_verb = "CAST"
|
||||
|
||||
self.close_verb = "STOP"
|
||||
|
||||
# Tells all clients that a node joined the socket server
|
||||
def open_client(self, alias):
|
||||
alias = message[5:]
|
||||
client = self.server.clients.get(alias, None)
|
||||
if client is None:
|
||||
return 1
|
||||
self.server.broadcast_message(client, message)
|
||||
return 0
|
||||
|
||||
# Informs the other clients a client left and closes that client's socket
|
||||
def close_client(self, alias):
|
||||
client = self.server.clients.get(alias, None)
|
||||
if client is None:
|
||||
return 1
|
||||
message = "LEFT {}".format(alias)
|
||||
self.server.broadcast_message(self.server.sock, message)
|
||||
self.server.close_sock(alias)
|
||||
return 0
|
||||
|
||||
# Lets the clients know the server is intentionally closing
|
||||
def close_server(self):
|
||||
self.server.broadcast_message(self.server.sock, self.close_verb)
|
||||
return -1
|
||||
if message == "QUIT":
|
||||
client_fd = server._get_client_fd(sock)
|
||||
if client_fd is None:
|
||||
|
||||
# Sends a heartbeat to the client to detect if it is still responding
|
||||
def send_heartbeat(self, alias):
|
||||
client = self.server.clients.get(alias, None)
|
||||
if client is None:
|
||||
return 1
|
||||
sock = client.get("sock", None)
|
||||
if sock is None:
|
||||
return 1
|
||||
self.server.send_message(sock, "PING")
|
||||
return 0
|
||||
|
||||
# Format a message before sending to client(s)
|
||||
# Prepends message size code along with replacing variables in message
|
||||
def format_message(self, message, *args):
|
||||
formatted = None
|
||||
if len(args) > 0:
|
||||
formatted = message.format(*args) + self.server.end_of_line
|
||||
else:
|
||||
formatted = message + self.server.end_of_line
|
||||
|
||||
# Puts message size at the front of the message
|
||||
prefixed = pack(">I", len(formatted)) + formatted.encode()
|
||||
return prefixed
|
||||
|
||||
# Get message from socket with `format_message` in mind
|
||||
def get_message(self, sock):
|
||||
raw_message_size = self.server.receive_bytes(sock, 4, False)
|
||||
if raw_message_size is None:
|
||||
return None
|
||||
message_size = unpack(">I", raw_message_size)[0]
|
||||
if self.max_message_size > 0 and message_size > self.max_message_size:
|
||||
return None
|
||||
eol = self.server.end_of_line
|
||||
return self.server.receive_bytes(sock, message_size).strip(eol)
|
||||
|
||||
# Takes the server object, the client socket, and a message to process
|
||||
# Each message returns a status code:
|
||||
# -1 : Going offline
|
||||
# 0 : Success
|
||||
# 1 : Failure
|
||||
# 2 : Invalid
|
||||
def message(self, sock, message):
|
||||
# print("DEBUG:", message)
|
||||
|
||||
send = self.send_verb + " "
|
||||
cast = self.broadcast_verb + " "
|
||||
send_size = len(send)
|
||||
cast_size = len(cast)
|
||||
|
||||
# React to heartbeat from client
|
||||
if message == "PONG":
|
||||
client_fd = self.server.get_client_fd(sock)
|
||||
client = self.server.clients.get(client_fd, dict())
|
||||
client_alive = client.get("alive", None)
|
||||
if client_aliave is None:
|
||||
return 1
|
||||
elif client_alive:
|
||||
return 1
|
||||
# Setting this to True is what tells the server the heartbeat worked
|
||||
client["alive"] = True
|
||||
return 0
|
||||
client_address = [a for fd, a in server.lookup if fd == client_fd][0]
|
||||
client_name = "{}:{}".format(*client_address)
|
||||
server._broadcast_message(server.sock, "LEAVE {}".format(client_name))
|
||||
server._close_sock(sock)
|
||||
return 1
|
||||
elif message == "LIST":
|
||||
fds = list() #list(map(str, server.clients.keys()))
|
||||
client_fd = server._get_client_fd(sock)
|
||||
for fd in server.clients.keys():
|
||||
if fd == server.sock_fd:
|
||||
fds.append("*{}".format(fd))
|
||||
elif fd == client_fd:
|
||||
fds.append("+{}".format(fd))
|
||||
else:
|
||||
fds.append(str(fd))
|
||||
server._send_message(sock, ",".join(fds))
|
||||
return 1
|
||||
elif message[:5] == "SEND ":
|
||||
params = message[5:].split(" ", 1)
|
||||
if len(params) < 2:
|
||||
|
||||
# Tell the clients to stop before server itself stops
|
||||
elif message == self.close_verb:
|
||||
status = self.close_server()
|
||||
self.server.stop()
|
||||
return status
|
||||
|
||||
# Informs the other clients one left and closes that client's socket
|
||||
elif message == "QUIT":
|
||||
client_alias = self.server.get_client_alias_by_sock(sock)
|
||||
if client_alias is None:
|
||||
return 1
|
||||
return self.close_client(client_alias)
|
||||
|
||||
# Lists all client alises, puts itself first and the server second
|
||||
elif message == "LIST":
|
||||
aliases = list()
|
||||
client_alias = self.server.get_client_alias_by_sock(sock)
|
||||
if client_alias is None:
|
||||
return 1
|
||||
self.server_alias = self.server.sock_alias
|
||||
for alias in self.server.clients.keys():
|
||||
# We need to skip ints since the file descriptors are also keys
|
||||
if type(alias) is int:
|
||||
continue
|
||||
# The server and sending client are skipped to retain ordering
|
||||
elif alias == self.server.sock_alias:
|
||||
continue
|
||||
elif alias == client_alias:
|
||||
continue
|
||||
else:
|
||||
aliases.append(alias)
|
||||
listed = ",".join([client_alias, self.server_alias] + aliases)
|
||||
self.server.send_message(sock, listed)
|
||||
return 0
|
||||
fd, response = params
|
||||
client_sock = server.clients.get(int(fd), dict()).get("sock", None)
|
||||
if client_sock is None:
|
||||
|
||||
# Sends a message to the client with the specified client (via an alias)
|
||||
elif message[:send_size] == send:
|
||||
params = message[(send_size + 1):].split(" ", 1)
|
||||
if len(params) < 2:
|
||||
return 1
|
||||
alias, response = params
|
||||
client = self.server.clients.get(alias, dict())
|
||||
client_sock = client.get("sock", None)
|
||||
if client_sock is None:
|
||||
return 1
|
||||
self.server.send_message(client_sock, response)
|
||||
return 0
|
||||
server._send_message(client_sock, response)
|
||||
return 1
|
||||
elif message[:6] == "BCAST ":
|
||||
response = message[6:]
|
||||
server._broadcast_message(sock, response)
|
||||
return 1
|
||||
else:
|
||||
return 2
|
||||
|
||||
# Broadcasts a message to all other clients
|
||||
elif message[:cast_size] == cast:
|
||||
response = message[(cast_size + 1):]
|
||||
self.server.broadcast_message(sock, response)
|
||||
return 0
|
||||
|
||||
# All other commands are invalid
|
||||
else:
|
||||
return 2
|
|
@ -0,0 +1 @@
|
|||
from abots.ui.tui import TUI
|
|
@ -0,0 +1,18 @@
|
|||
from curses import initscr, noecho, cbreak
|
||||
|
||||
"""
|
||||
|
||||
ui\TUI: Text User Interface
|
||||
===========================
|
||||
|
||||
The curses library is one of those things I always wanted to use, but never got
|
||||
around to it.
|
||||
That ends here, as this script will try to take curses and abstract it into a
|
||||
nice framework I can re-use without needing to pull out a manual for curses to
|
||||
figure out exactly what everything does (which is what I will be doing during
|
||||
the duration of writing this script).
|
||||
|
||||
"""
|
||||
|
||||
class TUI:
|
||||
pass
|
|
@ -1,3 +1,5 @@
|
|||
pytest
|
||||
python-gnupg
|
||||
cryptography
|
||||
flask
|
||||
websockets
|
Loading…
Reference in New Issue