# L'eseguibile si ottiene con pyinstaller --onefile --clean --noconsole client_UDP_crypto_protocollo_TLV_csharp.py # compatibile Windows 11 # compatibile Ubuntu 24.10 # compatibile python 3.12.7 import argparse import socket import select # Necessario per il server TCP import time import logging import threading # Necessario per il server TCP import sys import os import queue # Necessario per la comunicazione inter-thread from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.backends import default_backend from cryptography.exceptions import InvalidSignature import signal # Per la gestione dei segnali # --- Componenti del Server TCP e Redirezione I/O --- class OutputQueueHandler(logging.Handler): """Un gestore di log che inserisce i record formattati in una coda.""" def __init__(self, output_queue): super().__init__() self.output_queue = output_queue def emit(self, record): try: msg = self.format(record) # --- AGGIUNGI QUESTA RIGA PER DEBUG --- #print(f"DEBUG HANDLER: Mettendo in coda: {msg}", file=sys.__stdout__) # Stampa sulla console originale # --- FINE AGGIUNTA --- self.output_queue.put(msg) except Exception: self.handleError(record) class StdoutQueueRedirector: """Un oggetto simile a un file che reindirizza le scritture stdout a una coda.""" def __init__(self, output_queue): self.output_queue = output_queue self.original_stdout = sys.stdout # Mantiene un riferimento se necessario def write(self, text): # Evita loop infiniti se queue.put causa output if text and not text.isspace(): # Non accodare righe vuote o solo spazi bianchi self.output_queue.put(text.rstrip()) # Rimuove il newline finale spesso aggiunto da print def flush(self): # Necessario per l'interfaccia file-like pass def isatty(self): # Finge di non essere un terminale return False class TCPServer(threading.Thread): """Server TCP non bloccante per gestire controllo e I/O.""" def __init__(self, host, port, output_queue, shutdown_event, command_callback): super().__init__(daemon=True, name="TCPServerThread") self.host = host self.port = port self.output_queue = output_queue self.shutdown_event = shutdown_event self.command_callback = command_callback # Callback per i comandi ricevuti self.server_socket = None self.client_socket = None self.client_address = None self._client_lock = threading.Lock() self._running = threading.Event() # Stato interno di esecuzione per il server TCP self.logger = logging.getLogger('TCPServer') # Usa la configurazione del logger principale self.on_init = None # Callback per connessione con server TCP self.connected = False def _log(self, level, message): # Helper per assicurare che i log del server TCP passino attraverso il sistema di code self.logger.log(level, message) def run(self): """Loop principale per ascoltare connessioni e gestire I/O.""" self._running.set() try: self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.server_socket.bind((self.host, self.port)) self.server_socket.listen(1) self.server_socket.settimeout(1.0) # Timeout per accept self._log(logging.INFO, f"Server TCP di Controllo in ascolto su {self.host}:{self.port}") # Avvia il thread di invio output output_thread = threading.Thread(target=self._send_output_loop, daemon=True, name="TCPOutputSender") output_thread.start() while self._running.is_set() and not self.shutdown_event.is_set(): try: conn, addr = self.server_socket.accept() with self._client_lock: if self.client_socket: self._log(logging.WARNING, f"Chiusura connessione TCP esistente da {self.client_address} per accettarne una nuova da {addr}") try: self.client_socket.sendall(b"Server Controllo: Chiusura connessione causa nuovo client.\r\n") self.client_socket.close() except socket.error: pass # Ignora errori sulla chiusura del vecchio socket self.client_socket = conn self.client_address = addr self._log(logging.INFO, f"Client TCP di Controllo connesso da {addr}") self.client_socket.sendall(b"Server Controllo: Connessione stabilita. Invia 'exit' per terminare.\r\n") # Avvia un thread per gestire la ricezione comandi da questo client recv_thread = threading.Thread(target=self._handle_client_recv, args=(conn, addr), daemon=True, name=f"TCPClientRecv-{addr}") recv_thread.start() self.on_init("connesso") except socket.timeout: continue # Riesegue il ciclo per controllare i flag running/shutdown except socket.error as e: if self._running.is_set(): # Evita log di errore durante lo shutdown self._log(logging.ERROR, f"Errore socket accept TCP: {e}") break # Esce dal ciclo su errore grave del socket except Exception as e: if self._running.is_set(): self._log(logging.ERROR, f"Errore inatteso nel loop accept TCP: {e}") break except socket.error as e: self._log(logging.ERROR, f"Errore socket Server TCP in setup: {e}") except Exception as e: self._log(logging.ERROR, f"Errore inatteso avvio Server TCP: {e}") finally: self._log(logging.INFO, "Server TCP di Controllo in fase di shutdown.") self._running.clear() # Segnala ai loop interni di fermarsi with self._client_lock: if self.client_socket: try: self.client_socket.close() except socket.error: pass self.client_socket = None if self.server_socket: self.server_socket.close() self.output_queue.put(None) # Segnala al thread di output di terminare if output_thread.is_alive(): output_thread.join(timeout=2) # Attende brevemente il thread di output def _handle_client_recv(self, client_socket, client_address): """Gestisce la ricezione dati da un client TCP connesso.""" try: client_socket.settimeout(2.0) # Controlla periodicamente buffer = bytearray() while self._running.is_set() and not self.shutdown_event.is_set(): # Controlla se questo è ancora il client attivo with self._client_lock: if self.client_socket != client_socket: self._log(logging.INFO, f"Thread ricezione TCP per {client_address} terminato (client sostituito).") break # Esce se un nuovo client si è connesso # Controlla se il socket è ancora leggibile ready_to_read, _, _ = select.select([client_socket], [], [], 0.5) # Breve poll if ready_to_read: try: data = client_socket.recv(1024) if not data: self._log(logging.INFO, f"Client TCP {client_address} disconnesso.") break # Connessione chiusa dal client buffer.extend(data) # Processa le linee (i comandi sono basati su linee) while b'\r\n' in buffer: line, buffer = buffer.split(b'\r\n', 1) command = line.decode('utf-8', errors='ignore').strip() if command: self._log(logging.DEBUG, f"TCP Ricevuto comando: '{command}' da {client_address}") self.command_callback(command) # Esegue la callback del comando except socket.timeout: continue # Nessun dato ricevuto, riesegue il ciclo except (socket.error, ConnectionResetError) as e: self._log(logging.WARNING, f"Errore socket TCP ricezione da {client_address}: {e}") break except Exception as e: self._log(logging.ERROR, f"Errore inatteso ricezione da client TCP {client_address}: {e}") break elif self.shutdown_event.is_set(): break # Esce se lo shutdown principale è segnalato finally: with self._client_lock: # Chiude solo se questo è ancora il client corrente if self.client_socket == client_socket: self._log(logging.INFO, f"Chiusura connessione TCP per {client_address}") try: client_socket.close() except socket.error: pass self.client_socket = None self.client_address = None def _send_output_loop(self): """Invia continuamente messaggi dalla coda di output al client.""" while True: try: # --- AGGIUNGI QUESTA RIGA PER DEBUG --- #print("DEBUG SENDLOOP: In attesa sulla coda...", file=sys.__stdout__) # Potrebbe essere troppo verboso # --- FINE AGGIUNTA --- message = self.output_queue.get() # Blocca finché un elemento è disponibile # --- AGGIUNGI QUESTA RIGA PER DEBUG --- #print(f"DEBUG SENDLOOP: Ottenuto dalla coda: {message}", file=sys.__stdout__) # --- FINE AGGIUNTA -- if message is None: # Segnale di shutdown self._log(logging.DEBUG, "Sender Output TCP ricevuto segnale di shutdown.") break with self._client_lock: if self.client_socket: try: # Assicura che il messaggio termini con newline per la visualizzazione client if isinstance(message, str): message = message.encode('utf-8') if not message.endswith(b'\r\n'): message += b'\r\n' self.client_socket.sendall(message) except (socket.error, BrokenPipeError, ConnectionResetError) as e: self._log(logging.WARNING, f"TCP Fallito invio a {self.client_address}: {e}. Client probabilmente disconnesso.") # Non chiudere il socket qui, lascia che il thread recv lo gestisca except Exception as e: self._log(logging.ERROR, f"Errore inatteso invio messaggio TCP: {e}") # Se nessun client è connesso, il messaggio viene scartato (o potrebbe essere bufferizzato) self.output_queue.task_done() # Marca il task come completato except Exception as e: # Logga errori nel processamento della coda stessa self._log(logging.ERROR, f"Errore nel loop output TCP: {e}") # Evita di andare in loop veloce su errore time.sleep(0.1) self._log(logging.INFO,"Thread Sender Output TCP terminato.") def stop(self): """Ferma il server TCP.""" self._log(logging.INFO, "Arresto Server TCP di Controllo...") self._running.clear() # Segnala ai loop di fermarsi # Non serve chiudere esplicitamente i socket qui, il blocco finally in run() lo gestisce # --- Classe UDPClient Modificata --- class UDPClient: def __init__(self, server_host, server_port, output_queue, shutdown_event, buffer_size=1024, timeout=5, max_fragment_size=486, max_retries=3): """ Inizializza l'oggetto UDPClient con integrazione TCP/coda. """ self.server_host = server_host self.server_port = server_port self.buffer_size = buffer_size self.timeout = timeout # self.max_fragment_size = max_fragment_size # Sarà ricalcolato self.max_retries = max_retries self.socket = None self.active = False # --- MODIFICATO --- self.output_queue = output_queue self.main_shutdown_event = shutdown_event self.logger = self._setup_logger() # Configura il logger per usare la coda # --- FINE MODIFICA --- self.partial_message = {} # Non più usato per la ricezione frammentata nel client self.on_message = None # Callback per messaggi ACK o altri dal server # Generazione chiavi RSA self.private_key = rsa.generate_private_key( public_exponent=65537, key_size=4096 ) self.public_key = self.private_key.public_key() self.public_key_pem = self.public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo ) self.server_public_key = None # Calcola dimensione massima frammento (basata sulla *nostra* chiave privata per la decrittazione dei dati ricevuti, # ma la dimensione di invio dipende dalla chiave pubblica del server) # La dimensione massima EFFETTIVA per l'invio è determinata dalla chiave del *server* dopo averla ricevuta. # Inizializziamo con la nostra, ma la ricalcoleremo. self._calculate_max_plaintext_size() # Calcola iniziale self.logger.info(f"Dimensione max iniziale plaintext (per frammento inviato): {self.max_plaintext_size}") def _setup_logger(self): """Configura il logger per inviare output alla coda.""" logger = logging.getLogger() # Prende il root logger o uno specifico # Impedisce di aggiungere handler multipli se chiamato di nuovo if not hasattr(logger, '_has_client_tcp_handlers'): logger.setLevel(logging.DEBUG) # Rimuove eventuali handler esistenti (come StreamHandler di default) for handler in logger.handlers[:]: logger.removeHandler(handler) # Aggiunge il gestore della coda queue_handler = OutputQueueHandler(self.output_queue) queue_handler.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(threadName)s - %(message)s') queue_handler.setFormatter(formatter) logger.addHandler(queue_handler) logger._has_client_tcp_handlers = True # Marca il logger come configurato # --- AGGIUNGI QUESTE RIGHE PER DEBUG --- #print(f"DEBUG: Logger '{logger.name}' configurato. Livello Logger: {logging.getLevelName(logger.level)}. Livello Handler: {logging.getLevelName(queue_handler.level)}", file=self.output_queue.original_stdout if hasattr(self.output_queue, 'original_stdout') else sys.__stdout__) # --- FINE AGGIUNTA -- return logger def _calculate_max_plaintext_size(self): """Calcola la dimensione massima del payload in chiaro per la crittografia RSA OAEP.""" # La dimensione dipende dalla chiave usata per la *crittografia*. # Quando *inviamo*, criptiamo con la chiave pubblica del server. # Quando *riceviamo*, decriptiamo con la nostra chiave privata. # La dimensione del frammento deve essere tale che *dopo* la crittografia stia nel pacchetto UDP. # MA la crittografia viene fatta *prima* di aggiungere l'header TLV. # Quindi, il limite è dato dalla capacità di RSA. key_to_check = self.server_public_key if self.server_public_key else self.public_key # Usa la chiave server se disponibile key_size_bytes = key_to_check.key_size // 8 # Formula per OAEP con SHA256: key_size_in_bytes - 2 * hash_length - 2 hash_length = hashes.SHA256.digest_size # 32 bytes self.max_plaintext_size = key_size_bytes - 2 * hash_length - 2 self.logger.info(f"Calcolata max_plaintext_size = {self.max_plaintext_size} bytes (basata su key_size={key_to_check.key_size})") def start(self): """Avvia il client UDP, scambia le chiavi.""" if self.active: self.logger.warning("Il client UDP è già attivo.") return False # Indica fallimento o già attivo if self.main_shutdown_event.is_set(): self.logger.warning("Tentativo di avviare il client UDP durante lo shutdown.") return False try: self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.socket.settimeout(self.timeout) self.active = True self.logger.info(f"Client UDP avviato, tentativo connessione a {self.server_host}:{self.server_port}") # 1. Invia la nostra chiave pubblica (non frammentata, non criptata) self.logger.info("Invio chiave pubblica al server...") key_sent = self.send_tlv(b'K', self.public_key_pem, force_no_fragment=True, require_ack=False) # Non aspettare ACK per la chiave if not key_sent: self.logger.error("Fallito invio chiave pubblica iniziale.") self.close() return False time.sleep(0.2) # Pausa per dare tempo al server di processare la chiave # 2. Richiede e riceve la chiave pubblica del server if not self._request_server_public_key(): self.logger.error("Fallito ottenimento chiave pubblica del server.") self.close() return False # Indica fallimento # Ricalcola max_plaintext_size ora che abbiamo la chiave del server self._calculate_max_plaintext_size() self.logger.info("Scambio chiavi completato. Client UDP pronto.") return True # Indica successo except socket.error as e: self._handle_error(f"Errore socket UDP durante l'avvio: {e}") self.close() return False except Exception as e: self._handle_error(f"Errore generico durante l'avvio UDP: {e}") self.close() return False def _request_server_public_key(self): """Richiede e riceve la chiave pubblica del server.""" retries = 0 while retries < self.max_retries and self.server_public_key is None and not self.main_shutdown_event.is_set(): try: self.logger.info(f"Richiesta chiave pubblica server (tentativo {retries + 1}/{self.max_retries}).") # Invia richiesta 'R' (non frammentata, non criptata) req_sent = self.send_tlv(b'R', b"REQ_PUB_KEY", force_no_fragment=True, require_ack=False) if not req_sent: self.logger.warning("Fallito invio richiesta chiave pubblica.") retries += 1 time.sleep(1) # Pausa prima del ri-tentativo continue # Attende la risposta (che dovrebbe essere 'K') response = self.receive_tlv(expected_type=b'K') # Si aspetta specificamente una chiave if response: type, value = response if type == b'K': try: self.server_public_key = serialization.load_pem_public_key(value, backend=default_backend()) self.logger.info("Chiave pubblica del server ricevuta e caricata correttamente.") return True # Successo except Exception as e: self.logger.error(f"Errore nel caricamento della chiave pubblica ricevuta: {e}") # La risposta era tipo K ma non valida, consideralo un fallimento per questo tentativo retries +=1 time.sleep(1) else: # Ricevuto qualcosa di diverso da K, strano. self.logger.warning(f"Ricevuta risposta inattesa ({type.decode()}) durante attesa chiave server.") retries += 1 time.sleep(1) # else: receive_tlv ha già loggato timeout o errore except socket.timeout: self.logger.warning(f"Timeout in attesa della chiave pubblica server (tentativo {retries + 1}).") retries += 1 except Exception as e: self._handle_error(f"Errore durante ricezione chiave server: {e}") retries += 1 time.sleep(1) # Pausa dopo errore generico # Fine tentativi if self.server_public_key is None: self.logger.error("Impossibile ottenere chiave pubblica del server dopo multipli tentativi.") return False return True # Dovrebbe essere già ritornato true sopra def receive_raw(self): """Riceve dati grezzi UDP.""" if not self.active or not self.socket or self.main_shutdown_event.is_set(): # self._handle_error("Client UDP non attivo/socket non inizializzato o in shutdown.") return None, None try: # Usa select per non bloccare indefinitamente se impostato non bloccante, # ma qui usiamo il timeout del socket. data, addr = self.socket.recvfrom(self.buffer_size) self.logger.debug(f"UDP Raw Received: {len(data)} bytes da {addr}") return data, addr except socket.timeout: self.logger.debug("Timeout ricezione UDP.") return None, None except socket.error as e: # Non loggare errori se ci stiamo fermando if not self.main_shutdown_event.is_set(): self._handle_error(f"Errore ricezione UDP: {e}") return None, None except Exception as e: if not self.main_shutdown_event.is_set(): self._handle_error(f"Errore generico ricezione UDP: {e}") return None, None def receive_tlv(self, expected_type=None): """Riceve UN messaggio TLV completo, decifra e restituisce (type, value).""" data, addr = self.receive_raw() if data is None: return None # Timeout o errore self.logger.debug(f"UDP Dati ricevuti per parsing TLV: {data[:60]}...") # --- Decifratura --- # I messaggi DAL server (es. ACK, o messaggi M se il server li inviasse) # sarebbero criptati con la NOSTRA chiave pubblica. Decifriamo con la NOSTRA privata. # La chiave pubblica del server ('K') NON è criptata. try: type_peek = data[0:1] # Guarda il tipo prima di decidere se decifrare payload_to_decrypt = data # Di default prova a decifrare tutto if type_peek == b'K': # La chiave pubblica non dovrebbe essere criptata self.logger.debug("Ricevuto tipo 'K', si presume non criptato.") decrypted_data = data elif self.private_key: try: decrypted_data = self.private_key.decrypt( payload_to_decrypt, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None) ) self.logger.debug(f"Dati decifrati con successo ({len(payload_to_decrypt)} -> {len(decrypted_data)} bytes)") except (ValueError, InvalidSignature) as e: self.logger.warning(f"Decifratura fallita (ValueError/InvalidSignature): {e}. Considero dati come non criptati.") decrypted_data = data # Usa dati originali se la decifratura fallisce (potrebbe essere ACK non criptato?) except Exception as e: self.logger.error(f"Errore imprevisto durante decifratura: {e}") return None # Errore grave decifratura else: # Nessuna chiave privata? Impossibile decifrare. Assume non criptato. self.logger.warning("Chiave privata non disponibile per decifrare.") decrypted_data = data except IndexError: self.logger.error("Dati ricevuti troppo corti per determinare il tipo.") return None # --- Estrazione TLV dai dati (potenzialmente decifrati) --- try: if len(decrypted_data) < 3: self.logger.error(f"Dati (post-decifratura) troppo corti per TLV: {len(decrypted_data)} bytes") return None type = decrypted_data[0:1] length = int.from_bytes(decrypted_data[1:3], byteorder='big') expected_total_len = 3 + length if expected_total_len > len(decrypted_data): self.logger.error(f"Lunghezza TLV ({length}) non valida per i dati disponibili ({len(decrypted_data)})") # Potrebbe essere un frammento perso? O dati corrotti. return None value = decrypted_data[3:expected_total_len] if len(decrypted_data) > expected_total_len: self.logger.warning(f"Dati extra ({len(decrypted_data) - expected_total_len} bytes) dopo il TLV ricevuto.") self.logger.debug(f"TLV Estratto: Tipo={type.decode()}, Lunghezza={length}") # Controllo tipo atteso if expected_type and type != expected_type: self.logger.warning(f"Ricevuto tipo TLV '{type.decode()}' ma atteso '{expected_type.decode()}'.") # Potremmo decidere di scartarlo o processarlo comunque # Per ora, lo restituiamo ma logghiamo l'avviso. # Esegui callback se impostata e il messaggio non è una chiave (che gestiamo internamente) if type != b'K' and self.on_message: try: # Decodifica il valore per la callback (es. ACK o messaggi M dal server) message_str = value.decode('utf-8', errors='replace') self.on_message(type, message_str, addr) # Passa tipo, messaggio stringa, indirizzo except Exception as e: self.logger.error(f"Errore durante esecuzione callback on_message: {e}") return type, value # Restituisce tipo e valore in bytes except (IndexError, ValueError) as e: self.logger.error(f"Errore nell'estrazione/parsing del TLV: {e}. Dati (post-decifratura): {decrypted_data[:60]}...") return None except Exception as e: self.logger.error(f"Errore generico in receive_tlv post-decifratura: {e}") return None def close(self): """Chiude la connessione UDP.""" if not self.active: return self.logger.info("Arresto Client UDP...") self.active = False # Non impostare main_shutdown_event qui, dovrebbe essere gestito dal chiamante principale try: if self.socket: self.socket.close() self.socket = None except socket.error as e: self._handle_error(f"Errore chiusura socket UDP: {e}") except Exception as e: self._handle_error(f"Errore generico chiusura UDP: {e}") finally: self.logger.info("Client UDP arrestato.") def _encrypt_data(self, data_bytes): """Cripta i dati usando la chiave pubblica del server.""" if not self.server_public_key: self.logger.error("Impossibile criptare: chiave pubblica del server non disponibile.") return None # Fallimento crittografia try: encrypted = self.server_public_key.encrypt( data_bytes, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None) ) return encrypted except ValueError as e: # Questo può accadere se i dati sono più grandi di quanto la chiave possa gestire self.logger.error(f"Errore di crittografia (ValueError): {e}. Dimensione dati: {len(data_bytes)}, Max per chiave: {self.max_plaintext_size}") return None except Exception as e: self.logger.error(f"Errore imprevisto durante crittografia: {e}") return None def send_tlv(self, type, value, force_no_fragment=False, require_ack=True): """Invia un messaggio TLV, gestendo frammentazione PRIMA della crittografia e ACK.""" if not self.active or self.main_shutdown_event.is_set(): self.logger.warning("Invio TLV fallito: Client UDP non attivo o in shutdown.") return False if not self.socket: self.logger.error("Invio TLV fallito: Socket non inizializzato.") return False address = (self.server_host, self.server_port) success = False try: if isinstance(value, str): value = value.encode('utf-8') # --- Costruzione Messaggio TLV (in chiaro) --- length = len(value) length_bytes = length.to_bytes(2, byteorder='big', signed=False) tlv_message_plaintext = type + length_bytes + value plaintext_len = len(tlv_message_plaintext) # --- Determina se Criptare --- should_encrypt = (type != b'K' and type != b'R') # Verifica chiave server se si deve criptare if should_encrypt and not self.server_public_key: self.logger.error(f"Impossibile inviare TLV tipo {type.decode()} criptato: chiave server mancante.") return False # --- Gestione Invio --- # Controlla se la frammentazione è necessaria BASANDOSI sulla dimensione IN CHIARO fragment_needed = False if not force_no_fragment: if should_encrypt and plaintext_len > self.max_plaintext_size: fragment_needed = True self.logger.info(f"Frammentazione necessaria per TLV tipo {type.decode()} (Dimensione plaintext: {plaintext_len} > {self.max_plaintext_size})") elif not should_encrypt and plaintext_len > 486: # Limite arbitrario per non criptati grandi # Potremmo voler frammentare anche messaggi grandi non criptati # Ma per K e R è gestito da force_no_fragment, quindi questo caso è raro fragment_needed = True self.logger.info(f"Frammentazione necessaria per TLV non criptato tipo {type.decode()} (Dimensione: {plaintext_len} > 486)") if fragment_needed: # --- Frammentazione (sul plaintext) --- msg_id = int.from_bytes(os.urandom(4), byteorder='big') | 0x80000000 # Divide il TLV PLAINTEXT in frammenti plaintext_fragments = [tlv_message_plaintext[i:i + self.max_plaintext_size] for i in range(0, len(tlv_message_plaintext), self.max_plaintext_size)] total_frags = len(plaintext_fragments) self.logger.debug(f"Divido plaintext in {total_frags} frammenti (max_size={self.max_plaintext_size}, msg_id={msg_id})") fragments_sent_ok = True for i, plaintext_frag in enumerate(plaintext_fragments): if self.main_shutdown_event.is_set(): self.logger.warning("Invio frammenti interrotto da shutdown.") fragments_sent_ok = False break frag_num = i + 1 payload_to_send = plaintext_frag # CRIPTA IL SINGOLO FRAMMENTO PLAINTEXT if should_encrypt: encrypted_frag_payload = self._encrypt_data(plaintext_frag) if encrypted_frag_payload is None: self.logger.error(f"Errore crittografia frammento {frag_num}/{total_frags}. Invio interrotto.") fragments_sent_ok = False break payload_to_send = encrypted_frag_payload self.logger.debug(f"Criptato frammento {frag_num}: {len(plaintext_frag)} bytes -> {len(payload_to_send)} bytes") # COSTRUISCI IL PACCHETTO UDP CON HEADER + PAYLOAD (criptato o no) header = msg_id.to_bytes(4, byteorder='big') + \ frag_num.to_bytes(1, byteorder='big') + \ total_frags.to_bytes(2, byteorder='big') packet = header + payload_to_send try: self.socket.sendto(packet, address) self.logger.debug(f"Inviato frammento UDP {frag_num}/{total_frags} (msg_id={msg_id}, {len(packet)} bytes)") time.sleep(0.01) # Pausa except socket.error as e: self._handle_error(f"Errore invio frammento {frag_num}: {e}") fragments_sent_ok = False break if fragments_sent_ok: self.logger.debug(f"Invio TLV frammentato (id {msg_id}) completato.") if require_ack and type == b'M': self.logger.debug(f"Attesa ACK per messaggio frammentato msg_id={msg_id}") ack_response = self.receive_tlv(expected_type=b'A') if ack_response and ack_response[0] == b'A': self.logger.info(f"ACK ricevuto per messaggio frammentato (msg_id={msg_id})") success = True else: self.logger.warning(f"ACK NON ricevuto o errato per messaggio frammentato (msg_id={msg_id})") success = False else: success = True else: success = False # Invio frammenti fallito else: # --- Non Frammentato (o Forzato Non Frammentato) --- payload_to_send = tlv_message_plaintext if should_encrypt: encrypted_tlv = self._encrypt_data(tlv_message_plaintext) if encrypted_tlv is None: return False payload_to_send = encrypted_tlv self.logger.debug(f"Criptato TLV (non frammentato): {len(tlv_message_plaintext)} -> {len(payload_to_send)} bytes") # Aggiunge header per non frammentato header = b'\x00\x00\x00\x00\x01\x00\x01' # msg_id=0, frag_num=1, total_frags=1 packet = header + payload_to_send self.socket.sendto(packet, address) log_type = "NON frammentato" if not force_no_fragment else "NON frammentato (forzato)" self.logger.debug(f"Inviato TLV {log_type} (tipo {type.decode()}): {len(packet)} bytes") # Attende ACK se richiesto (es. per tipo 'M') if require_ack and type == b'M': self.logger.debug("Attesa ACK per messaggio non frammentato...") ack_response = self.receive_tlv(expected_type=b'A') if ack_response and ack_response[0] == b'A': self.logger.info("ACK ricevuto per messaggio non frammentato.") success = True else: self.logger.warning("ACK NON ricevuto o errato per messaggio non frammentato.") success = False else: success = True # Invio completato, ACK non richiesto (o per K/R) return success except (socket.error, ValueError) as e: self._handle_error(f"Errore invio TLV (tipo {type.decode()}): {e}") return False except Exception as e: self._handle_error(f"Errore generico invio TLV (tipo {type.decode()}): {e}") return False def _handle_error(self, message): """Gestisce gli errori (ora logga tramite il sistema logger->queue).""" self.logger.error(message) # --- Logica Principale Applicazione --- # Variabili globali (usare con cautela) udp_client = None tcp_server = None original_stdout = sys.stdout main_shutdown = threading.Event() # Evento unico per coordinare lo shutdown output_queue = queue.Queue() # Coda per tutto l'output def process_tcp_command(command): """Callback per gestire i comandi ricevuti via TCP.""" global main_shutdown, udp_client # Permette modifica logger = logging.getLogger('MainClient') # Ottiene istanza logger logger.debug(f"Comando TCP ricevuto per processare: '{command}'") if not udp_client or not udp_client.active: logger.error("Impossibile processare comando: Client UDP non attivo o non inizializzato.") return if command.lower() == "exit": logger.info("Comando 'exit' ricevuto via TCP. Avvio arresto...") main_shutdown.set() # Segnala a tutti i thread di fermarsi elif command.lower() == "status": # Aggiungere qui log di stato se utile logger.info("--- STATUS CLIENT UDP ---") logger.info(f"Attivo: {udp_client.active}") logger.info(f"Connesso a: {udp_client.server_host}:{udp_client.server_port}") logger.info(f"Chiave server presente: {'Sì' if udp_client.server_public_key else 'No'}") logger.info(f"Max Plaintext Size: {udp_client.max_plaintext_size}") logger.info("------------------------") else: # Invia il comando come messaggio 'M' via UDP logger.info(f"Invio comando '{command[:50]}...' come messaggio UDP...") # Il require_ack=True in send_tlv farà sì che attenda l'ACK success = udp_client.send_tlv(b'M', command, require_ack=True) if success: logger.info(f"Messaggio '{command[:50]}...' inviato con successo (ACK ricevuto).") else: logger.error(f"Fallito invio messaggio '{command[:50]}...' o ACK non ricevuto.") def udp_message_callback(type, message, address): """Callback per messaggi UDP ricevuti dal server (es. ACK).""" logger = logging.getLogger('UDPCallback') # Il logger invierà l'output alla coda TCP logger.info(f"CALLBACK UDP: Ricevuto Tipo={type.decode()}, Msg='{message[:60]}...' da {address}") # Non fare azioni bloccanti qui dentro def tcp_init_callback(message): """Callback per connessione al server TCP""" tcp_server.connected = True pass #logger = logging.getLogger('UDPCallback') # Il logger invierà l'output alla coda TCP #logger.info(f"CALLBACK UDP: Ricevuto Tipo={type.decode()}, Msg='{message[:60]}...' da {address}") # Non fare azioni bloccanti qui dentro def signal_handler(sig, frame): """Gestisce SIGINT (Ctrl+C) per arresto pulito.""" # Usa original_stdout per questo messaggio critico iniziale print("\r\nRicevuto Segnale di Interruzione (Ctrl+C o Term). Arresto in corso...", file=original_stdout) logging.warning("Segnale di Interruzione Ricevuto! Avvio arresto...") main_shutdown.set() # Segnala shutdown # --- Esecuzione MAIN --- if __name__ == "__main__": #udp_server_host = "localhost" # Host del SERVER UDP #udp_server_port = 5001 # Porta del SERVER UDP #tcp_control_host = "0.0.0.0" # Ascolta su tutte le interfacce per il controllo TCP #tcp_control_port = 5003 # Porta per il SERVER TCP DI CONTROLLO (questo script) # Definizione Argomenti Command Line parser = argparse.ArgumentParser(description="Client UDP con protocollo TLV, crittografia RSA e controllo TCP.") parser.add_argument( '--udp-server-host', type=str, default="localhost", # Mantieni i default originali help="Indirizzo IP o hostname del server UDP remoto (default: localhost)" ) parser.add_argument( '--udp-server-port', type=int, default=5001, # Mantieni i default originali help="Porta del server UDP remoto (default: 5001)" ) parser.add_argument( '--tcp-control-port', type=int, default=5003, # Mantieni i default originali help="Porta locale su cui ascoltare per le connessioni TCP di controllo (default: 5003)" ) args = parser.parse_args() udp_server_host = args.udp_server_host udp_server_port = args.udp_server_port tcp_control_host = "0.0.0.0" tcp_control_port = args.tcp_control_port # 1. Setup evento di shutdown condiviso main_shutdown = threading.Event() # 2. Setup coda di output output_queue = queue.Queue() # 3. Redirige stdout stdout_redirector = StdoutQueueRedirector(output_queue) sys.stdout = stdout_redirector # 4. Istanzia UDP Client (passando coda ed evento) # La configurazione del logger avviene al suo interno udp_client = UDPClient(udp_server_host, udp_server_port, output_queue, main_shutdown) udp_client.on_message = udp_message_callback # Imposta callback per ACK/messaggi dal server # 5. Configura Logger (dopo istanza client) logger = logging.getLogger('MainClient') # Logger per la parte main # 6. Istanzia TCP Server (passando coda, evento e gestore comandi) tcp_server = TCPServer(tcp_control_host, tcp_control_port, output_queue, main_shutdown, process_tcp_command) tcp_server.on_init = tcp_init_callback # Imposta callback per connessione con il server # 7. Setup Gestore Segnali signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) logger.info("--- Avvio Applicazione Client ---") # 8. Avvia Client UDP e Server TCP try: tcp_server.start() # Avvia il thread del server TCP while True: if tcp_server.connected == True: break time.sleep(1) # Avvia prima il client UDP (scambio chiavi) if not udp_client.start(): logger.critical("Fallimento avvio client UDP. Impossibile continuare.") # Il cleanup avverrà nel finally, ma assicuriamoci che lo shutdown sia segnalato main_shutdown.set() # Potrebbe essere necessario attendere un po' per far svuotare la coda al logger time.sleep(1) sys.exit(1) # Esce se il client UDP non parte # Se il client UDP è partito, avvia il server TCP # tcp_server.start() # Avvia il thread del server TCP # 9. Loop Principale - Attende segnale di shutdown logger.info(f"Client UDP pronto. Server TCP di controllo in ascolto su {tcp_control_host}:{tcp_control_port}") logger.info("Connetti un client TCP per inviare comandi (es. messaggi da inviare via UDP) o 'exit' per terminare.") # Mantiene vivo il thread principale controllando l'evento while not main_shutdown.is_set(): # Controlli periodici opzionali if not tcp_server.is_alive() and tcp_server._running.is_set(): logger.error("!!! Thread principale Server TCP non più attivo ma dovrebbe esserlo!") main_shutdown.set() break main_shutdown.wait(timeout=5.0) # Attende con timeout permette controlli except Exception as e: logger.critical(f"Errore critico nel main loop del client: {e}", exc_info=True) main_shutdown.set() # Assicura lo shutdown su errore critico finally: # 10. Cleanup logger.info("--- Inizio procedura di arresto Client ---") if not main_shutdown.is_set(): main_shutdown.set() # Assicura che l'evento sia impostato logger.info("Arresto Server TCP di Controllo...") if tcp_server and tcp_server.is_alive(): tcp_server.stop() # Segnala al thread TCP di fermarsi tcp_server.join(timeout=3) # Attende il thread TCP if tcp_server.is_alive(): logger.warning("Thread Server TCP di Controllo non terminato in tempo.") logger.info("Arresto Client UDP...") if udp_client and udp_client.active: udp_client.close() # Chiude il socket UDP logger.info("Attesa svuotamento coda output...") # Dare un istante al thread di output TCP (se ancora vivo) per inviare gli ultimi messaggi time.sleep(0.5) # Potrebbe essere utile mettere un ultimo None per sicurezza se il thread non è terminato output_queue.put(None) # Ripristina stdout sys.stdout = original_stdout print("--- Arresto Client Completato ---")