feat(server.py): add socket-based communication

This commit is contained in:
2025-08-28 20:04:32 +02:00
parent 4e16a70174
commit 639e1f73a0

View File

@@ -1,54 +1,181 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import annotations from __future__ import annotations
from typing import Any
import logging as lg import logging as lg
import random as rn
import threading import threading
import time import time
import selectors
import socket
import types
from typing import Any
from judas_protocol import Message
from judas_server.backend.client import Client
class BackendServer: class BackendServer:
def __init__(self) -> None: def __init__(self, host: str = "0.0.0.0", port: int = 3692) -> None:
"""Initialize the backend server.
Args:
host (str): The host IP address to bind the server to.
port (int): The port number to bind the server to.
"""
self.logger: lg.Logger = lg.getLogger( self.logger: lg.Logger = lg.getLogger(
f"{__name__}.{self.__class__.__name__}" f"{__name__}.{self.__class__.__name__}"
) )
self.logger.debug("Initializing Server...") self.logger.debug("Initializing Server...")
# TODO: add socket logic here self.selector = selectors.DefaultSelector()
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._bind_socket(host, port)
self.server_socket.listen()
self.server_socket.setblocking(False)
self.selector.register(
self.server_socket, selectors.EVENT_READ, data=None
)
self.clients: dict[str, dict[str, dict[str, Any]]] = { self.clients: dict[str, Client] = {}
"C_01": {
"one_time": {
"hostname": "mock-host",
"platform": "windows 11",
"cpu_info": "i7",
},
"polled": {"cpu_usage": 0, "ram_usage": 0},
"ondemand": {},
}
}
self.running: bool = False self.running: bool = False
def _bind_socket(self, host: str, port: int) -> None:
"""Bind the server socket to the specified host and port.
Args:
host (str): The host IP address to bind the server to.
port (int): The port number to bind the server to.
"""
self.logger.debug(f"Binding socket to {host}:{port}")
while True:
try:
self.server_socket.bind((host, port))
self.logger.debug(f"Socket bound to {host}:{port}")
break
except OSError as e:
self.logger.error(
f"Failed to bind socket to {host}:{port}, retrying...: {e}"
)
time.sleep(1)
def _accept_connection(self, sock: socket.socket) -> None:
"""Accept a new client connection.
Args:
sock (socket.socket): The selected socket.
"""
conn, addr = sock.accept()
self.logger.info(f"[+] Accepted connection from {addr}")
conn.setblocking(False)
# wait for hello message to get mac_id
conn.settimeout(5)
message = conn.recv(1024)
conn.settimeout(None)
message = Message.from_bytes(message)
mac_id = message.payload.get("mac", None)
if mac_id is None:
self.logger.error(
f"[-] No mac_id provided by {addr}, closing connection"
)
conn.close()
return
client = Client(id_=mac_id, addr=addr, socket=conn)
self.clients[mac_id] = client
events = selectors.EVENT_READ | selectors.EVENT_WRITE
self.selector.register(conn, events, data=client)
self.logger.info(f"[+] Registered client {client}")
def _disconnect(self, client: Client) -> None:
"""Disconnect a client and clean up resources.
Args:
sock (socket.socket): The client socket to disconnect.
"""
self.logger.info(f"[-] Disconnecting {client}")
self.selector.unregister(client.socket)
client.disconnect()
def _handle_connection(
self, key: selectors.SelectorKey, mask: int
) -> None:
"""Handle a client connection.
Args:
key (selectors.SelectorKey): The selector key for the client.
mask (int): The event mask.
"""
sock: socket.socket = key.fileobj
client = key.data
try:
if mask & selectors.EVENT_READ:
recv_data = sock.recv(1024)
if recv_data:
self.logger.debug(
f"[<] Received data from {client}: {recv_data!r}"
)
client.inbound += recv_data
while b"\n" in client.inbound:
line, client.inbound = client.inbound.split(b"\n", 1)
self.logger.info(
f"[<] Complete message from {client}: {line!r}"
)
# send ACK
ack = Message.ack().to_bytes()
self.logger.debug(f"[>] Sending ACK to {client}")
client.outbound += ack
else:
self._disconnect(client)
if mask & selectors.EVENT_WRITE:
if client.outbound:
self.logger.debug(
f"[>] Sending data to {client}: {client.outbound!r}"
)
sent = sock.send(client.outbound)
client.outbound = client.outbound[sent:]
except ConnectionResetError as e:
self.logger.error(f"Connection reset by {client}, disconnect: {e}")
self._disconnect(client)
def run(self) -> None: def run(self) -> None:
"""Start the backend server."""
self.running = True self.running = True
threading.Thread( threading.Thread(
name="BackendServer thread", target=self._loop, daemon=True name="BackendServer thread", target=self._loop, daemon=True
).start() ).start()
def _loop(self) -> None: def _loop(self) -> None:
self.logger.info("Starting server loop...") """Main server loop to handle incoming connections and data."""
self.logger.info("Server is running...")
try:
while self.running: while self.running:
for client in self.clients.values(): events = self.selector.select(timeout=1)
client["polled"]["cpu_usage"] = round(rn.uniform(0, 100), 1) for key, mask in events:
client["polled"]["ram_usage"] = round(rn.uniform(0, 100), 1) if key.data is None:
time.sleep(1) self._accept_connection(key.fileobj)
else:
self._handle_connection(key, mask)
except Exception as e:
self.logger.error(f"Server error: {e}")
raise e
finally:
self.selector.close()
self.server_socket.close()
self.logger.info("Server has stopped.")
self.logger.info("Server loop stopped.") # def get_client_data(
# self, client_id: str
def get_client_data( # ) -> dict[str, dict[str, Any]] | None:
self, client_id: str # return self.clients.get(client_id, None)
) -> dict[str, dict[str, Any]] | None:
return self.clients.get(client_id, None)