# _
# | |
# _ __ ___ ___ _ __ ___ ___ _ _| | _ _ ___
# | '_ ` _ \ / _ \| '_ \ / _ \ / __| | | | || | | / __|
# | | | | | | (_) | | | | (_) | (__| |_| | || |_| \__ \
# |_| |_| |_|\___/|_| |_|\___/ \___|\__,_|_(_)__,_|___/
#
# (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)