first push
This commit is contained in:
commit
9f0ea0298b
13
config/voip-italia.json
Normal file
13
config/voip-italia.json
Normal 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
217
index.html
Normal 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">×</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
279
scanner.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user