From f7485f452360beae96280fd212f52c7904871784 Mon Sep 17 00:00:00 2001 From: moon Date: Sun, 14 Sep 2025 13:47:32 +0330 Subject: moved to 3rd party --- client/3rd-party/client.py | 683 +++++++++++++++++++++++++++++++++++++++++++++ client/3rd-party/client.ts | 674 ++++++++++++++++++++++++++++++++++++++++++++ client/client.py | 683 --------------------------------------------- client/client.ts | 674 -------------------------------------------- 4 files changed, 1357 insertions(+), 1357 deletions(-) create mode 100644 client/3rd-party/client.py create mode 100644 client/3rd-party/client.ts delete mode 100644 client/client.py delete mode 100644 client/client.ts (limited to 'client') diff --git a/client/3rd-party/client.py b/client/3rd-party/client.py new file mode 100644 index 0000000..56a8faa --- /dev/null +++ b/client/3rd-party/client.py @@ -0,0 +1,683 @@ +# 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 diff --git a/client/3rd-party/client.ts b/client/3rd-party/client.ts new file mode 100644 index 0000000..44f4634 --- /dev/null +++ b/client/3rd-party/client.ts @@ -0,0 +1,674 @@ +// ai-generated (do not trust) + +import { dlopen, FFIType, JSCallback, suffix } from 'bun:ffi' +import * as path from 'path' +import * as fs from 'fs' + +export interface RelayNeighbor { + active: boolean + hasIpv4: boolean + hasIpv6: boolean + v4Addr: Uint8Array // 4 bytes + v6Addr: Uint8Array // 16 bytes +} + +export interface RelayCapabilities { + retentionHours: number + canStore: boolean + leadingZeros: number + acceptsPlain: boolean +} + +export interface RelayStatistics { + neighbors: number + heldPackets: bigint + quickConnect: RelayNeighbor[] // 4 elements +} + +export interface RelayInfo { + majorVersion: number + minorVersion: number + motd: string // 128 bytes max + capabilities: RelayCapabilities + statistics: RelayStatistics +} + +export interface DataPacketContents { + encrypted: boolean + createdTimeMs: bigint + author: Uint8Array // 32 bytes + authorSigningPub: Uint8Array // 32 bytes + recipientCount: number + recipients: Uint8Array[] // 32 arrays of 32 bytes each + data: Uint8Array // 65536 bytes + signature: Uint8Array // 64 bytes +} + +export interface DataPacket { + nonce: number + hash: Uint8Array // 32 bytes + contents: DataPacketContents +} + +export interface KeyPair { + encryptionPub: Uint8Array // 32 bytes + encryptionPriv: Uint8Array // 32 bytes + signingPub: Uint8Array // 32 bytes + signingPriv: Uint8Array // 64 bytes +} + +export interface DataPacketList { + packets: DataPacket[] + count: number + newOffset: bigint +} + +export enum ClientError { + Success = 0, + NetworkFail = 1, + InvalidAddress = 2, + ConnectionFailed = 3, + SendFailed = 4, + RecvFailed = 5, + ServerBusy = 6, + InvalidResponse = 7, + DeserializeFailed = 8, + AllocFailed = 9, + FileNotFound = 10, + FileReadFailed = 11, + BadArgument = 12 +} + +function getRuntime(): 'bun' | 'deno' | 'node' { + if (typeof Bun !== 'undefined') return 'bun' + if (typeof Deno !== 'undefined') return 'deno' + return 'node' +} + +function findLibrary(): string { + const runtime = getRuntime() + const libNames = ['libmpclient.so', 'libmpclient.dylib', 'libmpclient.dll'] + const searchPaths = [ + './', + './lib/', + '../lib/', + '/usr/local/lib/', + '/usr/lib/', + process.env.MOONPASTE_LIB_PATH || '' + ].filter(p => p.length > 0) + + for (const searchPath of searchPaths) { + for (const libName of libNames) { + const fullPath = path.join(searchPath, libName) + if (fs.existsSync(fullPath)) { + return fullPath + } + } + } + + throw new Error('Could not find moonpaste client library. Searched for: ' + libNames.join(', ')) +} + +class MoonpasteFFI { + private lib: any + private runtime: string + + constructor() { + this.runtime = getRuntime() + const libPath = findLibrary() + + switch (this.runtime) { + case 'bun': + this.lib = dlopen(libPath, { + mp_get_server_info: { + args: [FFIType.cstring, FFIType.i32, FFIType.ptr], + returns: FFIType.i32 + }, + mp_get_server_neighbors: { + args: [FFIType.cstring, FFIType.i32, FFIType.u8, FFIType.ptr, FFIType.ptr], + returns: FFIType.i32 + }, + mp_push_packet: { + args: [FFIType.cstring, FFIType.i32, FFIType.ptr, FFIType.ptr], + returns: FFIType.i32 + }, + mp_get_packet: { + args: [FFIType.cstring, FFIType.i32, FFIType.ptr, FFIType.ptr], + returns: FFIType.i32 + }, + mp_get_firehose_packets: { + args: [FFIType.cstring, FFIType.i32, FFIType.u64, FFIType.u16, FFIType.ptr], + returns: FFIType.i32 + }, + mp_create_packet_from_bytes: { + args: [FFIType.ptr, FFIType.usize, FFIType.u8, FFIType.ptr, FFIType.ptr], + returns: FFIType.ptr + }, + mp_create_packet_from_file: { + args: [FFIType.cstring, FFIType.u8, FFIType.ptr, FFIType.ptr, FFIType.ptr], + returns: FFIType.i32 + }, + mp_run_pow: { + args: [FFIType.ptr, FFIType.u8], + returns: FFIType.ptr + }, + mp_generate_keypair: { + args: [], + returns: FFIType.ptr + } + }) + break + + case 'deno': + this.lib = Deno.dlopen(libPath, { + mp_get_server_info: { + parameters: ['buffer', 'i32', 'pointer'], + result: 'i32' + }, + mp_get_server_neighbors: { + parameters: ['buffer', 'i32', 'u8', 'pointer', 'pointer'], + result: 'i32' + }, + mp_push_packet: { + parameters: ['buffer', 'i32', 'pointer', 'pointer'], + result: 'i32' + }, + mp_get_packet: { + parameters: ['buffer', 'i32', 'pointer', 'pointer'], + result: 'i32' + }, + mp_get_firehose_packets: { + parameters: ['buffer', 'i32', 'u64', 'u16', 'pointer'], + result: 'i32' + }, + mp_create_packet_from_bytes: { + parameters: ['pointer', 'usize', 'u8', 'pointer', 'pointer'], + result: 'pointer' + }, + mp_create_packet_from_file: { + parameters: ['buffer', 'u8', 'pointer', 'pointer', 'pointer'], + result: 'i32' + }, + mp_run_pow: { + parameters: ['pointer', 'u8'], + result: 'pointer' + }, + mp_generate_keypair: { + parameters: [], + result: 'pointer' + } + }) + break + + case 'node': + try { + const ffi = require('ffi-napi') // dependency + const ref = require('ref-napi') // dependency + + this.lib = ffi.Library(libPath, { + mp_get_server_info: ['int', ['string', 'int', 'pointer']], + mp_get_server_neighbors: ['int', ['string', 'int', 'uint8', 'pointer', 'pointer']], + mp_push_packet: ['int', ['string', 'int', 'pointer', 'pointer']], + mp_get_packet: ['int', ['string', 'int', 'pointer', 'pointer']], + mp_get_firehose_packets: ['int', ['string', 'int', 'uint64', 'uint16', 'pointer']], + mp_create_packet_from_bytes: ['pointer', ['pointer', 'size_t', 'uint8', 'pointer', 'pointer']], + mp_create_packet_from_file: ['int', ['string', 'uint8', 'pointer', 'pointer', 'pointer']], + mp_run_pow: ['pointer', ['pointer', 'uint8']], + mp_generate_keypair: ['pointer', []] + }) + } catch (e) { + throw new Error('Node.js FFI requires ffi-napi and ref-napi packages: npm install ffi-napi ref-napi') + } + break + } + } + + private allocateBuffer(size: number): any { + switch (this.runtime) { + case 'bun': + return new ArrayBuffer(size) + case 'deno': + return new ArrayBuffer(size) + case 'node': + const ref = require('ref-napi') + return ref.alloc(size) + } + } + + private stringToBuffer(str: string): any { + switch (this.runtime) { + case 'bun': + case 'deno': + return new TextEncoder().encode(str + '\0') + case 'node': + return Buffer.from(str + '\0', 'utf8') + } + } + + async getServerInfo(ip: string, port: number): Promise { + const infoBuffer = this.allocateBuffer(1024) + const ipBuffer = this.stringToBuffer(ip) + + const result = this.lib.symbols.mp_get_server_info(ipBuffer, port, infoBuffer) + + if (result !== ClientError.Success) { + throw new Error(`Failed to get server info: ${result}`) + } + + return this.parseRelayInfo(infoBuffer) + } + + async getServerNeighbors(ip: string, port: number, count: number): Promise { + const neighborsPtr = this.allocateBuffer(8) // pointer + const countPtr = this.allocateBuffer(1) // uint8 + const ipBuffer = this.stringToBuffer(ip) + + const result = this.lib.symbols.mp_get_server_neighbors(ipBuffer, port, count, neighborsPtr, countPtr) + + if (result !== ClientError.Success) { + throw new Error(`Failed to get neighbors: ${result}`) + } + + return this.parseNeighbors(neighborsPtr, countPtr) + } + + async pushPacket(ip: string, port: number, packet: DataPacket): Promise { + const successPtr = this.allocateBuffer(1) + const ipBuffer = this.stringToBuffer(ip) + const packetBuffer = this.serializePacket(packet) + + const result = this.lib.symbols.mp_push_packet(ipBuffer, port, packetBuffer, successPtr) + + if (result !== ClientError.Success) { + throw new Error(`Failed to push packet: ${result}`) + } + + return this.readBool(successPtr) + } + + async getPacket(ip: string, port: number, hash: Uint8Array): Promise { + const packetBuffer = this.allocateBuffer(70000) // approximation + const ipBuffer = this.stringToBuffer(ip) + + const result = this.lib.symbols.mp_get_packet(ipBuffer, port, hash, packetBuffer) + + if (result !== ClientError.Success) { + throw new Error(`Failed to get packet: ${result}`) + } + + return this.parseDataPacket(packetBuffer) + } + + async getFirehosePackets(ip: string, port: number, offset: bigint, count: number): Promise { + const listBuffer = this.allocateBuffer(1024) + const ipBuffer = this.stringToBuffer(ip) + + const result = this.lib.symbols.mp_get_firehose_packets(ipBuffer, port, offset, count, listBuffer) + + if (result !== ClientError.Success) { + throw new Error(`Failed to get firehose packets: ${result}`) + } + + return this.parseDataPacketList(listBuffer) + } + + createPacketFromBytes(input: Uint8Array, recipientCount: number, recipients: Uint8Array[] | null, authorKeys: KeyPair): DataPacket { + const recipientsBuffer = recipients ? this.serializeRecipients(recipients) : null + const keysBuffer = this.serializeKeyPair(authorKeys) + + const result = this.lib.symbols.mp_create_packet_from_bytes( + input, + input.length, + recipientCount, + recipientsBuffer, + keysBuffer + ) + + return this.parseDataPacket(result) + } + + async createPacketFromFile(filename: string, recipientCount: number, recipients: Uint8Array[] | null, authorKeys: KeyPair): Promise { + const packetBuffer = this.allocateBuffer(4096) + const filenameBuffer = this.stringToBuffer(filename) + const recipientsBuffer = recipients ? this.serializeRecipients(recipients) : null + const keysBuffer = this.serializeKeyPair(authorKeys) + + const result = this.lib.symbols.mp_create_packet_from_file( + filenameBuffer, + recipientCount, + recipientsBuffer, + keysBuffer, + packetBuffer + ) + + if (result !== ClientError.Success) { + throw new Error(`Failed to create packet from file: ${result}`) + } + + return this.parseDataPacket(packetBuffer) + } + + runProofOfWork(packet: DataPacket, leadingZeros: number): DataPacket { + const packetBuffer = this.serializePacket(packet) + const result = this.lib.symbols.mp_run_pow(packetBuffer, leadingZeros) + return this.parseDataPacket(result) + } + + generateKeyPair(): KeyPair { + const result = this.lib.symbols.mp_generate_keypair() + return this.parseKeyPair(result) + } + + private parseRelayInfo(buffer: any): RelayInfo { + const view = new DataView(buffer instanceof ArrayBuffer ? buffer : buffer.buffer) + let offset = 0 + + const majorVersion = view.getUint8(offset++) + const minorVersion = view.getUint8(offset++) + + const motdBytes = new Uint8Array(buffer, offset, 128) + const motd = new TextDecoder().decode(motdBytes).replace(/\0.*$/, '') + offset += 128 + + const retentionHours = view.getUint16(offset, true) // little endian + offset += 2 + const canStore = view.getUint8(offset++) !== 0 + const leadingZeros = view.getUint8(offset++) + const acceptsPlain = view.getUint8(offset++) !== 0 + + const neighbors = view.getUint16(offset, true) + offset += 2 + const heldPackets = view.getBigUint64(offset, true) + offset += 8 + + const quickConnect: RelayNeighbor[] = [] + for (let i = 0; i < 4; i++) { + quickConnect.push(this.parseRelayNeighborAt(buffer, offset)) + offset += 23 // 1 + 1 + 1 + 4 + 16 + } + + return { + majorVersion, + minorVersion, + motd, + capabilities: { + retentionHours, + canStore, + leadingZeros, + acceptsPlain + }, + statistics: { + neighbors, + heldPackets, + quickConnect + } + } + } + + private parseRelayNeighborAt(buffer: any, offset: number): RelayNeighbor { + const view = new DataView(buffer instanceof ArrayBuffer ? buffer : buffer.buffer) + + const active = view.getUint8(offset) !== 0 + const hasIpv4 = view.getUint8(offset + 1) !== 0 + const hasIpv6 = view.getUint8(offset + 2) !== 0 + + const v4Addr = new Uint8Array(buffer, offset + 3, 4) + const v6Addr = new Uint8Array(buffer, offset + 7, 16) + + return { active, hasIpv4, hasIpv6, v4Addr, v6Addr } + } + + private parseNeighbors(neighborsPtr: any, countPtr: any): RelayNeighbor[] { + const count = this.readUint8(countPtr) + const neighbors: RelayNeighbor[] = [] + + if (count > 0 && neighborsPtr) { + for (let i = 0; i < count; i++) { + const neighborOffset = i * 23 // RELAY_NEIGHBOR_WIRE_LEN + neighbors.push(this.parseRelayNeighborAt(neighborsPtr, neighborOffset)) + } + } + + return neighbors + } + + private parseDataPacket(buffer: any): DataPacket { + const view = new DataView(buffer instanceof ArrayBuffer ? buffer : buffer.buffer) + let offset = 0 + + const nonce = view.getUint32(offset, true) + offset += 4 + + const hash = new Uint8Array(buffer, offset, 32) + offset += 32 + + const encrypted = view.getUint8(offset++) !== 0 + const createdTimeMs = view.getBigUint64(offset, true) + offset += 8 + + const author = new Uint8Array(buffer, offset, 32) + offset += 32 + const authorSigningPub = new Uint8Array(buffer, offset, 32) + offset += 32 + + const recipientCount = view.getUint8(offset++) + const recipients: Uint8Array[] = [] + for (let i = 0; i < 32; i++) { + recipients.push(new Uint8Array(buffer, offset, 32)) + offset += 32 + } + + const data = new Uint8Array(buffer, offset, 65536) + offset += 65536 + + const signature = new Uint8Array(buffer, offset, 64) + + return { + nonce, + hash, + contents: { + encrypted, + createdTimeMs, + author, + authorSigningPub, + recipientCount, + recipients, + data, + signature + } + } + } + + private parseDataPacketList(buffer: any): DataPacketList { + const view = new DataView(buffer instanceof ArrayBuffer ? buffer : buffer.buffer) + + const newOffset = view.getBigUint64(0, true) + const availableBytes = buffer.byteLength - 8 + const packetSize = 4 + 32 + (1 + 8 + 32 + 32 + 1 + (32 * 32) + 65536 + 64) // PACKET_SIZE + const packetCount = Math.floor(availableBytes / packetSize) + + const packets: DataPacket[] = [] + for (let i = 0; i < packetCount; i++) { + const packetOffset = 8 + (i * packetSize) + const packetBuffer = buffer.slice(packetOffset, packetOffset + packetSize) + packets.push(this.parseDataPacket(packetBuffer)) + } + + return { + packets, + count: packetCount, + newOffset + } + } + + private parseKeyPair(buffer: any): KeyPair { + const encryptionPub = new Uint8Array(buffer, 0, 32) + const encryptionPriv = new Uint8Array(buffer, 32, 32) + const signingPub = new Uint8Array(buffer, 64, 32) + const signingPriv = new Uint8Array(buffer, 96, 64) + + return { + encryptionPub, + encryptionPriv, + signingPub, + signingPriv + } + } + + private serializePacket(packet: DataPacket): any { + const packetSize = 4 + 32 + (1 + 8 + 32 + 32 + 1 + (32 * 32) + 65536 + 64) + const buffer = new ArrayBuffer(packetSize) + const view = new DataView(buffer) + let offset = 0 + + view.setUint32(offset, packet.nonce, true) + offset += 4 + + new Uint8Array(buffer, offset, 32).set(packet.hash) + offset += 32 + + view.setUint8(offset++, packet.contents.encrypted ? 1 : 0) + view.setBigUint64(offset, packet.contents.createdTimeMs, true) + offset += 8 + + new Uint8Array(buffer, offset, 32).set(packet.contents.author) + offset += 32 + new Uint8Array(buffer, offset, 32).set(packet.contents.authorSigningPub) + offset += 32 + + view.setUint8(offset++, packet.contents.recipientCount) + for (let i = 0; i < 32; i++) { + if (i < packet.contents.recipients.length) { + new Uint8Array(buffer, offset, 32).set(packet.contents.recipients[i]) + } + offset += 32 + } + + new Uint8Array(buffer, offset, 65536).set(packet.contents.data) + offset += 65536 + + new Uint8Array(buffer, offset, 64).set(packet.contents.signature) + + return buffer + } + + private serializeRecipients(recipients: Uint8Array[]): any { + const buffer = new ArrayBuffer(32 * 32) // MAX_RECIPIENTS * 32 bytes each + for (let i = 0; i < Math.min(recipients.length, 32); i++) { + new Uint8Array(buffer, i * 32, 32).set(recipients[i]) + } + return buffer + } + + private serializeKeyPair(keyPair: KeyPair): any { + const buffer = new ArrayBuffer(32 + 32 + 32 + 64) // total keypair size + let offset = 0 + + new Uint8Array(buffer, offset, 32).set(keyPair.encryptionPub) + offset += 32 + new Uint8Array(buffer, offset, 32).set(keyPair.encryptionPriv) + offset += 32 + new Uint8Array(buffer, offset, 32).set(keyPair.signingPub) + offset += 32 + new Uint8Array(buffer, offset, 64).set(keyPair.signingPriv) + + return buffer + } + + private readBool(ptr: any): boolean { + return this.readUint8(ptr) !== 0 + } + + private readUint8(ptr: any): number { + switch (this.runtime) { + case 'bun': + case 'deno': + return new Uint8Array(ptr)[0] + case 'node': + return ptr.readUInt8(0) + default: + return 0 + } + } +} + +// Main client class +export class MoonpasteClient { + private ffi: MoonpasteFFI + + constructor() { + this.ffi = new MoonpasteFFI() + } + + async getServerInfo(ip: string, port: number): Promise { + return this.ffi.getServerInfo(ip, port) + } + + async getNeighbors(ip: string, port: number, count: number = 5): Promise { + return this.ffi.getServerNeighbors(ip, port, count) + } + + async pushPacket(ip: string, port: number, packet: DataPacket): Promise { + return this.ffi.pushPacket(ip, port, packet) + } + + async getPacket(ip: string, port: number, hash: Uint8Array): Promise { + return this.ffi.getPacket(ip, port, hash) + } + + async getFirehosePackets(ip: string, port: number, offset: bigint = 0n, count: number = 10): Promise { + return this.ffi.getFirehosePackets(ip, port, offset, count) + } + + createPacket(message: string | Uint8Array, recipients?: Uint8Array[], authorKeys?: KeyPair): DataPacket { + const data = typeof message === 'string' ? new TextEncoder().encode(message) : message + const keys = authorKeys || this.generateKeyPair() + const recipientCount = recipients ? recipients.length : 0 + + return this.ffi.createPacketFromBytes(data, recipientCount, recipients || null, keys) + } + + async createPacketFromFile(filename: string, recipients?: Uint8Array[], authorKeys?: KeyPair): Promise { + const keys = authorKeys || this.generateKeyPair() + const recipientCount = recipients ? recipients.length : 0 + + return this.ffi.createPacketFromFile(filename, recipientCount, recipients || null, keys) + } + + runProofOfWork(packet: DataPacket, leadingZeros: number): DataPacket { + return this.ffi.runProofOfWork(packet, leadingZeros) + } + + generateKeyPair(): KeyPair { + return this.ffi.generateKeyPair() + } + + // Utility methods + static hashToHex(hash: Uint8Array): string { + return Array.from(hash).map(b => b.toString(16).padStart(2, '0')).join('') + } + + static hexToHash(hex: string): Uint8Array { + const result = new Uint8Array(32) + for (let i = 0; i < 32; i++) { + result[i] = parseInt(hex.substr(i * 2, 2), 16) + } + return result + } + + static formatIpAddress(neighbor: RelayNeighbor): string { + if (neighbor.hasIpv4) { + return Array.from(neighbor.v4Addr).join('.') + } else if (neighbor.hasIpv6) { + const parts: string[] = [] + for (let i = 0; i < 8; i++) { + const word = (neighbor.v6Addr[i * 2] << 8) | neighbor.v6Addr[i * 2 + 1] + parts.push(word.toString(16)) + } + return parts.join(':') + } + return 'N/A' + } +} + +export default MoonpasteClient \ No newline at end of file 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 diff --git a/client/client.ts b/client/client.ts deleted file mode 100644 index 4f6031a..0000000 --- a/client/client.ts +++ /dev/null @@ -1,674 +0,0 @@ -// moonpaste-client.ts -import { dlopen, FFIType, JSCallback, suffix } from 'bun:ffi' -import * as path from 'path' -import * as fs from 'fs' - -// Type definitions -export interface RelayNeighbor { - active: boolean - hasIpv4: boolean - hasIpv6: boolean - v4Addr: Uint8Array // 4 bytes - v6Addr: Uint8Array // 16 bytes -} - -export interface RelayCapabilities { - retentionHours: number - canStore: boolean - leadingZeros: number - acceptsPlain: boolean -} - -export interface RelayStatistics { - neighbors: number - heldPackets: bigint - quickConnect: RelayNeighbor[] // 4 elements -} - -export interface RelayInfo { - majorVersion: number - minorVersion: number - motd: string // 128 bytes max - capabilities: RelayCapabilities - statistics: RelayStatistics -} - -export interface DataPacketContents { - encrypted: boolean - createdTimeMs: bigint - author: Uint8Array // 32 bytes - authorSigningPub: Uint8Array // 32 bytes - recipientCount: number - recipients: Uint8Array[] // 32 arrays of 32 bytes each - data: Uint8Array // 65536 bytes - signature: Uint8Array // 64 bytes -} - -export interface DataPacket { - nonce: number - hash: Uint8Array // 32 bytes - contents: DataPacketContents -} - -export interface KeyPair { - encryptionPub: Uint8Array // 32 bytes - encryptionPriv: Uint8Array // 32 bytes - signingPub: Uint8Array // 32 bytes - signingPriv: Uint8Array // 64 bytes -} - -export interface DataPacketList { - packets: DataPacket[] - count: number - newOffset: bigint -} - -export enum ClientError { - Success = 0, - NetworkFail = 1, - InvalidAddress = 2, - ConnectionFailed = 3, - SendFailed = 4, - RecvFailed = 5, - ServerBusy = 6, - InvalidResponse = 7, - DeserializeFailed = 8, - AllocFailed = 9, - FileNotFound = 10, - FileReadFailed = 11, - BadArgument = 12 -} - -function getRuntime(): 'bun' | 'deno' | 'node' { - if (typeof Bun !== 'undefined') return 'bun' - if (typeof Deno !== 'undefined') return 'deno' - return 'node' -} - -function findLibrary(): string { - const runtime = getRuntime() - const libNames = ['libmpclient.so', 'libmpclient.dylib', 'libmpclient.dll'] - const searchPaths = [ - './', - './lib/', - '../lib/', - '/usr/local/lib/', - '/usr/lib/', - process.env.MOONPASTE_LIB_PATH || '' - ].filter(p => p.length > 0) - - for (const searchPath of searchPaths) { - for (const libName of libNames) { - const fullPath = path.join(searchPath, libName) - if (fs.existsSync(fullPath)) { - return fullPath - } - } - } - - throw new Error('Could not find moonpaste client library. Searched for: ' + libNames.join(', ')) -} - -class MoonpasteFFI { - private lib: any - private runtime: string - - constructor() { - this.runtime = getRuntime() - const libPath = findLibrary() - - switch (this.runtime) { - case 'bun': - this.lib = dlopen(libPath, { - mp_get_server_info: { - args: [FFIType.cstring, FFIType.i32, FFIType.ptr], - returns: FFIType.i32 - }, - mp_get_server_neighbors: { - args: [FFIType.cstring, FFIType.i32, FFIType.u8, FFIType.ptr, FFIType.ptr], - returns: FFIType.i32 - }, - mp_push_packet: { - args: [FFIType.cstring, FFIType.i32, FFIType.ptr, FFIType.ptr], - returns: FFIType.i32 - }, - mp_get_packet: { - args: [FFIType.cstring, FFIType.i32, FFIType.ptr, FFIType.ptr], - returns: FFIType.i32 - }, - mp_get_firehose_packets: { - args: [FFIType.cstring, FFIType.i32, FFIType.u64, FFIType.u16, FFIType.ptr], - returns: FFIType.i32 - }, - mp_create_packet_from_bytes: { - args: [FFIType.ptr, FFIType.usize, FFIType.u8, FFIType.ptr, FFIType.ptr], - returns: FFIType.ptr - }, - mp_create_packet_from_file: { - args: [FFIType.cstring, FFIType.u8, FFIType.ptr, FFIType.ptr, FFIType.ptr], - returns: FFIType.i32 - }, - mp_run_pow: { - args: [FFIType.ptr, FFIType.u8], - returns: FFIType.ptr - }, - mp_generate_keypair: { - args: [], - returns: FFIType.ptr - } - }) - break - - case 'deno': - this.lib = Deno.dlopen(libPath, { - mp_get_server_info: { - parameters: ['buffer', 'i32', 'pointer'], - result: 'i32' - }, - mp_get_server_neighbors: { - parameters: ['buffer', 'i32', 'u8', 'pointer', 'pointer'], - result: 'i32' - }, - mp_push_packet: { - parameters: ['buffer', 'i32', 'pointer', 'pointer'], - result: 'i32' - }, - mp_get_packet: { - parameters: ['buffer', 'i32', 'pointer', 'pointer'], - result: 'i32' - }, - mp_get_firehose_packets: { - parameters: ['buffer', 'i32', 'u64', 'u16', 'pointer'], - result: 'i32' - }, - mp_create_packet_from_bytes: { - parameters: ['pointer', 'usize', 'u8', 'pointer', 'pointer'], - result: 'pointer' - }, - mp_create_packet_from_file: { - parameters: ['buffer', 'u8', 'pointer', 'pointer', 'pointer'], - result: 'i32' - }, - mp_run_pow: { - parameters: ['pointer', 'u8'], - result: 'pointer' - }, - mp_generate_keypair: { - parameters: [], - result: 'pointer' - } - }) - break - - case 'node': - try { - const ffi = require('ffi-napi') // dependency - const ref = require('ref-napi') // dependency - - this.lib = ffi.Library(libPath, { - mp_get_server_info: ['int', ['string', 'int', 'pointer']], - mp_get_server_neighbors: ['int', ['string', 'int', 'uint8', 'pointer', 'pointer']], - mp_push_packet: ['int', ['string', 'int', 'pointer', 'pointer']], - mp_get_packet: ['int', ['string', 'int', 'pointer', 'pointer']], - mp_get_firehose_packets: ['int', ['string', 'int', 'uint64', 'uint16', 'pointer']], - mp_create_packet_from_bytes: ['pointer', ['pointer', 'size_t', 'uint8', 'pointer', 'pointer']], - mp_create_packet_from_file: ['int', ['string', 'uint8', 'pointer', 'pointer', 'pointer']], - mp_run_pow: ['pointer', ['pointer', 'uint8']], - mp_generate_keypair: ['pointer', []] - }) - } catch (e) { - throw new Error('Node.js FFI requires ffi-napi and ref-napi packages: npm install ffi-napi ref-napi') - } - break - } - } - - private allocateBuffer(size: number): any { - switch (this.runtime) { - case 'bun': - return new ArrayBuffer(size) - case 'deno': - return new ArrayBuffer(size) - case 'node': - const ref = require('ref-napi') - return ref.alloc(size) - } - } - - private stringToBuffer(str: string): any { - switch (this.runtime) { - case 'bun': - case 'deno': - return new TextEncoder().encode(str + '\0') - case 'node': - return Buffer.from(str + '\0', 'utf8') - } - } - - async getServerInfo(ip: string, port: number): Promise { - const infoBuffer = this.allocateBuffer(1024) - const ipBuffer = this.stringToBuffer(ip) - - const result = this.lib.symbols.mp_get_server_info(ipBuffer, port, infoBuffer) - - if (result !== ClientError.Success) { - throw new Error(`Failed to get server info: ${result}`) - } - - return this.parseRelayInfo(infoBuffer) - } - - async getServerNeighbors(ip: string, port: number, count: number): Promise { - const neighborsPtr = this.allocateBuffer(8) // pointer - const countPtr = this.allocateBuffer(1) // uint8 - const ipBuffer = this.stringToBuffer(ip) - - const result = this.lib.symbols.mp_get_server_neighbors(ipBuffer, port, count, neighborsPtr, countPtr) - - if (result !== ClientError.Success) { - throw new Error(`Failed to get neighbors: ${result}`) - } - - return this.parseNeighbors(neighborsPtr, countPtr) - } - - async pushPacket(ip: string, port: number, packet: DataPacket): Promise { - const successPtr = this.allocateBuffer(1) - const ipBuffer = this.stringToBuffer(ip) - const packetBuffer = this.serializePacket(packet) - - const result = this.lib.symbols.mp_push_packet(ipBuffer, port, packetBuffer, successPtr) - - if (result !== ClientError.Success) { - throw new Error(`Failed to push packet: ${result}`) - } - - return this.readBool(successPtr) - } - - async getPacket(ip: string, port: number, hash: Uint8Array): Promise { - const packetBuffer = this.allocateBuffer(70000) // approximation - const ipBuffer = this.stringToBuffer(ip) - - const result = this.lib.symbols.mp_get_packet(ipBuffer, port, hash, packetBuffer) - - if (result !== ClientError.Success) { - throw new Error(`Failed to get packet: ${result}`) - } - - return this.parseDataPacket(packetBuffer) - } - - async getFirehosePackets(ip: string, port: number, offset: bigint, count: number): Promise { - const listBuffer = this.allocateBuffer(1024) - const ipBuffer = this.stringToBuffer(ip) - - const result = this.lib.symbols.mp_get_firehose_packets(ipBuffer, port, offset, count, listBuffer) - - if (result !== ClientError.Success) { - throw new Error(`Failed to get firehose packets: ${result}`) - } - - return this.parseDataPacketList(listBuffer) - } - - createPacketFromBytes(input: Uint8Array, recipientCount: number, recipients: Uint8Array[] | null, authorKeys: KeyPair): DataPacket { - const recipientsBuffer = recipients ? this.serializeRecipients(recipients) : null - const keysBuffer = this.serializeKeyPair(authorKeys) - - const result = this.lib.symbols.mp_create_packet_from_bytes( - input, - input.length, - recipientCount, - recipientsBuffer, - keysBuffer - ) - - return this.parseDataPacket(result) - } - - async createPacketFromFile(filename: string, recipientCount: number, recipients: Uint8Array[] | null, authorKeys: KeyPair): Promise { - const packetBuffer = this.allocateBuffer(4096) - const filenameBuffer = this.stringToBuffer(filename) - const recipientsBuffer = recipients ? this.serializeRecipients(recipients) : null - const keysBuffer = this.serializeKeyPair(authorKeys) - - const result = this.lib.symbols.mp_create_packet_from_file( - filenameBuffer, - recipientCount, - recipientsBuffer, - keysBuffer, - packetBuffer - ) - - if (result !== ClientError.Success) { - throw new Error(`Failed to create packet from file: ${result}`) - } - - return this.parseDataPacket(packetBuffer) - } - - runProofOfWork(packet: DataPacket, leadingZeros: number): DataPacket { - const packetBuffer = this.serializePacket(packet) - const result = this.lib.symbols.mp_run_pow(packetBuffer, leadingZeros) - return this.parseDataPacket(result) - } - - generateKeyPair(): KeyPair { - const result = this.lib.symbols.mp_generate_keypair() - return this.parseKeyPair(result) - } - - private parseRelayInfo(buffer: any): RelayInfo { - const view = new DataView(buffer instanceof ArrayBuffer ? buffer : buffer.buffer) - let offset = 0 - - const majorVersion = view.getUint8(offset++) - const minorVersion = view.getUint8(offset++) - - const motdBytes = new Uint8Array(buffer, offset, 128) - const motd = new TextDecoder().decode(motdBytes).replace(/\0.*$/, '') - offset += 128 - - const retentionHours = view.getUint16(offset, true) // little endian - offset += 2 - const canStore = view.getUint8(offset++) !== 0 - const leadingZeros = view.getUint8(offset++) - const acceptsPlain = view.getUint8(offset++) !== 0 - - const neighbors = view.getUint16(offset, true) - offset += 2 - const heldPackets = view.getBigUint64(offset, true) - offset += 8 - - const quickConnect: RelayNeighbor[] = [] - for (let i = 0; i < 4; i++) { - quickConnect.push(this.parseRelayNeighborAt(buffer, offset)) - offset += 23 // 1 + 1 + 1 + 4 + 16 - } - - return { - majorVersion, - minorVersion, - motd, - capabilities: { - retentionHours, - canStore, - leadingZeros, - acceptsPlain - }, - statistics: { - neighbors, - heldPackets, - quickConnect - } - } - } - - private parseRelayNeighborAt(buffer: any, offset: number): RelayNeighbor { - const view = new DataView(buffer instanceof ArrayBuffer ? buffer : buffer.buffer) - - const active = view.getUint8(offset) !== 0 - const hasIpv4 = view.getUint8(offset + 1) !== 0 - const hasIpv6 = view.getUint8(offset + 2) !== 0 - - const v4Addr = new Uint8Array(buffer, offset + 3, 4) - const v6Addr = new Uint8Array(buffer, offset + 7, 16) - - return { active, hasIpv4, hasIpv6, v4Addr, v6Addr } - } - - private parseNeighbors(neighborsPtr: any, countPtr: any): RelayNeighbor[] { - const count = this.readUint8(countPtr) - const neighbors: RelayNeighbor[] = [] - - if (count > 0 && neighborsPtr) { - for (let i = 0; i < count; i++) { - const neighborOffset = i * 23 // RELAY_NEIGHBOR_WIRE_LEN - neighbors.push(this.parseRelayNeighborAt(neighborsPtr, neighborOffset)) - } - } - - return neighbors - } - - private parseDataPacket(buffer: any): DataPacket { - const view = new DataView(buffer instanceof ArrayBuffer ? buffer : buffer.buffer) - let offset = 0 - - const nonce = view.getUint32(offset, true) - offset += 4 - - const hash = new Uint8Array(buffer, offset, 32) - offset += 32 - - const encrypted = view.getUint8(offset++) !== 0 - const createdTimeMs = view.getBigUint64(offset, true) - offset += 8 - - const author = new Uint8Array(buffer, offset, 32) - offset += 32 - const authorSigningPub = new Uint8Array(buffer, offset, 32) - offset += 32 - - const recipientCount = view.getUint8(offset++) - const recipients: Uint8Array[] = [] - for (let i = 0; i < 32; i++) { - recipients.push(new Uint8Array(buffer, offset, 32)) - offset += 32 - } - - const data = new Uint8Array(buffer, offset, 65536) - offset += 65536 - - const signature = new Uint8Array(buffer, offset, 64) - - return { - nonce, - hash, - contents: { - encrypted, - createdTimeMs, - author, - authorSigningPub, - recipientCount, - recipients, - data, - signature - } - } - } - - private parseDataPacketList(buffer: any): DataPacketList { - const view = new DataView(buffer instanceof ArrayBuffer ? buffer : buffer.buffer) - - const newOffset = view.getBigUint64(0, true) - const availableBytes = buffer.byteLength - 8 - const packetSize = 4 + 32 + (1 + 8 + 32 + 32 + 1 + (32 * 32) + 65536 + 64) // PACKET_SIZE - const packetCount = Math.floor(availableBytes / packetSize) - - const packets: DataPacket[] = [] - for (let i = 0; i < packetCount; i++) { - const packetOffset = 8 + (i * packetSize) - const packetBuffer = buffer.slice(packetOffset, packetOffset + packetSize) - packets.push(this.parseDataPacket(packetBuffer)) - } - - return { - packets, - count: packetCount, - newOffset - } - } - - private parseKeyPair(buffer: any): KeyPair { - const encryptionPub = new Uint8Array(buffer, 0, 32) - const encryptionPriv = new Uint8Array(buffer, 32, 32) - const signingPub = new Uint8Array(buffer, 64, 32) - const signingPriv = new Uint8Array(buffer, 96, 64) - - return { - encryptionPub, - encryptionPriv, - signingPub, - signingPriv - } - } - - private serializePacket(packet: DataPacket): any { - const packetSize = 4 + 32 + (1 + 8 + 32 + 32 + 1 + (32 * 32) + 65536 + 64) - const buffer = new ArrayBuffer(packetSize) - const view = new DataView(buffer) - let offset = 0 - - view.setUint32(offset, packet.nonce, true) - offset += 4 - - new Uint8Array(buffer, offset, 32).set(packet.hash) - offset += 32 - - view.setUint8(offset++, packet.contents.encrypted ? 1 : 0) - view.setBigUint64(offset, packet.contents.createdTimeMs, true) - offset += 8 - - new Uint8Array(buffer, offset, 32).set(packet.contents.author) - offset += 32 - new Uint8Array(buffer, offset, 32).set(packet.contents.authorSigningPub) - offset += 32 - - view.setUint8(offset++, packet.contents.recipientCount) - for (let i = 0; i < 32; i++) { - if (i < packet.contents.recipients.length) { - new Uint8Array(buffer, offset, 32).set(packet.contents.recipients[i]) - } - offset += 32 - } - - new Uint8Array(buffer, offset, 65536).set(packet.contents.data) - offset += 65536 - - new Uint8Array(buffer, offset, 64).set(packet.contents.signature) - - return buffer - } - - private serializeRecipients(recipients: Uint8Array[]): any { - const buffer = new ArrayBuffer(32 * 32) // MAX_RECIPIENTS * 32 bytes each - for (let i = 0; i < Math.min(recipients.length, 32); i++) { - new Uint8Array(buffer, i * 32, 32).set(recipients[i]) - } - return buffer - } - - private serializeKeyPair(keyPair: KeyPair): any { - const buffer = new ArrayBuffer(32 + 32 + 32 + 64) // total keypair size - let offset = 0 - - new Uint8Array(buffer, offset, 32).set(keyPair.encryptionPub) - offset += 32 - new Uint8Array(buffer, offset, 32).set(keyPair.encryptionPriv) - offset += 32 - new Uint8Array(buffer, offset, 32).set(keyPair.signingPub) - offset += 32 - new Uint8Array(buffer, offset, 64).set(keyPair.signingPriv) - - return buffer - } - - private readBool(ptr: any): boolean { - return this.readUint8(ptr) !== 0 - } - - private readUint8(ptr: any): number { - switch (this.runtime) { - case 'bun': - case 'deno': - return new Uint8Array(ptr)[0] - case 'node': - return ptr.readUInt8(0) - default: - return 0 - } - } -} - -// Main client class -export class MoonpasteClient { - private ffi: MoonpasteFFI - - constructor() { - this.ffi = new MoonpasteFFI() - } - - async getServerInfo(ip: string, port: number): Promise { - return this.ffi.getServerInfo(ip, port) - } - - async getNeighbors(ip: string, port: number, count: number = 5): Promise { - return this.ffi.getServerNeighbors(ip, port, count) - } - - async pushPacket(ip: string, port: number, packet: DataPacket): Promise { - return this.ffi.pushPacket(ip, port, packet) - } - - async getPacket(ip: string, port: number, hash: Uint8Array): Promise { - return this.ffi.getPacket(ip, port, hash) - } - - async getFirehosePackets(ip: string, port: number, offset: bigint = 0n, count: number = 10): Promise { - return this.ffi.getFirehosePackets(ip, port, offset, count) - } - - createPacket(message: string | Uint8Array, recipients?: Uint8Array[], authorKeys?: KeyPair): DataPacket { - const data = typeof message === 'string' ? new TextEncoder().encode(message) : message - const keys = authorKeys || this.generateKeyPair() - const recipientCount = recipients ? recipients.length : 0 - - return this.ffi.createPacketFromBytes(data, recipientCount, recipients || null, keys) - } - - async createPacketFromFile(filename: string, recipients?: Uint8Array[], authorKeys?: KeyPair): Promise { - const keys = authorKeys || this.generateKeyPair() - const recipientCount = recipients ? recipients.length : 0 - - return this.ffi.createPacketFromFile(filename, recipientCount, recipients || null, keys) - } - - runProofOfWork(packet: DataPacket, leadingZeros: number): DataPacket { - return this.ffi.runProofOfWork(packet, leadingZeros) - } - - generateKeyPair(): KeyPair { - return this.ffi.generateKeyPair() - } - - // Utility methods - static hashToHex(hash: Uint8Array): string { - return Array.from(hash).map(b => b.toString(16).padStart(2, '0')).join('') - } - - static hexToHash(hex: string): Uint8Array { - const result = new Uint8Array(32) - for (let i = 0; i < 32; i++) { - result[i] = parseInt(hex.substr(i * 2, 2), 16) - } - return result - } - - static formatIpAddress(neighbor: RelayNeighbor): string { - if (neighbor.hasIpv4) { - return Array.from(neighbor.v4Addr).join('.') - } else if (neighbor.hasIpv6) { - const parts: string[] = [] - for (let i = 0; i < 8; i++) { - const word = (neighbor.v6Addr[i * 2] << 8) | neighbor.v6Addr[i * 2 + 1] - parts.push(word.toString(16)) - } - return parts.join(':') - } - return 'N/A' - } -} - -export default MoonpasteClient \ No newline at end of file -- cgit v1.2.3