commit 9f0ea0298b4df3c460ec73304fc763011ef6a67b Author: Andrea Santaniello Date: Thu Oct 31 18:31:42 2024 +0100 first push diff --git a/config/voip-italia.json b/config/voip-italia.json new file mode 100644 index 0000000..f999f9b --- /dev/null +++ b/config/voip-italia.json @@ -0,0 +1,13 @@ +{ + "server_name": "Voip-Italia", + "host": "server.voip-italia.net", + "port": 10024, + "email": "sysop@monocul.us", + "password": "AUTHME00", + "callsign": "ROBOT", + "client_type": 0, + "description": "Robot Servizio Weblist", + "country": "The Internet", + "city": "Monoculus", + "net": "Nazionale" +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..642d703 --- /dev/null +++ b/index.html @@ -0,0 +1,217 @@ + + + + + + + FRN Servers Client Status + + + + + + + +
+

FRN Servers Client Status

+
+ +
+
+
+ + + + + + diff --git a/scanner.py b/scanner.py new file mode 100644 index 0000000..5a87fea --- /dev/null +++ b/scanner.py @@ -0,0 +1,279 @@ +# _ +# | | +# _ __ ___ ___ _ __ ___ ___ _ _| | _ _ ___ +# | '_ ` _ \ / _ \| '_ \ / _ \ / __| | | | || | | / __| +# | | | | | | (_) | | | | (_) | (__| |_| | || |_| \__ \ +# |_| |_| |_|\___/|_| |_|\___/ \___|\__,_|_(_)__,_|___/ +# +# (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)