diff options
| author | 2025-09-14 13:47:32 +0330 | |
|---|---|---|
| committer | 2025-09-14 13:47:32 +0330 | |
| commit | f7485f452360beae96280fd212f52c7904871784 (patch) | |
| tree | c44e61fbcdc5c0b856fd07959f5440ea6b249bb9 /client/client.py | |
| parent | ts + py clients (diff) | |
moved to 3rd party
Diffstat (limited to 'client/client.py')
| -rw-r--r-- | client/client.py | 683 |
1 files changed, 0 insertions, 683 deletions
diff --git a/client/client.py b/client/client.py deleted file mode 100644 index 56a8faa..0000000 --- a/client/client.py +++ /dev/null @@ -1,683 +0,0 @@ -# ai generated (i don't like python) - -import ctypes -import os -import platform -from ctypes import ( - Structure, Union, POINTER, c_uint8, c_uint16, c_uint32, c_uint64, - c_bool, c_int, c_char, c_void_p, c_size_t, CFUNCTYPE, byref, cast -) -from typing import List, Optional, Tuple, Union as PyUnion -from dataclasses import dataclass -from enum import IntEnum -import sys - -# Type aliases -byte = c_uint8 -hash_t = byte * 32 -public_key = byte * 32 -private_key = byte * 32 -signature_t = byte * 64 -ipv4 = byte * 4 -ipv6 = byte * 16 - -# Constants -MAX_RECIPIENTS = 32 -MOTD_SIZE = 128 -DATA_SIZE = 65536 -SIGN_PUBLICKEYBYTES = 32 -SIGN_SECRETKEYBYTES = 64 - -class ClientError(IntEnum): - SUCCESS = 0 - NETWORK_FAIL = 1 - INVALID_ADDRESS = 2 - CONNECTION_FAILED = 3 - SEND_FAILED = 4 - RECV_FAILED = 5 - SERVER_BUSY = 6 - INVALID_RESPONSE = 7 - DESERIALIZE_FAILED = 8 - ALLOC_FAILED = 9 - FILE_NOT_FOUND = 10 - FILE_READ_FAILED = 11 - BAD_ARGUMENT = 12 - -# C Structure definitions -class RelayNeighbor(Structure): - _fields_ = [ - ('active', c_bool), - ('has_ipv4', c_bool), - ('has_ipv6', c_bool), - ('v4_addr', ipv4), - ('v6_addr', ipv6) - ] - -class RelayCapabilities(Structure): - _fields_ = [ - ('retention_hours', c_uint16), - ('can_store', c_bool), - ('leading_zeros', byte), - ('accepts_plain', c_bool) - ] - -class RelayStatistics(Structure): - _fields_ = [ - ('neighbors', c_uint16), - ('held_packets', c_uint64), - ('quick_connect', RelayNeighbor * 4) - ] - -class RelayInfo(Structure): - _fields_ = [ - ('major_version', byte), - ('minor_version', byte), - ('motd', c_char * MOTD_SIZE), - ('capabilities', RelayCapabilities), - ('statistics', RelayStatistics) - ] - -class DataPacketContents(Structure): - _fields_ = [ - ('encrypted', c_bool), - ('created_time_ms', c_uint64), - ('author', public_key), - ('author_signing_pub', public_key), - ('recipient_count', c_uint8), - ('recipients', public_key * MAX_RECIPIENTS), - ('data', byte * DATA_SIZE), - ('signature', signature_t) - ] - -class DataPacket(Structure): - _fields_ = [ - ('nonce', c_uint32), - ('hash', hash_t), - ('contents', DataPacketContents) - ] - -class KeyPair(Structure): - _fields_ = [ - ('encryption_pub', public_key), - ('encryption_priv', private_key), - ('signing_pub', byte * SIGN_PUBLICKEYBYTES), - ('signing_priv', byte * SIGN_SECRETKEYBYTES) - ] - -class DataPacketList(Structure): - _fields_ = [ - ('packets', POINTER(DataPacket)), - ('count', c_uint16), - ('new_offset', c_uint64) - ] - -# Python dataclasses for easier usage -@dataclass -class PyRelayNeighbor: - active: bool - has_ipv4: bool - has_ipv6: bool - v4_addr: bytes - v6_addr: bytes - - @classmethod - def from_c(cls, c_neighbor: RelayNeighbor) -> 'PyRelayNeighbor': - return cls( - active=c_neighbor.active, - has_ipv4=c_neighbor.has_ipv4, - has_ipv6=c_neighbor.has_ipv6, - v4_addr=bytes(c_neighbor.v4_addr), - v6_addr=bytes(c_neighbor.v6_addr) - ) - - def format_ip(self) -> str: - if self.has_ipv4: - return '.'.join(str(b) for b in self.v4_addr) - elif self.has_ipv6: - parts = [] - for i in range(0, 16, 2): - word = (self.v6_addr[i] << 8) | self.v6_addr[i + 1] - parts.append(f'{word:x}') - return ':'.join(parts) - return 'N/A' - -@dataclass -class PyRelayCapabilities: - retention_hours: int - can_store: bool - leading_zeros: int - accepts_plain: bool - -@dataclass -class PyRelayStatistics: - neighbors: int - held_packets: int - quick_connect: List[PyRelayNeighbor] - -@dataclass -class PyRelayInfo: - major_version: int - minor_version: int - motd: str - capabilities: PyRelayCapabilities - statistics: PyRelayStatistics - - @classmethod - def from_c(cls, c_info: RelayInfo) -> 'PyRelayInfo': - return cls( - major_version=c_info.major_version, - minor_version=c_info.minor_version, - motd=c_info.motd.decode('utf-8', errors='ignore').rstrip('\x00'), - capabilities=PyRelayCapabilities( - retention_hours=c_info.capabilities.retention_hours, - can_store=c_info.capabilities.can_store, - leading_zeros=c_info.capabilities.leading_zeros, - accepts_plain=c_info.capabilities.accepts_plain - ), - statistics=PyRelayStatistics( - neighbors=c_info.statistics.neighbors, - held_packets=c_info.statistics.held_packets, - quick_connect=[PyRelayNeighbor.from_c(n) for n in c_info.statistics.quick_connect] - ) - ) - -@dataclass -class PyDataPacketContents: - encrypted: bool - created_time_ms: int - author: bytes - author_signing_pub: bytes - recipient_count: int - recipients: List[bytes] - data: bytes - signature: bytes - -@dataclass -class PyDataPacket: - nonce: int - hash: bytes - contents: PyDataPacketContents - - @classmethod - def from_c(cls, c_packet: DataPacket) -> 'PyDataPacket': - recipients = [] - for i in range(c_packet.contents.recipient_count): - recipients.append(bytes(c_packet.contents.recipients[i])) - - return cls( - nonce=c_packet.nonce, - hash=bytes(c_packet.hash), - contents=PyDataPacketContents( - encrypted=c_packet.contents.encrypted, - created_time_ms=c_packet.contents.created_time_ms, - author=bytes(c_packet.contents.author), - author_signing_pub=bytes(c_packet.contents.author_signing_pub), - recipient_count=c_packet.contents.recipient_count, - recipients=recipients, - data=bytes(c_packet.contents.data), - signature=bytes(c_packet.contents.signature) - ) - ) - - def to_c(self) -> DataPacket: - c_packet = DataPacket() - c_packet.nonce = self.nonce - - for i, b in enumerate(self.hash[:32]): - c_packet.hash[i] = b - - c_packet.contents.encrypted = self.contents.encrypted - c_packet.contents.created_time_ms = self.contents.created_time_ms - c_packet.contents.recipient_count = self.contents.recipient_count - - for i, b in enumerate(self.contents.author[:32]): - c_packet.contents.author[i] = b - - for i, b in enumerate(self.contents.author_signing_pub[:32]): - c_packet.contents.author_signing_pub[i] = b - - for i, recipient in enumerate(self.contents.recipients[:MAX_RECIPIENTS]): - for j, b in enumerate(recipient[:32]): - c_packet.contents.recipients[i][j] = b - - for i, b in enumerate(self.contents.data[:DATA_SIZE]): - c_packet.contents.data[i] = b - - for i, b in enumerate(self.contents.signature[:64]): - c_packet.contents.signature[i] = b - - return c_packet - -@dataclass -class PyKeyPair: - encryption_pub: bytes - encryption_priv: bytes - signing_pub: bytes - signing_priv: bytes - - @classmethod - def from_c(cls, c_keypair: KeyPair) -> 'PyKeyPair': - return cls( - encryption_pub=bytes(c_keypair.encryption_pub), - encryption_priv=bytes(c_keypair.encryption_priv), - signing_pub=bytes(c_keypair.signing_pub), - signing_priv=bytes(c_keypair.signing_priv) - ) - - def to_c(self) -> KeyPair: - c_keypair = KeyPair() - - for i, b in enumerate(self.encryption_pub[:32]): - c_keypair.encryption_pub[i] = b - for i, b in enumerate(self.encryption_priv[:32]): - c_keypair.encryption_priv[i] = b - for i, b in enumerate(self.signing_pub[:32]): - c_keypair.signing_pub[i] = b - for i, b in enumerate(self.signing_priv[:64]): - c_keypair.signing_priv[i] = b - - return c_keypair - -@dataclass -class PyDataPacketList: - packets: List[PyDataPacket] - count: int - new_offset: int - -class MoonpasteClientError(Exception): - '''Exception raised for moonpaste client errors''' - def __init__(self, error_code: ClientError, message: str = ''): - self.error_code = error_code - self.message = message or f'Client error: {error_code.name}' - super().__init__(self.message) - -def find_library() -> str: - '''Find the moonpaste client library''' - system = platform.system().lower() - - if system == 'linux': - lib_names = ['libmpclient.so'] - elif system == 'darwin': - lib_names = ['libmpclient.dylib'] - elif system == 'windows': - lib_names = ['libmpclient.dll'] - else: - lib_names = ['libmpclient.so', 'libmpclient.dylib', 'libmpclient.dll'] - - search_paths = [ - './', - './lib/', - '../lib/', - '/usr/local/lib/', - '/usr/lib/', - os.environ.get('MOONPASTE_LIB_PATH', '') - ] - - search_paths = [p for p in search_paths if p] - - for search_path in search_paths: - for lib_name in lib_names: - full_path = os.path.join(search_path, lib_name) - if os.path.exists(full_path): - return full_path - - raise FileNotFoundError(f'Could not find moonpaste client library. Searched for: {lib_names}') - -class MoonpasteFFI: - '''Low-level FFI wrapper for the moonpaste client library''' - - def __init__(self): - lib_path = find_library() - self.lib = ctypes.CDLL(lib_path) - self._setup_function_signatures() - - def _setup_function_signatures(self): - '''Setup C function signatures''' - - # mp_get_server_info - self.lib.mp_get_server_info.argtypes = [ - ctypes.c_char_p, # ip - c_int, # port - POINTER(RelayInfo) # out_info - ] - self.lib.mp_get_server_info.restype = c_int - - # mp_get_server_neighbors - self.lib.mp_get_server_neighbors.argtypes = [ - ctypes.c_char_p, # ip - c_int, # port - byte, # count - POINTER(POINTER(RelayNeighbor)), # out_neighbors - POINTER(byte) # out_count - ] - self.lib.mp_get_server_neighbors.restype = c_int - - # mp_push_packet - self.lib.mp_push_packet.argtypes = [ - ctypes.c_char_p, # ip - c_int, # port - DataPacket, # packet - POINTER(c_bool) # out_success - ] - self.lib.mp_push_packet.restype = c_int - - # mp_get_packet - self.lib.mp_get_packet.argtypes = [ - ctypes.c_char_p, # ip - c_int, # port - hash_t, # packet_hash - POINTER(DataPacket) # out_packet - ] - self.lib.mp_get_packet.restype = c_int - - # mp_get_firehose_packets - self.lib.mp_get_firehose_packets.argtypes = [ - ctypes.c_char_p, # ip - c_int, # port - c_uint64, # offset - c_uint16, # count - POINTER(DataPacketList) # out_list - ] - self.lib.mp_get_firehose_packets.restype = c_int - - # mp_create_packet_from_bytes - self.lib.mp_create_packet_from_bytes.argtypes = [ - POINTER(byte), # input - c_size_t, # input_len - byte, # recipient_count - POINTER(hash_t), # recipients - KeyPair # author_keys - ] - self.lib.mp_create_packet_from_bytes.restype = DataPacket - - # mp_create_packet_from_file - self.lib.mp_create_packet_from_file.argtypes = [ - ctypes.c_char_p, # filename - byte, # recipient_count - POINTER(hash_t), # recipients - KeyPair, # author_keys - POINTER(DataPacket) # out_packet - ] - self.lib.mp_create_packet_from_file.restype = c_int - - # mp_run_pow - self.lib.mp_run_pow.argtypes = [ - DataPacket, # packet - byte # leading_zeros - ] - self.lib.mp_run_pow.restype = DataPacket - - # mp_generate_keypair - self.lib.mp_generate_keypair.argtypes = [] - self.lib.mp_generate_keypair.restype = KeyPair - -class MoonpasteClient: - '''High-level Python client for moonpaste''' - - def __init__(self): - self.ffi = MoonpasteFFI() - - def get_server_info(self, ip: str, port: int) -> PyRelayInfo: - '''Get server information and capabilities''' - info = RelayInfo() - - result = self.ffi.lib.mp_get_server_info( - ip.encode('utf-8'), - port, - byref(info) - ) - - if result != ClientError.SUCCESS: - raise MoonpasteClientError(ClientError(result)) - - return PyRelayInfo.from_c(info) - - def get_neighbors(self, ip: str, port: int, count: int = 5) -> List[PyRelayNeighbor]: - '''Get a list of neighbors from the server''' - neighbors_ptr = POINTER(RelayNeighbor)() - out_count = byte() - - result = self.ffi.lib.mp_get_server_neighbors( - ip.encode('utf-8'), - port, - count, - byref(neighbors_ptr), - byref(out_count) - ) - - if result != ClientError.SUCCESS: - raise MoonpasteClientError(ClientError(result)) - - neighbors = [] - if out_count.value > 0 and neighbors_ptr: - for i in range(out_count.value): - neighbors.append(PyRelayNeighbor.from_c(neighbors_ptr[i])) - - # Free the allocated memory - ctypes.c_void_p.in_dll(self.ffi.lib, 'free').argtypes = [c_void_p] - ctypes.c_void_p.in_dll(self.ffi.lib, 'free')(cast(neighbors_ptr, c_void_p)) - - return neighbors - - def push_packet(self, ip: str, port: int, packet: PyDataPacket) -> bool: - '''Push a packet to the server''' - c_packet = packet.to_c() - success = c_bool() - - result = self.ffi.lib.mp_push_packet( - ip.encode('utf-8'), - port, - c_packet, - byref(success) - ) - - if result != ClientError.SUCCESS: - raise MoonpasteClientError(ClientError(result)) - - return success.value - - def get_packet(self, ip: str, port: int, packet_hash: bytes) -> PyDataPacket: - '''Get a specific packet by its hash''' - if len(packet_hash) != 32: - raise ValueError('Hash must be exactly 32 bytes') - - c_hash = hash_t() - for i, b in enumerate(packet_hash): - c_hash[i] = b - - packet = DataPacket() - - result = self.ffi.lib.mp_get_packet( - ip.encode('utf-8'), - port, - c_hash, - byref(packet) - ) - - if result != ClientError.SUCCESS: - raise MoonpasteClientError(ClientError(result)) - - return PyDataPacket.from_c(packet) - - def get_firehose_packets(self, ip: str, port: int, offset: int = 0, count: int = 10) -> PyDataPacketList: - '''Get a batch of packets from the server's firehose stream''' - packet_list = DataPacketList() - - result = self.ffi.lib.mp_get_firehose_packets( - ip.encode('utf-8'), - port, - offset, - count, - byref(packet_list) - ) - - if result != ClientError.SUCCESS: - raise MoonpasteClientError(ClientError(result)) - - packets = [] - if packet_list.count > 0 and packet_list.packets: - for i in range(packet_list.count): - packets.append(PyDataPacket.from_c(packet_list.packets[i])) - - # Free the allocated memory safely - try: - import ctypes.util - libc_name = ctypes.util.find_library('c') - if libc_name: - libc = ctypes.CDLL(libc_name) - libc.free.argtypes = [c_void_p] - libc.free(cast(packet_list.packets, c_void_p)) - else: - # Platform-specific fallback - try: - if sys.platform.startswith('linux'): - libc = ctypes.CDLL('libc.so.6') - elif sys.platform == 'darwin': - libc = ctypes.CDLL('libc.dylib') - elif sys.platform == 'win32': - libc = ctypes.CDLL('msvcrt.dll') - else: - libc = None - - if libc: - libc.free.argtypes = [c_void_p] - libc.free(cast(packet_list.packets, c_void_p)) - except OSError: - pass # Can't free, but better than crash - except: - pass # Memory will be freed when process exits - - return PyDataPacketList( - packets=packets, - count=packet_list.count, - new_offset=packet_list.new_offset - ) - - def create_packet(self, - message: PyUnion[str, bytes], - recipients: Optional[List[bytes]] = None, - author_keys: Optional[PyKeyPair] = None) -> PyDataPacket: - '''Create a data packet from message content''' - - if isinstance(message, str): - data = message.encode('utf-8') - else: - data = message - - if author_keys is None: - author_keys = self.generate_keypair() - - c_keys = author_keys.to_c() - - # Convert data to ctypes array - data_len = min(len(data), DATA_SIZE) - c_data = (byte * data_len)(*data[:data_len]) - - # Handle recipients - recipient_count = 0 - c_recipients = None - - if recipients: - recipient_count = min(len(recipients), MAX_RECIPIENTS) - c_recipients = (hash_t * recipient_count)() - for i, recipient in enumerate(recipients[:recipient_count]): - if len(recipient) != 32: - raise ValueError(f'Recipient {i} must be exactly 32 bytes') - for j, b in enumerate(recipient): - c_recipients[i][j] = b - - packet = self.ffi.lib.mp_create_packet_from_bytes( - cast(c_data, POINTER(byte)), - data_len, - recipient_count, - cast(c_recipients, POINTER(hash_t)) if c_recipients else None, - c_keys - ) - - return PyDataPacket.from_c(packet) - - def create_packet_from_file(self, - filename: str, - recipients: Optional[List[bytes]] = None, - author_keys: Optional[PyKeyPair] = None) -> PyDataPacket: - '''Create a data packet from file contents''' - - if author_keys is None: - author_keys = self.generate_keypair() - - c_keys = author_keys.to_c() - - # Handle recipients - recipient_count = 0 - c_recipients = None - - if recipients: - recipient_count = min(len(recipients), MAX_RECIPIENTS) - c_recipients = (hash_t * recipient_count)() - for i, recipient in enumerate(recipients[:recipient_count]): - if len(recipient) != 32: - raise ValueError(f'Recipient {i} must be exactly 32 bytes') - for j, b in enumerate(recipient): - c_recipients[i][j] = b - - packet = DataPacket() - - result = self.ffi.lib.mp_create_packet_from_file( - filename.encode('utf-8'), - recipient_count, - cast(c_recipients, POINTER(hash_t)) if c_recipients else None, - c_keys, - byref(packet) - ) - - if result != ClientError.SUCCESS: - raise MoonpasteClientError(ClientError(result)) - - return PyDataPacket.from_c(packet) - - def run_proof_of_work(self, packet: PyDataPacket, leading_zeros: int) -> PyDataPacket: - '''Run Proof-of-Work on a packet''' - c_packet = packet.to_c() - - result_packet = self.ffi.lib.mp_run_pow(c_packet, leading_zeros) - - return PyDataPacket.from_c(result_packet) - - def generate_keypair(self) -> PyKeyPair: - '''Generate a new encryption and signing keypair''' - c_keypair = self.ffi.lib.mp_generate_keypair() - return PyKeyPair.from_c(c_keypair) - - # Utility methods - @staticmethod - def hash_to_hex(hash_bytes: bytes) -> str: - '''Convert a hash to hexadecimal string''' - return hash_bytes.hex() - - @staticmethod - def hex_to_hash(hex_string: str) -> bytes: - '''Convert hexadecimal string to hash bytes''' - if len(hex_string) != 64: - raise ValueError('Hash hex string must be exactly 64 characters') - return bytes.fromhex(hex_string) - - def create_and_push_packet(self, - ip: str, - port: int, - message: PyUnion[str, bytes], - leading_zeros: int = 0, - recipients: Optional[List[bytes]] = None, - author_keys: Optional[PyKeyPair] = None) -> Tuple[PyDataPacket, bool]: - '''Create a packet, run PoW, and push it to the server''' - - # Create the packet - packet = self.create_packet(message, recipients, author_keys) - - # Run proof of work if required - if leading_zeros > 0: - packet = self.run_proof_of_work(packet, leading_zeros) - - # Push to server - success = self.push_packet(ip, port, packet) - - return packet, success
\ No newline at end of file |
