# _ # | | # _ __ ___ ___ _ __ ___ ___ _ _| | _ _ ___ # | '_ ` _ \ / _ \| '_ \ / _ \ / __| | | | || | | / __| # | | | | | | (_) | | | | (_) | (__| |_| | || |_| \__ \ # |_| |_| |_|\___/|_| |_|\___/ \___|\__,_|_(_)__,_|___/ # # (c) Andrea Santaniello 31/10/24 - FRN Weblist import socket import threading import xml.etree.ElementTree as ET import cherrypy import os import json import time class FRNClient: def __init__(self, server_name, host, port, email, password, callsign, band_and_channel, description, country, city, net): self.server_name = server_name # Unique identifier for the server self.host = host self.port = port self.email = email self.password = password self.callsign = callsign self.band_and_channel = band_and_channel self.description = description self.country = country self.city = city self.net = net self.sock = None self.connected = False self.clients = [] self.lock = threading.Lock() self.stop_event = threading.Event() def connect(self): while not self.stop_event.is_set(): try: self.sock = socket.create_connection((self.host, self.port)) self.connected = True print(f"Connected to FRN server: {self.server_name}") # Send the connection string connection_string = self.build_connection_string() self.sock.sendall(connection_string.encode('ascii')) # Read server responses protocol_version = self.read_line() server_response = self.read_line() self.handle_server_response(server_response) # Send 'RX0' if connected successfully self.sock.sendall(b'RX0\r\n') # Start receiving data self.receive_loop() except Exception as e: print(f"Connection failed to {self.server_name}: {e}") self.connected = False if not self.stop_event.is_set(): print(f"Reconnecting to {self.server_name} in 5 seconds...") time.sleep(5) print(f"Stopped connection attempts to {self.server_name}") def build_connection_string(self): connection_string = ( f"CT:2014000" f"{self.email}" f"{self.password}" f"{self.callsign}" f"0" f"Crosslink" f"{self.description}" f"{self.country}" f"{self.city}" f"{self.net}\r\n" ) return connection_string def read_line(self): line = b'' while not line.endswith(b'\r\n'): data = self.sock.recv(1) if not data: break line += data return line.decode('ascii').strip() def handle_server_response(self, response): # Wrap the response in a root element to make it valid XML wrapped_response = f"{response}" try: root = ET.fromstring(wrapped_response) # Extract the elements access_level_elem = root.find('AL') if access_level_elem is not None: access_level = access_level_elem.text if access_level in ('OWNER', 'NETOWNER', 'ADMIN', 'OK'): print(f"Login successful with access level: {access_level}") else: raise Exception(f"Login failed with access level: {access_level}") else: raise Exception("Access level not found in server response.") except ET.ParseError as e: raise Exception(f"Failed to parse server response: {e}") def receive_loop(self): try: while self.connected and not self.stop_event.is_set(): try: # Send 'P' to the server self.sock.sendall(b'P\r\n') data_type = self.sock.recv(1) if not data_type: break data_type = data_type[0] if data_type == 0: # DT_IDLE time.sleep(0.1) # Sleep briefly to prevent tight loop continue elif data_type == 3: # DT_CLIENT_LIST self.handle_client_list() else: # Skip other data types for now self.skip_data(data_type) except Exception as e: print(f"Error in receive loop for {self.server_name}: {e}") self.connected = False break # Exit the loop to allow reconnection finally: self.connected = False if self.sock: self.sock.close() def handle_client_list(self): # Read active client index (2 bytes) active_client_index = self.sock.recv(2) # Read the number of clients num_clients_line = self.read_line() num_clients = int(num_clients_line) clients = [] for _ in range(num_clients): client_info_line = self.read_line() client_info = self.parse_client_info(client_info_line) clients.append(client_info) with self.lock: self.clients = clients def parse_client_info(self, info_line): # Map field names to more descriptive ones STATUS_MAP = { '0': 'Available', '1': 'Not available', '2': 'Absent' } MUTED_MAP = { '0': 'Unmuted', '1': 'Muted' } CLIENT_TYPE_MAP = { '0': 'Crosslink', '1': 'Gateway', '2': 'PC Only' } # Wrap the info line in a root element to make it valid XML wrapped_info = f"{info_line}" root = ET.fromstring(wrapped_info) client_info = {} for child in root: if child.tag == 'S': client_info['status'] = STATUS_MAP.get(child.text, 'Unknown') elif child.tag == 'M': client_info['muted'] = MUTED_MAP.get(child.text, 'Unknown') elif child.tag == 'NN': client_info['country'] = child.text elif child.tag == 'CT': client_info['city'] = child.text elif child.tag == 'BC': client_info['band_channel'] = child.text elif child.tag == 'CL': client_info['client_type'] = CLIENT_TYPE_MAP.get(child.text, 'Unknown') elif child.tag == 'ON': client_info['callsign'] = child.text elif child.tag == 'ID': client_info['id'] = child.text elif child.tag == 'DS': client_info['description'] = child.text else: client_info[child.tag] = child.text # For any other tags return client_info def skip_data(self, data_type): # Implement skipping of data for unsupported data types pass def get_clients(self): with self.lock: return self.clients.copy() def stop(self): self.stop_event.set() self.connected = False if self.sock: self.sock.close() class FRNManager: def __init__(self): self.clients = [] self.lock = threading.Lock() def add_client(self, frn_client): self.clients.append(frn_client) # Start the client's connect method in a new thread threading.Thread(target=frn_client.connect).start() def get_all_clients(self): servers = [] for client in self.clients: server_data = { 'server_name': client.server_name, 'clients': client.get_clients() } servers.append(server_data) return servers def stop_all(self): for client in self.clients: client.stop() class FRNAPI: def __init__(self, frn_manager): self.frn_manager = frn_manager @cherrypy.expose @cherrypy.tools.json_out() def clients(self): servers = self.frn_manager.get_all_clients() return {'servers': servers} @cherrypy.expose def index(self): return "FRN Client API is running. Access /clients to get the list of connected users." def run_api(frn_manager): api = FRNAPI(frn_manager) cherrypy.config.update({'server.socket_port': 8181}) cherrypy.quickstart(api) if __name__ == '__main__': frn_manager = FRNManager() config_dir = 'config' for filename in os.listdir(config_dir): if filename.endswith('.json'): config_path = os.path.join(config_dir, filename) with open(config_path, 'r') as config_file: config = json.load(config_file) frn_client = FRNClient( server_name=config['server_name'], host=config['host'], port=config['port'], email=config['email'], password=config['password'], callsign=config['callsign'], band_and_channel=config['band_and_channel'], description=config['description'], country=config['country'], city=config['city'], net=config['net'] ) frn_manager.add_client(frn_client) # Start the CherryPy REST API run_api(frn_manager)