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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user