diff options
| author | 2025-09-14 19:54:04 +0330 | |
|---|---|---|
| committer | 2025-09-14 19:54:04 +0330 | |
| commit | b4a0211a0fbd4f9d4928f03db33762f14d780d22 (patch) | |
| tree | bcb631597b2e51be8e266370bf1c90e45b7e36b0 /client | |
| parent | better (independent) client (diff) | |
better py client
Diffstat (limited to 'client')
| -rw-r--r-- | client/3rd-party/client.py | 1106 |
1 files changed, 401 insertions, 705 deletions
diff --git a/client/3rd-party/client.py b/client/3rd-party/client.py index 52675ad..bb78192 100644 --- a/client/3rd-party/client.py +++ b/client/3rd-party/client.py @@ -1,188 +1,48 @@ -# 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 +# ai-generated, do not trust (i don't like python) + +import asyncio +import aiohttp +import hashlib +from ecdsa import SigningKey, NIST256p +from typing import List, Tuple, Optional, Dict 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 +import struct + +MAX_BUFFER = 1 << 18 +MAX_BODY = 1 << 22 MAX_RECIPIENTS = 32 -MOTD_SIZE = 128 -DATA_SIZE = 65536 -SIGN_PUBLICKEYBYTES = 32 -SIGN_SECRETKEYBYTES = 64 +PACKET_SIZE = 4 + 32 + (1 + 8 + 32 + 32 + 1 + (32 * MAX_RECIPIENTS) + 65536 + 64) +RELAY_NEIGHBOR_WIRE_LEN = 1 + 1 + 1 + 4 + 16 + 2 -class ClientError(IntEnum): +class MpClientError: 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 + CONNECTION_FAILED = 2 + SERVER_BUSY = 3 + INVALID_RESPONSE = 4 + DESERIALIZE_FAILED = 5 + STRING_ENCODE_FAILED = 6 + BAD_ARGUMENT = 7 + @dataclass -class PyRelayNeighbor: +class RelayNeighbor: 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 + port: int @dataclass -class PyRelayStatistics: - neighbors: int - held_packets: int - quick_connect: List[PyRelayNeighbor] - -@dataclass -class PyRelayInfo: +class RelayInfo: 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] - ) - ) + motd: bytes + capabilities: Dict[str, any] + statistics: Dict[str, any] @dataclass -class PyDataPacketContents: +class DataPacketContents: encrypted: bool created_time_ms: int author: bytes @@ -193,558 +53,394 @@ class PyDataPacketContents: signature: bytes @dataclass -class PyDataPacket: +class DataPacket: 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 + contents: DataPacketContents @dataclass -class PyKeyPair: +class DataPacketList: + packets: List[DataPacket] + count: int + new_offset: int + +@dataclass +class KeyPair: 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) + +class MoonPasteClient: + def __init__(self): + self.ip: Optional[str] = None + self.port: Optional[int] = None + + def connect(self, ip: str, port: int) -> None: + self.ip = ip + self.port = port + + def _check_connected(self) -> bool: + return self.ip is not None and self.port is not None + + def _read_u16_le(self, buffer: bytes, offset: int) -> int: + return struct.unpack_from('<H', buffer, offset)[0] + + def _write_u16_le(self, buffer: bytearray, value: int, offset: int) -> None: + struct.pack_into('<H', buffer, offset, value) + + def _write_u32_le(self, buffer: bytearray, value: int, offset: int) -> None: + struct.pack_into('<I', buffer, offset, value) + + def _read_u32_le(self, buffer: bytes, offset: int) -> int: + return struct.unpack_from('<I', buffer, offset)[0] + + def _read_u64_le(self, buffer: bytes, offset: int) -> int: + return struct.unpack_from('<Q', buffer, offset)[0] + + def _write_u64_le(self, buffer: bytearray, value: int, offset: int) -> None: + struct.pack_into('<Q', buffer, offset, value) + + def _check_leading_zeros(self, hash_bytes: bytes, required_zeros: int) -> bool: + if required_zeros == 0: + return True + full_bytes = required_zeros // 8 + remaining_bits = required_zeros % 8 + for i in range(full_bytes): + if hash_bytes[i] != 0: + return False + if remaining_bits > 0: + mask = 0xFF << (8 - remaining_bits) + if hash_bytes[full_bytes] & mask != 0: + return False + return True + + async def _sha256(self, data: bytes) -> bytes: + return hashlib.sha256(data).digest() + + async def generate_key_pair(self) -> KeyPair: + enc_key = SigningKey.generate(curve=NIST256p) + sign_key = SigningKey.generate(curve=NIST256p) + return KeyPair( + encryption_pub=enc_key.verifying_key.to_string()[:32], + encryption_priv=enc_key.to_string()[:32], + signing_pub=sign_key.verifying_key.to_string()[:32], + signing_priv=sign_key.to_string()[:32] ) - - 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 + async def _sign_data(self, data: bytes, private_key: bytes) -> bytes: + sk = SigningKey.from_string(private_key, curve=NIST256p) + return sk.sign(data, hashfunc=hashlib.sha256)[:64] -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 - - # mp_query_author_packets - self.lib.mp_query_author_packets.argtypes = [ - ctypes.c_char_p, # ip - c_int, # port - KeyPair, # author_keys - c_uint16, # count - c_uint64, # offset - POINTER(DataPacketList) # out_list - ] - self.lib.mp_query_author_packets.restype = c_int - -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) + def _s2relay_neighbor(self, stream: bytes) -> Tuple[RelayNeighbor, bool]: + if len(stream) != RELAY_NEIGHBOR_WIRE_LEN: + return RelayNeighbor(False, False, False, b'\x00' * 4, b'\x00' * 16, 0), False + return RelayNeighbor( + active=stream[0] != 0, + has_ipv4=stream[1] != 0, + has_ipv6=stream[2] != 0, + v4_addr=stream[3:7], + v6_addr=stream[7:23], + port=self._read_u16_le(stream, 23) + ), True + + def _s2relay_info(self, stream: bytes) -> Tuple[RelayInfo, bool]: + expected_len = 1 + 1 + 128 + 2 + 1 + 1 + 1 + 2 + 8 + (4 * RELAY_NEIGHBOR_WIRE_LEN) + if len(stream) != expected_len: + return RelayInfo(0, 0, b'\x00' * 128, {'retention_hours': 0, 'can_store': False, 'leading_zeros': 0, 'accepts_plain': False}, {'neighbors': 0, 'held_packets': 0, 'quick_connect': []}), False + off = 0 + info = RelayInfo( + major_version=stream[off], + minor_version=stream[off + 1], + motd=stream[off + 2:off + 130], + capabilities={ + 'retention_hours': self._read_u16_le(stream, off + 130), + 'can_store': stream[off + 132] != 0, + 'leading_zeros': stream[off + 133], + 'accepts_plain': stream[off + 134] != 0 + }, + statistics={ + 'neighbors': self._read_u16_le(stream, off + 135), + 'held_packets': self._read_u64_le(stream, off + 137), + 'quick_connect': [] + } ) - - 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) + off += 145 + for _ in range(4): + neighbor, success = self._s2relay_neighbor(stream[off:off + RELAY_NEIGHBOR_WIRE_LEN]) + if not success: + return RelayInfo(0, 0, b'\x00' * 128, {'retention_hours': 0, 'can_store': False, 'leading_zeros': 0, 'accepts_plain': False}, {'neighbors': 0, 'held_packets': 0, 'quick_connect': []}), False + info.statistics['quick_connect'].append(neighbor) + off += RELAY_NEIGHBOR_WIRE_LEN + return info, True + + def _s2data_packet(self, stream: bytes) -> Tuple[DataPacket, bool]: + if len(stream) != PACKET_SIZE: + return DataPacket(0, b'\x00' * 32, DataPacketContents(False, 0, b'\x00' * 32, b'\x00' * 32, 0, [], b'\x00' * 65536, b'\x00' * 64)), False + off = 0 + packet = DataPacket( + nonce=self._read_u32_le(stream, off), + hash=stream[off + 4:off + 36], + contents=DataPacketContents( + encrypted=stream[off + 36] != 0, + created_time_ms=self._read_u64_le(stream, off + 37), + author=stream[off + 45:off + 77], + author_signing_pub=stream[off + 77:off + 109], + recipient_count=stream[off + 109], + recipients=[], + data=stream[off + 110 + 32 * MAX_RECIPIENTS:off + 110 + 32 * MAX_RECIPIENTS + 65536], + signature=stream[off + 110 + 32 * MAX_RECIPIENTS + 65536:off + 110 + 32 * MAX_RECIPIENTS + 65536 + 64] + ) ) - - if result != ClientError.SUCCESS: - raise MoonpasteClientError(ClientError(result)) - + off += 110 + for _ in range(MAX_RECIPIENTS): + packet.contents.recipients.append(stream[off:off + 32]) + off += 32 + return packet, True + + def _data_packet2s(self, data: DataPacket) -> Optional[bytes]: + if data.contents.recipient_count > MAX_RECIPIENTS: + return None + stream = bytearray(PACKET_SIZE) + off = 0 + self._write_u32_le(stream, data.nonce, off) + off += 4 + stream[off:off + 32] = data.hash + off += 32 + stream[off] = 1 if data.contents.encrypted else 0 + off += 1 + self._write_u64_le(stream, data.contents.created_time_ms, off) + off += 8 + stream[off:off + 32] = data.contents.author + off += 32 + stream[off:off + 32] = data.contents.author_signing_pub + off += 32 + stream[off] = data.contents.recipient_count + off += 1 + for i in range(MAX_RECIPIENTS): + stream[off:off + 32] = data.contents.recipients[i] if i < len(data.contents.recipients) else b'\x00' * 32 + off += 32 + stream[off:off + 65536] = data.contents.data + off += 65536 + stream[off:off + 64] = data.contents.signature + return bytes(stream) + + async def _client_request(self, data: bytes, data_len: int) -> Tuple[Optional[bytes], int, int]: + if not self._check_connected(): + return None, 0, MpClientError.CONNECTION_FAILED + is_ipv6 = ':' in self.ip + url = f"http://[{self.ip}]:{self.port}/" if is_ipv6 else f"http://{self.ip}:{self.port}/" + async with aiohttp.ClientSession() as session: + try: + async with session.post(url, data=data[:data_len], headers={'Content-Length': str(data_len), 'Connection': 'close'}) as response: + if response.status == 418: + return None, 0, MpClientError.SERVER_BUSY + if response.status != 200: + return None, 0, MpClientError.NETWORK_FAIL + response_data = await response.read() + content_length = int(response.headers.get('Content-Length', '0')) + if content_length > 0 and len(response_data) != content_length: + return None, 0, MpClientError.RECV_FAILED + if len(response_data) > MAX_BODY: + return None, 0, MpClientError.INVALID_RESPONSE + return response_data, len(response_data), MpClientError.SUCCESS + except aiohttp.ClientError: + return None, 0, MpClientError.CONNECTION_FAILED + + async def get_server_info(self) -> Tuple[RelayInfo, int]: + data = b'\x10' + response, resp_len, error = await self._client_request(data, len(data)) + if error != MpClientError.SUCCESS: + return RelayInfo(0, 0, b'\x00' * 128, {'retention_hours': 0, 'can_store': False, 'leading_zeros': 0, 'accepts_plain': False}, {'neighbors': 0, 'held_packets': 0, 'quick_connect': []}), error + if resp_len < 1 or response[0] != 0x17: + return RelayInfo(0, 0, b'\x00' * 128, {'retention_hours': 0, 'can_store': False, 'leading_zeros': 0, 'accepts_plain': False}, {'neighbors': 0, 'held_packets': 0, 'quick_connect': []}), MpClientError.INVALID_RESPONSE + info, success = self._s2relay_info(response[1:resp_len]) + return info, MpClientError.SUCCESS if success else MpClientError.DESERIALIZE_FAILED + + async def get_server_neighbors(self, count: int) -> Tuple[List[RelayNeighbor], int, int]: + data = bytearray(2) + data[0] = 0x11 + data[1] = count + response, resp_len, error = await self._client_request(data, len(data)) + if error != MpClientError.SUCCESS: + return [], 0, error + if resp_len < 1 or response[0] != 0x18: + return [], 0, MpClientError.INVALID_RESPONSE + num_neighbors = (resp_len - 1) // RELAY_NEIGHBOR_WIRE_LEN 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)) - + actual_count = 0 + for i in range(num_neighbors): + neighbor, success = self._s2relay_neighbor(response[1 + i * RELAY_NEIGHBOR_WIRE_LEN:1 + (i + 1) * RELAY_NEIGHBOR_WIRE_LEN]) + if success: + neighbors.append(neighbor) + actual_count += 1 + return neighbors, actual_count, MpClientError.SUCCESS + + async def push_packet(self, packet: DataPacket) -> Tuple[bool, int]: + send_data = self._data_packet2s(packet) + if send_data is None: + return False, MpClientError.BAD_ARGUMENT + full_data = bytearray(1 + PACKET_SIZE) + full_data[0] = 0x13 + full_data[1:] = send_data + response, resp_len, error = await self._client_request(full_data, len(full_data)) + if error != MpClientError.SUCCESS: + return False, error + if resp_len == 2 and response[0] == 0x1a: + return response[1] != 0, MpClientError.SUCCESS + return False, MpClientError.INVALID_RESPONSE + + async def get_packet(self, packet_hash: bytes) -> Tuple[DataPacket, int]: + send_data = bytearray(1 + 32) + send_data[0] = 0x14 + send_data[1:33] = packet_hash + response, resp_len, error = await self._client_request(send_data, len(send_data)) + if error != MpClientError.SUCCESS: + return DataPacket(0, b'\x00' * 32, DataPacketContents(False, 0, b'\x00' * 32, b'\x00' * 32, 0, [], b'\x00' * 65536, b'\x00' * 64)), error + if resp_len < 1 or response[0] != 0x1b: + return DataPacket(0, b'\x00' * 32, DataPacketContents(False, 0, b'\x00' * 32, b'\x00' * 32, 0, [], b'\x00' * 65536, b'\x00' * 64)), MpClientError.INVALID_RESPONSE + packet, success = self._s2data_packet(response[1:resp_len]) + return packet, MpClientError.SUCCESS if success else MpClientError.DESERIALIZE_FAILED + + async def get_firehose_packets(self, offset: int, count: int) -> Tuple[DataPacketList, int]: + send_data = bytearray(1 + 1 + 8) + send_data[0] = 0x12 + send_data[1] = count + self._write_u64_le(send_data, offset, 2) + response, resp_len, error = await self._client_request(send_data, len(send_data)) + if error != MpClientError.SUCCESS: + return DataPacketList([], 0, offset), error + if resp_len < 9 or response[0] != 0x19: + return DataPacketList([], 0, offset), MpClientError.INVALID_RESPONSE + new_offset = self._read_u64_le(response, 1) + available_bytes = resp_len - 9 + available_packets = available_bytes // PACKET_SIZE 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 + packet_count = 0 + for i in range(available_packets): + packet, success = self._s2data_packet(response[9 + i * PACKET_SIZE:9 + (i + 1) * PACKET_SIZE]) + if success: + packets.append(packet) + packet_count += 1 + return DataPacketList(packets, packet_count, new_offset), MpClientError.SUCCESS + + async def create_packet_from_bytes( + self, + input_bytes: bytes, + input_len: int, + recipient_count: int, + recipients: List[bytes], + author_keys: KeyPair + ) -> DataPacket: + packet = DataPacket( + nonce=0, + hash=b'\x00' * 32, + contents=DataPacketContents( + encrypted=False, + created_time_ms=int.from_bytes(struct.pack('<Q', int.from_bytes(b'\x00' * 8, 'little') + int(1000 * (await asyncio.get_event_loop().time()))), 'little'), + author=author_keys.encryption_pub, + author_signing_pub=author_keys.signing_pub, + recipient_count=min(recipient_count, MAX_RECIPIENTS), + recipients=[b'\x00' * 32 for _ in range(MAX_RECIPIENTS)], + data=b'\x00' * 65536, + signature=b'\x00' * 64 + ) ) - - 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 - + copy_len = min(input_len, 65536) + if input_bytes and copy_len > 0: + packet.contents.data = input_bytes[:copy_len] + b'\x00' * (65536 - copy_len) 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 - - def query_author_packets(self, ip: str, port: int, author_keys: PyKeyPair, count: int = 10, offset: int = 0) -> PyDataPacketList: - '''Query packets for a specific author''' - c_keys = author_keys.to_c() - packet_list = DataPacketList() - - result = self.ffi.lib.mp_query_author_packets( - ip.encode('utf-8'), - port, - c_keys, - count, - offset, - byref(packet_list) - ) - - if result != ClientError.SUCCESS: - raise MoonpasteClientError(ClientError(result)) - + for i in range(packet.contents.recipient_count): + packet.contents.recipients[i] = recipients[i] + msg = bytearray(1 + 8 + 32 + 32 + 1 + 32 * MAX_RECIPIENTS + 65536) + msg[0] = 1 if packet.contents.encrypted else 0 + self._write_u64_le(msg, packet.contents.created_time_ms, 1) + msg[9:41] = packet.contents.author + msg[41:73] = packet.contents.author_signing_pub + msg[73] = packet.contents.recipient_count + for i in range(MAX_RECIPIENTS): + msg[74 + 32 * i:74 + 32 * (i + 1)] = packet.contents.recipients[i] + msg[74 + 32 * MAX_RECIPIENTS:74 + 32 * MAX_RECIPIENTS + 65536] = packet.contents.data + packet.contents.signature = await self._sign_data(msg, author_keys.signing_priv) + return packet + + async def create_packet_from_string( + self, + input_str: str, + recipient_count: int, + recipients: List[bytes], + author_keys: KeyPair + ) -> Tuple[DataPacket, int]: + try: + input_bytes = input_str.encode('utf-8') + packet = await self.create_packet_from_bytes(input_bytes, len(input_bytes), recipient_count, recipients, author_keys) + return packet, MpClientError.SUCCESS + except UnicodeEncodeError: + return DataPacket(0, b'\x00' * 32, DataPacketContents(False, 0, b'\x00' * 32, b'\x00' * 32, 0, [], b'\x00' * 65536, b'\x00' * 64)), MpClientError.STRING_ENCODE_FAILED + + async def run_pow(self, packet: DataPacket, leading_zeros: int) -> DataPacket: + hash_input = bytearray(4 + PACKET_SIZE - 4) + contents_buffer = hash_input[4:] + contents_buffer[0] = 1 if packet.contents.encrypted else 0 + self._write_u64_le(contents_buffer, packet.contents.created_time_ms, 1) + contents_buffer[9:41] = packet.contents.author + contents_buffer[41:73] = packet.contents.author_signing_pub + contents_buffer[73] = packet.contents.recipient_count + for i in range(MAX_RECIPIENTS): + contents_buffer[74 + 32 * i:74 + 32 * (i + 1)] = packet.contents.recipients[i] + contents_buffer[74 + 32 * MAX_RECIPIENTS:74 + 32 * MAX_RECIPIENTS + 65536] = packet.contents.data + contents_buffer[74 + 32 * MAX_RECIPIENTS + 65536:74 + 32 * MAX_RECIPIENTS + 65536 + 64] = packet.contents.signature + nonce = 0 + while True: + self._write_u32_le(hash_input, nonce, 0) + hash_result = await self._sha256(hash_input) + if self._check_leading_zeros(hash_result, leading_zeros): + packet.nonce = nonce + packet.hash = hash_result + break + nonce += 1 + return packet + + async def query_author_packets( + self, + author_keys: KeyPair, + count: int, + offset: int + ) -> Tuple[DataPacketList, int]: + msg = bytearray(32 + 32 + 2 + 8) + msg[0:32] = author_keys.encryption_pub + msg[32:64] = author_keys.signing_pub + self._write_u16_le(msg, count, 64) + self._write_u64_le(msg, offset, 66) + sig = await self._sign_data(msg, author_keys.signing_priv) + send_data = bytearray(1 + 32 + 32 + 64 + 2 + 8) + off = 0 + send_data[off] = 0x16 + off += 1 + send_data[off:off + 32] = author_keys.encryption_pub + off += 32 + send_data[off:off + 32] = author_keys.signing_pub + off += 32 + send_data[off:off + 64] = sig + off += 64 + self._write_u16_le(send_data, count, off) + off += 2 + self._write_u64_le(send_data, offset, off) + response, resp_len, error = await self._client_request(send_data, len(send_data)) + if error != MpClientError.SUCCESS: + return DataPacketList([], 0, offset), error + if resp_len < 11 or response[0] != 0x1d: + return DataPacketList([], 0, offset), MpClientError.INVALID_RESPONSE + resp_count = self._read_u16_le(response, 1) + new_offset = self._read_u64_le(response, 3) + expected_len = 11 + resp_count * PACKET_SIZE + if resp_len != expected_len: + return DataPacketList([], 0, offset), MpClientError.INVALID_RESPONSE 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 - )
\ No newline at end of file + packet_count = 0 + for i in range(resp_count): + packet, success = self._s2data_packet(response[11 + i * PACKET_SIZE:11 + (i + 1) * PACKET_SIZE]) + if success: + packets.append(packet) + packet_count += 1 + return DataPacketList(packets, packet_count, new_offset), MpClientError.SUCCESS
\ No newline at end of file |
