280 lines
9.9 KiB
Python
280 lines
9.9 KiB
Python
|
# _
|
||
|
# | |
|
||
|
# _ __ ___ ___ _ __ ___ ___ _ _| | _ _ ___
|
||
|
# | '_ ` _ \ / _ \| '_ \ / _ \ / __| | | | || | | / __|
|
||
|
# | | | | | | (_) | | | | (_) | (__| |_| | || |_| \__ \
|
||
|
# |_| |_| |_|\___/|_| |_|\___/ \___|\__,_|_(_)__,_|___/
|
||
|
#
|
||
|
# (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:<VX>2014000</VX>"
|
||
|
f"<EA>{self.email}</EA>"
|
||
|
f"<PW>{self.password}</PW>"
|
||
|
f"<ON>{self.callsign}</ON>"
|
||
|
f"<CL>0</CL>"
|
||
|
f"<BC>Crosslink</BC>"
|
||
|
f"<DS>{self.description}</DS>"
|
||
|
f"<NN>{self.country}</NN>"
|
||
|
f"<CT>{self.city}</CT>"
|
||
|
f"<NT>{self.net}</NT>\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"<root>{response}</root>"
|
||
|
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"<root>{info_line}</root>"
|
||
|
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)
|