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