# compatibile Windows 11 # compatibile Ubuntu 24.10 # compatibile python 3.12.7 import socket import time import logging import sys import os 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 class UDPClient: def __init__(self, server_host, server_port, buffer_size=1024, timeout=5, max_fragment_size=486, max_retries=3): # max_fragment_size è un valore *iniziale*, verrà ricalcolato """ 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 # Questo valore verrà sovrascritto self.max_retries = max_retries self.socket = None self.active = False self.logger = self._setup_logger() self.partial_message = {} # Non serve più self.on_message = 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 self.public_key_pem = self.public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo ) self.server_public_key = None # Calcola la dimensione massima del frammento *PRIMA* della crittografia self.max_plaintext_size = self.private_key.key_size // 8 - 2 * (hashes.SHA256.digest_size) - 2 self.logger.info(f"Max plaintext size (per fragment): {self.max_plaintext_size}") 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 NON FRAMMENTATA self.send_tlv(b'K', self.public_key_pem, force_no_fragment=True) time.sleep(0.1) #Aggiungi una piccola pausa self._request_server_public_key() except socket.error as e: self._handle_error(f"Errore socket: {e}") self.close() except Exception as e: self._handle_error(f"Errore generico: {e}") self.close() 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: try: self.logger.info("Richiesta chiave pubblica al server.") self.send_tlv(b'R', b"REQ_PUB_KEY", force_no_fragment=True) response = self.receive_tlv() if response: type, value = response if type == b'K': self.server_public_key = serialization.load_pem_public_key(value, backend=default_backend()) self.logger.info("Chiave pubblica del server ricevuta.") break else: self.logger.warning("Risposta non valida.") except socket.timeout: self.logger.error(f"Timeout (tentativo {retries + 1}).") retries += 1 except Exception as e: self._handle_error(f"Errore ricezione chiave: {e}") retries += 1 if self.server_public_key is None: self.logger.error("Impossibile ottenere chiave server.") self.close() def receive(self): """Riceve un messaggio UDP (dati grezzi).""" if not self.active or not self.socket: self._handle_error("Client non attivo/socket non inizializzato.") return None, None try: data, addr = self.socket.recvfrom(self.buffer_size) self.logger.debug(f"Ricevuto: {data[:50]}...") return data, addr except socket.timeout: self.logger.debug("Timeout ricezione.") return None, None except socket.error as e: self._handle_error(f"Errore ricezione: {e}") return None, None except Exception as e: self._handle_error(f"Errore generico: {e}") return None, None def receive_tlv(self): """Riceve un messaggio TLV completo, decifra e restituisce.""" data, addr = self.receive() if data is None: return None if data: self.logger.debug(f"DEBUG CLIENT: Raw data received in receive_tlv: {data[:50]}...") # Decifra (se cifrato, e solo DOPO aver gestito la chiave pubblica). if self.private_key and not data.startswith(b"-----BEGIN"): try: decrypted_data = self.private_key.decrypt( data, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None) ) data = decrypted_data # Usa i dati decrittati except (ValueError, InvalidSignature): # Cattura anche InvalidSignature self.logger.debug("Impossibile decifrare (probabilmente non criptato).") pass # Prosegui con i dati originali se non è cifrato # Estrai TLV *DOPO* la decrittazione (o se non era cifrato). try: type = data[0:1] length = int.from_bytes(data[1:3], byteorder='big') value = data[3:3 + length] except Exception as e: self.logger.error(f"Errore nell'estrazione del TLV: {e}") return None if len(value) != length: self.logger.error("Lunghezza TLV errata.") return None # CONTROLLO CHIAVE PUBBLICA *DOPO* ESTRAZIONE TLV, usando 'type' e 'value' if type == b'K': if value.startswith(b"-----BEGIN PUBLIC KEY-----"): self.logger.debug("DEBUG CLIENT: Received data starts with PEM marker. Treating as public key.") try: # Carica la chiave pubblica usando 'value' self.server_public_key = serialization.load_pem_public_key(value, backend=default_backend()) self.logger.info("Chiave pubblica del server ricevuta e caricata.") return # Esci dopo aver caricato la chiave except Exception as e: self.logger.error(f"Error processing PEM key: {e}") return None else: self.logger.error("Messaggio di tipo K ma non inizia con il marker PEM.") return None # Gestione altri tipi di messaggio DOPO aver verificato la presenza di una chiave if self.on_message: if type == b'M': self.on_message(value.decode('latin-1'), addr) else: self.logger.debug(f"Messaggio non di tipo 'M': {value.decode('latin-1')}") else: self.logger.warning(f"Callback non impostata. Messaggio: {value.decode('latin-1')}") return type, value def close(self): """Chiude la connessione.""" 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 chiusura socket: {e}") except Exception as e: self._handle_error(f"Errore generico chiusura: {e}") finally: self.logger.info("Client arrestato.") def _encrypt_message(self, message): """Cripta il messaggio.""" 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 server non disponibile.") return message def send_tlv(self, type, value, force_no_fragment=False): """Invia un messaggio TLV (con opzione per non frammentare).""" if not self.active: self._handle_error("Client non attivo.") return address = (self.server_host, self.server_port) try: if isinstance(value, str): value = value.encode('utf-8') length = len(value) length_bytes = length.to_bytes(2, byteorder='big', signed=False) tlv_message = type + length_bytes + value # Gestione NON-frammentazione forzata e frammentazione if force_no_fragment: # Crittografa se necessario (solo per messaggi non di tipo K o R) if type != b'K' and type != b'R' and self.server_public_key: try: tlv_message = self._encrypt_message(tlv_message) except ValueError as e: self.logger.error(f"Errore crittografia (no fragment): {e}") return header = b'\x00\x00\x00\x00\x01\x00\x01' # Intestazione per non frammentato packet = header + tlv_message if self.socket: self.socket.sendto(packet, address) self.logger.debug(f"Inviato TLV NON frammentato (forzato): {packet[:50]}...") else: self._handle_error("Socket non inizializzato.") elif len(tlv_message) > self.max_plaintext_size: # Frammenta usando max_plaintext_size msg_id = int.from_bytes(os.urandom(4), byteorder='big') # Usa self.max_plaintext_size per la dimensione dei frammenti fragments = [tlv_message[i:i + self.max_plaintext_size] for i in range(0, len(tlv_message), self.max_plaintext_size)] total_frags = len(fragments) for i, frag_payload in enumerate(fragments): frag_num = i + 1 # Crittografa il *singolo frammento* (solo per messaggi non di tipo K o R) if type != b'K' and type != b'R' and self.server_public_key: try: frag_payload = self._encrypt_message(frag_payload) except ValueError as e: self.logger.error(f"Errore crittografia (frammento {frag_num}): {e}") return # o gestisci l'errore in altro modo, ad es. inviando un frammento vuoto header = msg_id.to_bytes(4, byteorder='big') + frag_num.to_bytes(1, byteorder='big') + total_frags.to_bytes(2, byteorder='big') packet = header + frag_payload self.socket.sendto(packet, address) time.sleep(0.01) # Piccola pausa tra i frammenti self.logger.debug(f"Inviato TLV frammentato (id {msg_id}), {total_frags} frammenti.") else: # Messaggio completo e non frammentato # Crittografa se necessario (solo per messaggi non di tipo K o R) if type != b'K' and type != b'R' and self.server_public_key: try: tlv_message = self._encrypt_message(tlv_message) except ValueError as e: self.logger.error(f"Errore crittografia (no fragment): {e}") return header = b'\x00\x00\x00\x00\x01\x00\x01' # Intestazione per non frammentato packet = header + tlv_message if self.socket: self.socket.sendto(packet, address) self.logger.debug(f"Inviato TLV NON frammentato: {packet[:50]}...") else: self._handle_error("Socket non inizializzato.") except (socket.error, ValueError) as e: self._handle_error(f"Errore invio TLV: {e}") except Exception as e: self._handle_error(f"Errore generico invio TLV: {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 in chunks.""" chunks = [] string_length = len(long_string) start_index = 0 while start_index < string_length: end_index = min(start_index + chunk_size, string_length) chunk = long_string[start_index:end_index] chunks.append(chunk) start_index = end_index return chunks def print_message_callback(messaggio, indirizzo): """Callback per messaggi.""" print(f"Callback: Messaggio: {messaggio}, da: {indirizzo}") def test_udp_server(server_host, server_port): """Funzione di test.""" client = UDPClient(server_host, server_port) client.on_message = print_message_callback client.start() # Messaggi di test da inviare test_messages = [ "Messaggio numero 1 - piccolo", "Messaggio numero 2 - un po' più lungo", "La permanenza presso la struttura può avere carattere permanente o temporaneo"+ " (il periodo minimo di soggiorno è di 15 giorni). Le dimissioni dell’ospite devono essere"+ " comunicate per scritto alla Direzione con almeno 30 giorni di preavviso con domanda personale"+ " o richiesta dei familiari."+ " Al momento dell’uscita dalla struttura verranno restituiti gli effetti personali in custodia"+ " ed il deposito cauzionale infruttifero versato al momento dell’ammissione. A richiesta e a"+ " pagamento è possibile ricevere la cartella clinica dell’ospite che verrà consegnata entro 30 giorni.", "Fine" # Messaggio speciale per indicare la fine del test ] try: for message in test_messages: if message == "Fine": print("Client: Terminato.") break print(f"Client: Invio: {message}") #chunks = split_string_into_chunks(message) #Non serve più #for chunk in chunks: #client.send(chunk) client.send_tlv(b'M', message) #Non serve più lo split, gestito da send_tlv response = client.receive_tlv() # Riceve ACK, ma gestisci il caso None if response: type, value = response if type == b'A': print(f"Client: Ricevuto ACK") time.sleep(1) except KeyboardInterrupt: print("Client: Interrotto.") finally: client.close() def main(): host = "localhost" port = 5001 print(f"Indirizzo del Server: {host}:{port}") test_udp_server(host, port) if __name__ == "__main__": main()