diff --git a/TODO b/TODO index aed4a75..20cd6dd 100755 --- a/TODO +++ b/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 \ No newline at end of file diff --git a/abots/__init__.py b/abots/__init__.py new file mode 100644 index 0000000..0634d5f --- /dev/null +++ b/abots/__init__.py @@ -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 + +""" \ No newline at end of file diff --git a/abots/helpers/__init__.py b/abots/helpers/__init__.py new file mode 100644 index 0000000..2bd2a43 --- /dev/null +++ b/abots/helpers/__init__.py @@ -0,0 +1 @@ +from abots.helpers.json import jots, jsto \ No newline at end of file diff --git a/abots/helpers/json.py b/abots/helpers/json.py new file mode 100644 index 0000000..a6b25a8 --- /dev/null +++ b/abots/helpers/json.py @@ -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 \ No newline at end of file diff --git a/abots/net/__init__.py b/abots/net/__init__.py index 6a0af77..32a3db6 100755 --- a/abots/net/__init__.py +++ b/abots/net/__init__.py @@ -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 \ No newline at end of file +from abots.net.socket_server_handler import SocketServerHandler +from abots.net.socket_client_handler import SocketClientHandler \ No newline at end of file diff --git a/abots/net/socket_client.py b/abots/net/socket_client.py index 1e4091d..a10921c 100755 --- a/abots/net/socket_client.py +++ b/abots/net/socket_client.py @@ -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: diff --git a/abots/net/socket_client_handler.py b/abots/net/socket_client_handler.py new file mode 100644 index 0000000..e69de29 diff --git a/abots/net/socket_server.py b/abots/net/socket_server.py index 0fbc39b..32fd7d8 100755 --- a/abots/net/socket_server.py +++ b/abots/net/socket_server.py @@ -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() \ No newline at end of file diff --git a/abots/net/socket_server_handler.py b/abots/net/socket_server_handler.py index 823897b..6bee69f 100755 --- a/abots/net/socket_server_handler.py +++ b/abots/net/socket_server_handler.py @@ -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 \ No newline at end of file + + # 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 \ No newline at end of file diff --git a/abots/ui/__init__.py b/abots/ui/__init__.py new file mode 100644 index 0000000..d7751fb --- /dev/null +++ b/abots/ui/__init__.py @@ -0,0 +1 @@ +from abots.ui.tui import TUI \ No newline at end of file diff --git a/abots/ui/tui.py b/abots/ui/tui.py new file mode 100644 index 0000000..03622a9 --- /dev/null +++ b/abots/ui/tui.py @@ -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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1eaa2bc..9b54ff9 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ pytest python-gnupg cryptography +flask +websockets \ No newline at end of file