#!/usr/bin/env python3 import socket import struct import threading import os import platform import getpass from pathlib import Path from typing import List, Optional, Callable, Dict DEFAULT_UDP_PORT = 4644 DEFAULT_TCP_PORT = 4644 class Peer: def __init__(self, address: str, signature: str, port: int = DEFAULT_UDP_PORT): self.address = address self.signature = signature self.port = port def __repr__(self): return f"Peer({self.address}, {self.signature}, port={self.port})" class DuktoProtocol: def __init__(self): self.local_udp_port = DEFAULT_UDP_PORT self.local_tcp_port = DEFAULT_TCP_PORT self.udp_socket: Optional[socket.socket] = None self.tcp_server: Optional[socket.socket] = None self.peers: Dict[str, Peer] = {} self.is_sending = False self.is_receiving = False self.is_awaiting_approval = False self.running = False # Confirmation for receiving self._transfer_decision = threading.Event() self._transfer_approved = False # Callbacks self.on_peer_added: Optional[Callable[[Peer], None]] = None self.on_peer_removed: Optional[Callable[[Peer], None]] = None self.on_receive_start: Optional[Callable[[str], None]] = None self.on_receive_request: Optional[Callable[[str], None]] = None self.on_receive_complete: Optional[Callable[[List[str], int], None]] = None self.on_receive_text: Optional[Callable[[str, int], None]] = None self.on_send_start: Optional[Callable[[str], None]] = None self.on_send_complete: Optional[Callable[[List[str]], None]] = None self.on_transfer_progress: Optional[Callable[[int, int], None]] = None self.on_error: Optional[Callable[[str], None]] = None def set_ports(self, udp_port: int, tcp_port: int): self.local_udp_port = udp_port self.local_tcp_port = tcp_port def get_system_signature(self) -> str: name = getpass.getuser() + "'s CLARA" hostname = socket.gethostname() system = platform.system() return f"{name} at {hostname} ({system})" def initialize(self): # UDP Socket for peer discovery self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) self.udp_socket.bind(('', self.local_udp_port)) # TCP Server for receiving files self.tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.tcp_server.bind(('', self.local_tcp_port)) self.tcp_server.listen(5) self.running = True # Start listener threads threading.Thread(target=self._udp_listener, daemon=True).start() threading.Thread(target=self._tcp_listener, daemon=True).start() def say_hello(self, dest: str = '', port: int = None): if port is None: port = self.local_udp_port # Prepare packet if port == DEFAULT_UDP_PORT and self.local_udp_port == DEFAULT_UDP_PORT: if dest == '': msg_type = b'\x01' # HELLO MESSAGE (broadcast) else: msg_type = b'\x02' # HELLO MESSAGE (unicast) packet = msg_type + self.get_system_signature().encode('utf-8') else: if dest == '': msg_type = b'\x04' # HELLO MESSAGE (broadcast) with PORT else: msg_type = b'\x05' # HELLO MESSAGE (unicast) with PORT packet = msg_type + struct.pack('': self._send_to_all_broadcast(packet, port) if port != DEFAULT_UDP_PORT: self._send_to_all_broadcast(packet, DEFAULT_UDP_PORT) else: self.udp_socket.sendto(packet, (dest, port)) def say_goodbye(self): packet = b'\x03' + b'Bye Bye' # Collect all discovered ports ports = {self.local_udp_port} if self.local_udp_port != DEFAULT_UDP_PORT: ports.add(DEFAULT_UDP_PORT) for peer in self.peers.values(): ports.add(peer.port) # Send to all ports for port in ports: self._send_to_all_broadcast(packet, port) def send_file(self, ip_dest: str, files: List[str], port: int = 0): if port == 0: port = DEFAULT_TCP_PORT if self.is_receiving or self.is_sending: if self.on_error: self.on_error("Already busy with another transfer") return self.is_sending = True threading.Thread(target=self._send_file_thread, args=(ip_dest, port, files), daemon=True).start() def send_text(self, ip_dest: str, text: str, port: int = 0): if port == 0: port = DEFAULT_TCP_PORT if self.is_receiving or self.is_sending: if self.on_error: self.on_error("Already busy with another transfer") return self.is_sending = True threading.Thread(target=self._send_text_thread, args=(ip_dest, port, text), daemon=True).start() def approve_transfer(self): self._transfer_approved = True self._transfer_decision.set() def reject_transfer(self): self._transfer_approved = False self._transfer_decision.set() def _udp_listener(self): while self.running: try: data, addr = self.udp_socket.recvfrom(4096) self._handle_message(data, addr[0]) except Exception as e: if self.running: print(f"UDP listener error: {e}") def _handle_message(self, data: bytes, sender: str): if len(data) == 0: return msg_type = data[0] if msg_type in (0x01, 0x02): # HELLO (broadcast/unicast) signature = data[1:].decode('utf-8', errors='ignore') if signature != self.get_system_signature(): self.peers[sender] = Peer(sender, signature, DEFAULT_UDP_PORT) if msg_type == 0x01: # Reply to broadcast self.say_hello(sender, DEFAULT_UDP_PORT) if self.on_peer_added: self.on_peer_added(self.peers[sender]) elif msg_type == 0x03: # GOODBYE if sender in self.peers: peer = self.peers[sender] if self.on_peer_removed: self.on_peer_removed(peer) del self.peers[sender] elif msg_type in (0x04, 0x05): # HELLO with PORT port = struct.unpack(' List[str]: expanded = [] for filepath in files: path = Path(filepath) if path.is_dir(): self._add_recursive(expanded, path) else: expanded.append(str(path)) return expanded def _add_recursive(self, expanded: List[str], path: Path): expanded.append(str(path)) if path.is_dir(): for entry in path.iterdir(): self._add_recursive(expanded, entry) def _compute_total_size(self, files: List[str]) -> int: total = 0 for filepath in files: path = Path(filepath) if path.is_file(): total += path.stat().st_size return total def _send_to_all_broadcast(self, packet: bytes, port: int): # Try common broadcast try: self.udp_socket.sendto(packet, ('255.255.255.255', port)) except: pass def shutdown(self): self.running = False self.say_goodbye() if self.udp_socket: self.udp_socket.close() if self.tcp_server: self.tcp_server.close()