# compatibile Windows 11 # compatibile Ubuntu 24.10 # compatibile python 3.12.7 import socket import time import logging import sys import os # Import per generare ID messaggio unici from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.backends import default_backend class UDPClient: """ Un client UDP per comunicazioni criptate con un server UDP, con gestione della frammentazione. """ def __init__(self, server_host, server_port, buffer_size=1024, timeout=5, max_fragment_size=100): # AAggiunto max_fragment_size, consigliato max_fragment_size = 490 """ Inizializza l'oggetto UDPClient. """ 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 # Dimensione massima del payload per frammento # Added max_fragment_size self.socket = None self.active = False self.logger = self._setup_logger() self.partial_message = bytearray() self.on_message = None # Callback per i messaggi ricevuti, inizialmente None # Genera una coppia di chiavi RSA self.private_key = rsa.generate_private_key( public_exponent=65537, key_size=4096 ) self.public_key = self.private_key.public_key() # Serializza la chiave pubblica per poterla inviare self.public_key_pem = self.public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo ) # Memorizza la chiave pubblica del server self.server_public_key = None def _setup_logger(self): """Configura il logger.""" logger = logging.getLogger('UDPClient') logger.setLevel(logging.DEBUG) console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(logging.DEBUG) console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') console_handler.setFormatter(console_formatter) logger.addHandler(console_handler) return logger def start(self): """Avvia il client UDP.""" if self.active: self.logger.warning("Il client è già attivo.") return 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 e connesso a {self.server_host}:{self.server_port}") # Invia la chiave pubblica al server all'avvio self.send(self.public_key_pem) # Richiedi e ricevi la chiave pubblica del server self._request_server_public_key() except socket.error as e: self._handle_error(f"Errore socket durante l'avvio: {e}") self.close() except Exception as e: self._handle_error(f"Errore generico durante l'avvio: {e}") self.close() def _request_server_public_key(self): """Richiede e riceve la chiave pubblica del server.""" try: # Invia una richiesta per la chiave pubblica del server self.logger.info("Richiesta chiave pubblica al server.") self.send(b"REQ_PUB_KEY\n") # Attendi la risposta del server con la chiave pubblica data, _ = self.socket.recvfrom(self.buffer_size) if b"BEGIN PUBLIC KEY" in data and b"END PUBLIC KEY" in data: self.server_public_key = serialization.load_pem_public_key(data) self.logger.info("Chiave pubblica del server ricevuta.") else: self.logger.warning("Risposta non valida ricevuta per la chiave pubblica del server.") except socket.timeout: self.logger.error("Timeout durante l'attesa della chiave pubblica del server.") except Exception as e: self._handle_error(f"Errore durante la ricezione della chiave pubblica del server: {e}") def receive(self): """ Riceve un messaggio UDP dal server e lo decifra se necessario. """ if not self.active or not self.socket: self._handle_error("Il client non è attivo o il socket non è inizializzato.") return None try: data, addr = self.socket.recvfrom(self.buffer_size) self.logger.debug(f"Ricevuto messaggio da {addr}: {data[:50]}...") # Decifra il messaggio se è criptato if self.private_key and data.startswith(b"-----BEGIN") == False: try: decrypted_data = self.private_key.decrypt( data, padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) ) data = decrypted_data except ValueError: self.logger.debug("Impossibile decifrare il messaggio, potrebbe non essere criptato.") if self.on_message: # se c'è un callback per i messaggi self.on_message(data.decode('latin-1'), addr) # chiama il callback con self, messaggio e indirizzo else: print("MessageProcessor: Nessuna callback on_message impostata per questo messaggio.") return data.decode('latin-1') except socket.timeout: self.logger.debug("Timeout durante la ricezione del messaggio.") return None except (socket.error, ValueError) as e: self._handle_error(f"Errore durante la ricezione: {e}") return None except Exception as e: self._handle_error(f"Errore generico durante la ricezione: {e}") return None def close(self): """Chiude la connessione del client e rilascia le risorse.""" if not self.active: return self.active = False try: if self.socket: self.socket.close() self.socket = None except socket.error as e: self._handle_error(f"Errore socket durante la chiusura: {e}") except Exception as e: self._handle_error(f"Errore generico durante la chiusura: {e}") finally: self.logger.info("Client UDP arrestato.") def _encrypt_message(self, message): # Aggiunta funzione helper per la crittografia """Cripta il messaggio usando la chiave pubblica del server.""" if self.server_public_key: return self.server_public_key.encrypt( message, padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) ) else: self.logger.warning("Chiave pubblica del server non disponibile, impossibile criptare il messaggio.") return message # Restituisci messaggio non criptato def send(self, message): """ Invia un messaggio UDP criptato al server, gestendo la frammentazione se necessario. """ if not self.active: self._handle_error("Il client non è attivo, impossibile inviare il messaggio") return address = (self.server_host, self.server_port) try: if isinstance(message, str): message = message.encode('utf-8') if message == b"REQ_PUB_KEY\n" or message == self.public_key_pem: # Non criptare la richiesta della chiave e la chiave pubblica message_to_send = message elif self.server_public_key: # Cripta il messaggio se abbiamo la chiave pubblica del server self.logger.debug(f"Lunghezza messaggio prima della crittografia: {len(message)}") # Aggiunto debug if(len(message)>488): # non può essere più lungo di 488 return try: encrypted_message = self._encrypt_message(message) except ValueError as e: self.logger.error(f"Errore di crittografia (previsto se messaggio troppo lungo): {e}") # Aggiunto debug errore message_to_send = encrypted_message if len(encrypted_message) > self.max_fragment_size: # Frammentazione necessaria msg_id = int.from_bytes(os.urandom(4), byteorder='big') # Genera msg_id univoco fragments = [encrypted_message[i:i + self.max_fragment_size] for i in range(0, len(encrypted_message), self.max_fragment_size)] total_fragments = len(fragments) for i, fragment_payload in enumerate(fragments): fragment_num = i + 1 header = msg_id.to_bytes(4, byteorder='big') + fragment_num.to_bytes(1, byteorder='big') + total_fragments.to_bytes(1, byteorder='big') packet = header + fragment_payload self.socket.sendto(packet, address) time.sleep(0.01) # Opzionale: piccolo ritardo tra i frammenti self.logger.debug(f"Inviato messaggio frammentato msg_id {msg_id} in {total_fragments} frammenti a {address}") return # Ferma qui dopo aver inviato i frammenti else: pass # Invia messaggio criptato non frammentato qui sotto else: self.logger.warning(f"Chiave pubblica del server non trovata. Invio non criptato.") message_to_send = message # Invia messaggio non criptato if self.socket: self.socket.sendto(message_to_send, address) # Invia messaggio non frammentato (criptato o non criptato) self.logger.debug(f"Inviato messaggio non frammentato a {address}: {message_to_send[:50]}...") else: self._handle_error("Il socket non è inizializzato, impossibile inviare il messaggio.") except (socket.error, ValueError) as e: self._handle_error(f"Errore durante l'invio a {address}: {e}") except Exception as e: self._handle_error(f"Errore generico durante l'invio a {address}: {e}") def _handle_error(self, message): """Gestisce gli errori.""" self.logger.error(message) def split_string_into_chunks(long_string, chunk_size=400): """ Divide una stringa lunga in una lista di stringhe di lunghezza massima chunk_size. Args: long_string (str): La stringa da dividere. chunk_size (int): La lunghezza massima di ogni chunk (default: 400). Returns: list: Una lista di stringhe (chunks) di lunghezza <= chunk_size. """ chunks = [] string_length = len(long_string) start_index = 0 while start_index < string_length: end_index = min(start_index + chunk_size, string_length) # Calcola l'indice di fine, non superare la lunghezza totale chunk = long_string[start_index:end_index] chunks.append(chunk) start_index = end_index return chunks # Funzione di callback che assegniamo a on_message def print_message_callback(messaggio, indirizzo): """ Funzione di callback per gestire i messaggi ricevuti. Prende il messaggio e l'indirizzo come argomenti. """ print(f"Callback: Messaggio ricevuto: {messaggio}") print(f"Callback: Indirizzo mittente: {indirizzo}") def test_udp_server(server_host, server_port): """ Funzione per testare il server UDP. Invia alcuni messaggi di testo al server e verifica che vengano ricevuti correttamente. """ client = UDPClient(server_host, server_port) client.on_message = print_message_callback # Assegna la funzione di callback client.start() # Messaggi di test da inviare test_messages = [ "Messaggio numero 1 - piccolo", "Messaggio numero 2 - un po' più lungo", "Messaggio numero 3 - Questo è un messaggio significativamente più lungo per testare la frammentazione. Dovrebbe superare la dimensione massima del frammento e quindi essere suddiviso in più pacchetti UDP."+ "Questo un altro messaggio molto lungo per verificare ulteriormente la frammentazione e l'assemblaggio corretto dei messaggi che sono stati suddivisi in diversi frammenti UDP. Speriamo che tutto funzioni come previsto!"+ "Questo un ulteriore messaggio molto lungo per verificare ulteriormente la frammentazione e l'assemblaggio corretto dei messaggi che sono stati suddivisi in diversi frammenti UDP. Speriamo che tutto funzioni come previsto!", "Fine" # Messaggio speciale per indicare la fine del test ] try: mess = '' mess = input("premi return per continuare...") for message in test_messages: if message == "Fine": print("Client: applicativo terminato come richiesto.") break print(f"Client: Invio messaggio: {message}") chunks = split_string_into_chunks(message) for chunk in chunks: client.send(chunk) # Ricevi la risposta dal server response = client.receive() time.sleep(1) # Ridotto il tempo di attesa per test più rapidi, se necessario # Verifica se il messaggio è 'Fine' per terminare il test except KeyboardInterrupt: print("Client: applicativo interrotto dall'utente.") finally: client.close() if __name__ == "__main__": SERVER_HOST = '127.0.0.1' SERVER_PORT = 5001 # Assicurati che sia la stessa porta del server test_udp_server(SERVER_HOST, SERVER_PORT)