first push

This commit is contained in:
Andrea Santaniello 2024-10-31 18:31:42 +01:00
commit 9f0ea0298b
3 changed files with 509 additions and 0 deletions

13
config/voip-italia.json Normal file
View File

@ -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"
}

217
index.html Normal file
View File

@ -0,0 +1,217 @@
<!DOCTYPE html>
<html lang="en">
<!--
# _
# | |
# _ __ ___ ___ _ __ ___ ___ _ _| | _ _ ___
# | '_ ` _ \ / _ \| '_ \ / _ \ / __| | | | || | | / __|
# | | | | | | (_) | | | | (_) | (__| |_| | || |_| \__ \
# |_| |_| |_|\___/|_| |_|\___/ \___|\__,_|_(_)__,_|___/
#
# (c) Andrea Santaniello 31/10/24 - FRN Weblist
-->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FRN Servers Client Status</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<style>
body {
background-color: #121212;
color: #e0e0e0;
}
.container-fluid {
margin-top: 20px;
}
.form-control {
background-color: #333;
color: #e0e0e0;
border: 1px solid #444;
}
.form-control::placeholder {
color: #bbb;
}
.card {
background-color: #1e1e1e;
border: 1px solid #444;
}
.card-header {
background-color: #333;
color: #e0e0e0;
}
.table {
color: #e0e0e0;
}
.table thead {
background-color: #333;
}
.status-icon {
font-size: 1.5rem;
}
.available {
color: #4caf50;
}
.unavailable {
color: #f44336;
}
.modal-body {
font-size: 1.1rem;
background-color: #1e1e1e;
color: #e0e0e0;
}
.modal-header {
background-color: #333;
color: #e0e0e0;
}
.modal-footer {
background-color: #222;
}
.modal-title {
font-weight: bold;
}
</style>
</head>
<body>
<div class="container-fluid">
<h2 class="text-center mt-4">FRN Servers Client Status</h2>
<div class="form-group">
<input type="text" id="searchInput" class="form-control" placeholder="Search clients...">
</div>
<div id="servers-container" class="client-table"></div>
</div>
<div class="modal fade" id="clientModal" tabindex="-1" role="dialog" aria-labelledby="clientModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="clientModalLabel">Client Details</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p><strong>Status:</strong> <span id="modalStatus"></span></p>
<p><strong>Muted:</strong> <span id="modalMuted"></span></p>
<p><strong>Country:</strong> <span id="modalCountry"></span></p>
<p><strong>City:</strong> <span id="modalCity"></span></p>
<p><strong>Band/Channel:</strong> <span id="modalBandChannel"></span></p>
<p><strong>Client Type:</strong> <span id="modalClientType"></span></p>
<p><strong>Callsign:</strong> <span id="modalCallsign"></span></p>
<p><strong>Description:</strong> <span id="modalDescription"></span></p>
<p><strong>ID:</strong> <pre id="modalId" style="color: #ffffff;"></pre></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script>
function updateClientStatus() {
$.get("http://localhost:8181/clients", function (data) {
renderServers(data.servers);
});
}
function renderServers(servers) {
let serversHtml = "";
servers.forEach(server => {
serversHtml += `<div class="card mb-4 server-card">
<div class="card-header">
<h5>${server.server_name}</h5>
</div>
<div class="card-body">
<table class="table table-bordered w-100">
<thead class="thead-dark">
<tr>
<th>Status</th>
<th>Muted</th>
<th>Country</th>
<th>City</th>
<th>Band/Channel</th>
<th>Client Type</th>
<th>Callsign</th>
<th>Description</th>
</tr>
</thead>
<tbody>`;
server.clients.forEach(client => {
let clientIcon = "";
if (client.client_type === "Gateway") {
clientIcon = `<i class="fa-solid fa-tower-broadcast"></i>`;
} else if (client.client_type === "Crosslink") {
clientIcon = `<i class="fa-solid fa-arrows-alt"></i>`;
} else if (client.client_type === "PC Only") {
clientIcon = `<i class="fa fa-desktop"></i>`;
}
serversHtml += `<tr class="client-row" data-client='${JSON.stringify(client)}'>
<td class="text-center">
<i class="fas fa-circle status-icon ${client.status === 'Available' ? 'available' : 'unavailable'}"></i>
</td>
<td>${client.muted}</td>
<td>${client.country}</td>
<td>${client.city}</td>
<td>${client.band_channel}</td>
<td>${clientIcon} ${client.client_type}</td>
<td>${client.callsign}</td>
<td>${client.description}</td>
</tr>`;
});
serversHtml += `</tbody>
</table>
</div>
</div>`;
});
$('#servers-container').html(serversHtml);
}
$(document).ready(function () {
updateClientStatus();
setInterval(updateClientStatus, 5000); // Update every 5 seconds
$(document).on('click', '.client-row', function () {
const client = $(this).data('client');
$('#modalStatus').text(client.status);
$('#modalMuted').text(client.muted);
$('#modalCountry').text(client.country);
$('#modalCity').text(client.city);
$('#modalBandChannel').text(client.band_channel);
$('#modalClientType').text(client.client_type);
$('#modalCallsign').text(client.callsign);
$('#modalDescription').text(client.description);
$('#modalId').text(client.id);
$('#clientModal').modal('show');
});
$('#searchInput').on('keyup', function () {
const value = $(this).val().toLowerCase();
const filteredServers = [];
$('.server-card').each(function () {
let serverVisible = false;
$(this).find('.client-row').each(function () {
const clientVisible = $(this).text().toLowerCase().indexOf(value) > -1;
$(this).toggle(clientVisible);
if (clientVisible) {
serverVisible = true;
}
});
$(this).toggle(serverVisible);
if (serverVisible) {
filteredServers.push($(this).html());
}
});
});
});
</script>
</body>
</html>

279
scanner.py Normal file
View File

@ -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:<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)