diff options
| -rw-r--r-- | client/client.py | 683 | ||||
| -rw-r--r-- | client/client.ts | 674 |
2 files changed, 1357 insertions, 0 deletions
diff --git a/client/client.py b/client/client.py new file mode 100644 index 0000000..56a8faa --- /dev/null +++ b/client/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/client.ts b/client/client.ts new file mode 100644 index 0000000..4f6031a --- /dev/null +++ b/client/client.ts @@ -0,0 +1,674 @@ +// 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<RelayInfo> { + 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<RelayNeighbor[]> { + 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<boolean> { + 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<DataPacket> { + 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<DataPacketList> { + 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<DataPacket> { + 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<RelayInfo> { + return this.ffi.getServerInfo(ip, port) + } + + async getNeighbors(ip: string, port: number, count: number = 5): Promise<RelayNeighbor[]> { + return this.ffi.getServerNeighbors(ip, port, count) + } + + async pushPacket(ip: string, port: number, packet: DataPacket): Promise<boolean> { + return this.ffi.pushPacket(ip, port, packet) + } + + async getPacket(ip: string, port: number, hash: Uint8Array): Promise<DataPacket> { + return this.ffi.getPacket(ip, port, hash) + } + + async getFirehosePackets(ip: string, port: number, offset: bigint = 0n, count: number = 10): Promise<DataPacketList> { + 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<DataPacket> { + 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 |
