From 50a397c8c7d8df8ea50943d7aa9f97d45ffdbc12 Mon Sep 17 00:00:00 2001 From: moon Date: Sun, 14 Sep 2025 19:49:26 +0330 Subject: better (independent) client --- client/3rd-party/client.ts | 1157 ++++++++++++++++++++------------------------ 1 file changed, 521 insertions(+), 636 deletions(-) (limited to 'client') diff --git a/client/3rd-party/client.ts b/client/3rd-party/client.ts index 711dc3b..34bfc91 100644 --- a/client/3rd-party/client.ts +++ b/client/3rd-party/client.ts @@ -1,696 +1,581 @@ -// 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 +// ai generated, do not trust + +const MAX_BUFFER = 1 << 18; +const MAX_BODY = 1 << 22; +const MAX_RECIPIENTS = 32; +const PACKET_SIZE = 4 + 32 + (1 + 8 + 32 + 32 + 1 + (32 * MAX_RECIPIENTS) + 65536 + 64); +const RELAY_NEIGHBOR_WIRE_LEN = 1 + 1 + 1 + 4 + 16 + 2; + +enum MpClientError { + SUCCESS = 0, + NETWORK_FAIL, + CONNECTION_FAILED, + SERVER_BUSY, + INVALID_RESPONSE, + DESERIALIZE_FAILED, + STRING_ENCODE_FAILED, + BAD_ARGUMENT, + RECV_FAILED } -export interface RelayCapabilities { - retentionHours: number - canStore: boolean - leadingZeros: number - acceptsPlain: boolean +interface RelayNeighbor { + active: boolean; + has_ipv4: boolean; + has_ipv6: boolean; + v4_addr: Uint8Array; + v6_addr: Uint8Array; + port: number; } -export interface RelayStatistics { - neighbors: number - heldPackets: bigint - quickConnect: RelayNeighbor[] // 4 elements +interface RelayInfo { + major_version: number; + minor_version: number; + motd: Uint8Array; + capabilities: { + retention_hours: number; + can_store: boolean; + leading_zeros: number; + accepts_plain: boolean; + }; + statistics: { + neighbors: number; + held_packets: bigint; + quick_connect: RelayNeighbor[]; + }; } -export interface RelayInfo { - majorVersion: number - minorVersion: number - motd: string // 128 bytes max - capabilities: RelayCapabilities - statistics: RelayStatistics +interface DataPacketContents { + encrypted: boolean; + created_time_ms: bigint; + author: Uint8Array; + author_signing_pub: Uint8Array; + recipient_count: number; + recipients: Uint8Array[]; + data: Uint8Array; + signature: Uint8Array; } -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 +interface DataPacket { + nonce: number; + hash: Uint8Array; + contents: DataPacketContents; } -export interface DataPacket { - nonce: number - hash: Uint8Array // 32 bytes - contents: DataPacketContents +interface DataPacketList { + packets: DataPacket[]; + count: number; + new_offset: bigint; } -export interface KeyPair { - encryptionPub: Uint8Array // 32 bytes - encryptionPriv: Uint8Array // 32 bytes - signingPub: Uint8Array // 32 bytes - signingPriv: Uint8Array // 64 bytes +interface KeyPair { + encryption_pub: Uint8Array; + encryption_priv: Uint8Array; + signing_pub: Uint8Array; + signing_priv: Uint8Array; } -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 -} +class MoonPasteClient { + private ip: string | null = null; + private port: number | null = null; -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 - } + connect(ip: string, port: number): void { + this.ip = ip; + this.port = port; } - } - - 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 - }, - mp_query_author_packets: { - args: [FFIType.cstring, FFIType.i32, FFIType.ptr, FFIType.u16, FFIType.u64, FFIType.ptr], - returns: FFIType.i32 - }, - }) - 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') + private checkConnected(): boolean { + return this.ip !== null && this.port !== null; } - } - 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}`) + private readU16LE(buffer: Uint8Array, offset: number): number { + return buffer[offset] | (buffer[offset + 1] << 8); } - 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}`) + private writeU16LE(buffer: Uint8Array, value: number, offset: number): void { + buffer[offset] = value & 0xFF; + buffer[offset + 1] = (value >> 8) & 0xFF; } - 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}`) + private writeU32LE(buffer: Uint8Array, value: number, offset: number): void { + buffer[offset] = value & 0xFF; + buffer[offset + 1] = (value >> 8) & 0xFF; + buffer[offset + 2] = (value >> 16) & 0xFF; + buffer[offset + 3] = (value >> 24) & 0xFF; } - 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}`) + private readU32LE(buffer: Uint8Array, offset: number): number { + return buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24); } - 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}`) + private readU64LE(buffer: Uint8Array, offset: number): bigint { + return ( + BigInt(buffer[offset]) | + (BigInt(buffer[offset + 1]) << 8n) | + (BigInt(buffer[offset + 2]) << 16n) | + (BigInt(buffer[offset + 3]) << 24n) | + (BigInt(buffer[offset + 4]) << 32n) | + (BigInt(buffer[offset + 5]) << 40n) | + (BigInt(buffer[offset + 6]) << 48n) | + (BigInt(buffer[offset + 7]) << 56n) + ); } - return this.parseDataPacketList(listBuffer) - } - - async queryAuthorPackets(ip: string, port: number, authorKeys: KeyPair, count: number, offset: bigint): Promise { - const keysBuffer = this.serializeKeyPair(authorKeys) - const listBuffer = this.allocateBuffer(1024) - const ipBuffer = this.stringToBuffer(ip) - - const result = this.lib.symbols.mp_query_author_packets(ipBuffer, port, keysBuffer, count, offset, listBuffer) - - if (result !== ClientError.Success) { - throw new Error(`Failed to query author packets: ${result}`) + private writeU64LE(buffer: Uint8Array, value: bigint, offset: number): void { + buffer[offset] = Number(value & 0xFFn); + buffer[offset + 1] = Number((value >> 8n) & 0xFFn); + buffer[offset + 2] = Number((value >> 16n) & 0xFFn); + buffer[offset + 3] = Number((value >> 24n) & 0xFFn); + buffer[offset + 4] = Number((value >> 32n) & 0xFFn); + buffer[offset + 5] = Number((value >> 40n) & 0xFFn); + buffer[offset + 6] = Number((value >> 48n) & 0xFFn); + buffer[offset + 7] = Number((value >> 56n) & 0xFFn); } - 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}`) + private checkLeadingZeros(hash: Uint8Array, requiredZeros: number): boolean { + if (requiredZeros === 0) return true; + const fullBytes = Math.floor(requiredZeros / 8); + const remainingBits = requiredZeros % 8; + for (let i = 0; i < fullBytes; i++) { + if (hash[i] !== 0) return false; + } + if (remainingBits > 0) { + const mask = 0xFF << (8 - remainingBits); + if ((hash[fullBytes] & mask) !== 0) return false; + } + return true; } - 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 + private async sha256(data: Uint8Array): Promise { + const digest = await crypto.subtle.digest('SHA-256', data); + return new Uint8Array(digest); } - return { - majorVersion, - minorVersion, - motd, - capabilities: { - retentionHours, - canStore, - leadingZeros, - acceptsPlain - }, - statistics: { - neighbors, - heldPackets, - quickConnect - } + async generateKeyPair(): Promise { + const { publicKey: encPub, privateKey: encPriv } = await crypto.subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['sign', 'verify'] + ); + const { publicKey: signPub, privateKey: signPriv } = await crypto.subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['sign', 'verify'] + ); + const encPubBytes = new Uint8Array(await crypto.subtle.exportKey('raw', encPub)); + const encPrivBytes = new Uint8Array(await crypto.subtle.exportKey('raw', encPriv)); + const signPubBytes = new Uint8Array(await crypto.subtle.exportKey('raw', signPub)); + const signPrivBytes = new Uint8Array(await crypto.subtle.exportKey('raw', signPriv)); + return { + encryption_pub: encPubBytes.slice(0, 32), + encryption_priv: encPrivBytes.slice(0, 32), + signing_pub: signPubBytes.slice(0, 32), + signing_priv: signPrivBytes.slice(0, 32), + }; } - } - - 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)) - } + private async signData(data: Uint8Array, privateKey: Uint8Array): Promise { + const key = await crypto.subtle.importKey( + 'raw', + privateKey, + { name: 'ECDSA', namedCurve: 'P-256' }, + false, + ['sign'] + ); + const signature = await crypto.subtle.sign( + { name: 'ECDSA', hash: 'SHA-256' }, + key, + data + ); + return new Uint8Array(signature).slice(0, 64); } - 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 + private s2relayNeighbor(len: number, stream: Uint8Array): { neighbor: RelayNeighbor; success: boolean } { + if (len !== RELAY_NEIGHBOR_WIRE_LEN) { + return { neighbor: { active: false, has_ipv4: false, has_ipv6: false, v4_addr: new Uint8Array(4), v6_addr: new Uint8Array(16), port: 0 }, success: false }; + } + const neighbor: RelayNeighbor = { + active: stream[0] !== 0, + has_ipv4: stream[1] !== 0, + has_ipv6: stream[2] !== 0, + v4_addr: stream.slice(3, 7), + v6_addr: stream.slice(7, 23), + port: this.readU16LE(stream, 23), + }; + return { neighbor, success: true }; } - 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 s2relayInfo(len: number, stream: Uint8Array): { info: RelayInfo; success: boolean } { + const expectedLen = 1 + 1 + 128 + 2 + 1 + 1 + 1 + 2 + 8 + (4 * RELAY_NEIGHBOR_WIRE_LEN); + if (len !== expectedLen) { + return { info: { major_version: 0, minor_version: 0, motd: new Uint8Array(128), capabilities: { retention_hours: 0, can_store: false, leading_zeros: 0, accepts_plain: false }, statistics: { neighbors: 0, held_packets: 0n, quick_connect: [] } }, success: false }; + } + let off = 0; + const info: RelayInfo = { + major_version: stream[off++], + minor_version: stream[off++], + motd: stream.slice(off, off + 128), + capabilities: { + retention_hours: this.readU16LE(stream, off), + can_store: stream[off + 2] !== 0, + leading_zeros: stream[off + 3], + accepts_plain: stream[off + 4] !== 0, + }, + statistics: { + neighbors: this.readU16LE(stream, off + 5), + held_packets: this.readU64LE(stream, off + 7), + quick_connect: [], + }, + }; + off += 15; + for (let i = 0; i < 4; i++) { + const { neighbor, success } = this.s2relayNeighbor(RELAY_NEIGHBOR_WIRE_LEN, stream.slice(off, off + RELAY_NEIGHBOR_WIRE_LEN)); + if (!success) { + return { info: { major_version: 0, minor_version: 0, motd: new Uint8Array(128), capabilities: { retention_hours: 0, can_store: false, leading_zeros: 0, accepts_plain: false }, statistics: { neighbors: 0, held_packets: 0n, quick_connect: [] } }, success: false }; + } + info.statistics.quick_connect.push(neighbor); + off += RELAY_NEIGHBOR_WIRE_LEN; + } + return { info, success: true }; } - } - - 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)) + private s2dataPacket(len: number, stream: Uint8Array): { packet: DataPacket; success: boolean } { + if (len !== PACKET_SIZE) { + return { packet: { nonce: 0, hash: new Uint8Array(32), contents: { encrypted: false, created_time_ms: 0n, author: new Uint8Array(32), author_signing_pub: new Uint8Array(32), recipient_count: 0, recipients: [], data: new Uint8Array(65536), signature: new Uint8Array(64) } }, success: false }; + } + let off = 0; + const packet: DataPacket = { + nonce: this.readU32LE(stream, off), + hash: stream.slice(off + 4, off + 36), + contents: { + encrypted: stream[off + 36] !== 0, + created_time_ms: this.readU64LE(stream, off + 37), + author: stream.slice(off + 45, off + 77), + author_signing_pub: stream.slice(off + 77, off + 109), + recipient_count: stream[off + 109], + recipients: [], + data: stream.slice(off + 110 + 32 * MAX_RECIPIENTS, off + 110 + 32 * MAX_RECIPIENTS + 65536), + signature: stream.slice(off + 110 + 32 * MAX_RECIPIENTS + 65536, off + 110 + 32 * MAX_RECIPIENTS + 65536 + 64), + }, + }; + off += 110; + for (let i = 0; i < MAX_RECIPIENTS; i++) { + packet.contents.recipients.push(stream.slice(off, off + 32)); + off += 32; + } + return { packet, success: true }; } - 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 + private dataPacket2s(data: DataPacket): Uint8Array | null { + if (data.contents.recipient_count > MAX_RECIPIENTS) { + return null; + } + const stream = new Uint8Array(PACKET_SIZE); + let off = 0; + this.writeU32LE(stream, data.nonce, off); + off += 4; + stream.set(data.hash, off); + off += 32; + stream[off++] = data.contents.encrypted ? 1 : 0; + this.writeU64LE(stream, data.contents.created_time_ms, off); + off += 8; + stream.set(data.contents.author, off); + off += 32; + stream.set(data.contents.author_signing_pub, off); + off += 32; + stream[off++] = data.contents.recipient_count; + for (let i = 0; i < MAX_RECIPIENTS; i++) { + stream.set(data.contents.recipients[i] || new Uint8Array(32), off); + off += 32; + } + stream.set(data.contents.data, off); + off += 65536; + stream.set(data.contents.signature, off); + return stream; } - 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 + private async clientRequest(data: Uint8Array, dataLen: number): Promise<{ response: Uint8Array | null; respLen: number; error: MpClientError }> { + if (!this.checkConnected()) { + return { response: null, respLen: 0, error: MpClientError.CONNECTION_FAILED }; + } + const isIpv6 = this.ip!.includes(':'); + const url = isIpv6 ? `http://[${this.ip}]:${this.port}/` : `http://${this.ip}:${this.port}/`; + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Length': dataLen.toString(), + 'Connection': 'close', + }, + body: data.slice(0, dataLen), + }); + + if (!response.ok) { + if (response.status === 418) { + return { response: null, respLen: 0, error: MpClientError.SERVER_BUSY }; + } + return { response: null, respLen: 0, error: MpClientError.NETWORK_FAIL }; + } + + const arrayBuffer = await response.arrayBuffer(); + const responseData = new Uint8Array(arrayBuffer); + const contentLength = parseInt(response.headers.get('Content-Length') || '0', 10); + + if (contentLength > 0 && responseData.length !== contentLength) { + return { response: null, respLen: 0, error: MpClientError.RECV_FAILED }; + } + if (responseData.length > MAX_BODY) { + return { response: null, respLen: 0, error: MpClientError.INVALID_RESPONSE }; + } + + return { response: responseData, respLen: responseData.length, error: MpClientError.SUCCESS }; + } catch (err) { + return { response: null, respLen: 0, error: MpClientError.CONNECTION_FAILED }; + } } - } -} - -// 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 getServerInfo(): Promise<{ info: RelayInfo; error: MpClientError }> { + const data = new Uint8Array([0x10]); + const { response, respLen, error } = await this.clientRequest(data, data.length); + if (error !== MpClientError.SUCCESS) { + return { info: { major_version: 0, minor_version: 0, motd: new Uint8Array(128), capabilities: { retention_hours: 0, can_store: false, leading_zeros: 0, accepts_plain: false }, statistics: { neighbors: 0, held_packets: 0n, quick_connect: [] } }, error }; + } + if (respLen < 1 || response![0] !== 0x17) { + return { info: { major_version: 0, minor_version: 0, motd: new Uint8Array(128), capabilities: { retention_hours: 0, can_store: false, leading_zeros: 0, accepts_plain: false }, statistics: { neighbors: 0, held_packets: 0n, quick_connect: [] } }, error: MpClientError.INVALID_RESPONSE }; + } + const { info, success } = this.s2relayInfo(respLen - 1, response!.slice(1)); + return { info, error: success ? MpClientError.SUCCESS : MpClientError.DESERIALIZE_FAILED }; + } - async createPacketFromFile(filename: string, recipients?: Uint8Array[], authorKeys?: KeyPair): Promise { - const keys = authorKeys || this.generateKeyPair() - const recipientCount = recipients ? recipients.length : 0 + async getServerNeighbors(count: number): Promise<{ neighbors: RelayNeighbor[]; count: number; error: MpClientError }> { + const data = new Uint8Array([0x11, count]); + const { response, respLen, error } = await this.clientRequest(data, data.length); + if (error !== MpClientError.SUCCESS) { + return { neighbors: [], count: 0, error }; + } + if (respLen < 1 || response![0] !== 0x18) { + return { neighbors: [], count: 0, error: MpClientError.INVALID_RESPONSE }; + } + const numNeighbors = Math.floor((respLen - 1) / RELAY_NEIGHBOR_WIRE_LEN); + const neighbors: RelayNeighbor[] = []; + let actualCount = 0; + for (let i = 0; i < numNeighbors; i++) { + const { neighbor, success } = this.s2relayNeighbor(RELAY_NEIGHBOR_WIRE_LEN, response!.slice(1 + i * RELAY_NEIGHBOR_WIRE_LEN)); + if (success) { + neighbors.push(neighbor); + actualCount++; + } + } + return { neighbors, count: actualCount, error: MpClientError.SUCCESS }; + } - return this.ffi.createPacketFromFile(filename, recipientCount, recipients || null, keys) - } + async pushPacket(packet: DataPacket): Promise<{ success: boolean; error: MpClientError }> { + const sendData = this.dataPacket2s(packet); + if (!sendData) { + return { success: false, error: MpClientError.BAD_ARGUMENT }; + } + const sendLen = 1 + PACKET_SIZE; + const fullData = new Uint8Array(sendLen); + fullData[0] = 0x13; + fullData.set(sendData, 1); + const { response, respLen, error } = await this.clientRequest(fullData, sendLen); + if (error !== MpClientError.SUCCESS) { + return { success: false, error }; + } + if (respLen === 2 && response![0] === 0x1a) { + return { success: response![1] !== 0, error: MpClientError.SUCCESS }; + } + return { success: false, error: MpClientError.INVALID_RESPONSE }; + } - runProofOfWork(packet: DataPacket, leadingZeros: number): DataPacket { - return this.ffi.runProofOfWork(packet, leadingZeros) - } + async getPacket(packetHash: Uint8Array): Promise<{ packet: DataPacket; error: MpClientError }> { + const sendLen = 1 + 32; + const sendData = new Uint8Array(sendLen); + sendData[0] = 0x14; + sendData.set(packetHash, 1); + const { response, respLen, error } = await this.clientRequest(sendData, sendLen); + if (error !== MpClientError.SUCCESS) { + return { packet: { nonce: 0, hash: new Uint8Array(32), contents: { encrypted: false, created_time_ms: 0n, author: new Uint8Array(32), author_signing_pub: new Uint8Array(32), recipient_count: 0, recipients: [], data: new Uint8Array(65536), signature: new Uint8Array(64) } }, error }; + } + if (respLen < 1 || response![0] !== 0x1b) { + return { packet: { nonce: 0, hash: new Uint8Array(32), contents: { encrypted: false, created_time_ms: 0n, author: new Uint8Array(32), author_signing_pub: new Uint8Array(32), recipient_count: 0, recipients: [], data: new Uint8Array(65536), signature: new Uint8Array(64) } }, error: MpClientError.INVALID_RESPONSE }; + } + const { packet, success } = this.s2dataPacket(respLen - 1, response!.slice(1)); + return { packet, error: success ? MpClientError.SUCCESS : MpClientError.DESERIALIZE_FAILED }; + } - generateKeyPair(): KeyPair { - return this.ffi.generateKeyPair() - } + async getFirehosePackets(offset: bigint, count: number): Promise<{ list: DataPacketList; error: MpClientError }> { + const sendLen = 1 + 1 + 8; + const sendData = new Uint8Array(sendLen); + sendData[0] = 0x12; + sendData[1] = count; + this.writeU64LE(sendData, offset, 2); + const { response, respLen, error } = await this.clientRequest(sendData, sendLen); + if (error !== MpClientError.SUCCESS) { + return { list: { packets: [], count: 0, new_offset: offset }, error }; + } + if (respLen < 9 || response![0] !== 0x19) { + return { list: { packets: [], count: 0, new_offset: offset }, error: MpClientError.INVALID_RESPONSE }; + } + const newOffset = this.readU64LE(response!, 1); + const availableBytes = respLen - 9; + const availablePackets = Math.floor(availableBytes / PACKET_SIZE); + const packets: DataPacket[] = []; + let packetCount = 0; + for (let i = 0; i < availablePackets; i++) { + const { packet, success } = this.s2dataPacket(PACKET_SIZE, response!.slice(9 + i * PACKET_SIZE)); + if (success) { + packets.push(packet); + packetCount++; + } + } + return { list: { packets, count: packetCount, new_offset: newOffset }, error: MpClientError.SUCCESS }; + } - async queryAuthorPackets(ip: string, port: number, authorKeys: KeyPair, count: number = 10, offset: bigint = 0n): Promise { - return this.ffi.queryAuthorPackets(ip, port, authorKeys, count, offset) - } + async createPacketFromBytes( + input: Uint8Array, + inputLen: number, + recipientCount: number, + recipients: Uint8Array[], + authorKeys: KeyPair + ): Promise { + const packet: DataPacket = { + nonce: 0, + hash: new Uint8Array(32), + contents: { + encrypted: false, + created_time_ms: BigInt(Date.now()), + author: authorKeys.encryption_pub, + author_signing_pub: authorKeys.signing_pub, + recipient_count: recipientCount > MAX_RECIPIENTS ? MAX_RECIPIENTS : recipientCount, + recipients: new Array(MAX_RECIPIENTS).fill(new Uint8Array(32)), + data: new Uint8Array(65536), + signature: new Uint8Array(64), + }, + }; + const copyLen = inputLen > 65536 ? 65536 : inputLen; + if (input && copyLen > 0) { + packet.contents.data.set(input.slice(0, copyLen)); + } + if (recipients) { + for (let i = 0; i < packet.contents.recipient_count; i++) { + packet.contents.recipients[i] = recipients[i]; + } + } + const msgLen = 1 + 8 + 32 + 32 + 1 + 32 * MAX_RECIPIENTS + 65536; + const msgBuf = new Uint8Array(msgLen); + msgBuf[0] = packet.contents.encrypted ? 1 : 0; + this.writeU64LE(msgBuf, packet.contents.created_time_ms, 1); + msgBuf.set(packet.contents.author, 9); + msgBuf.set(packet.contents.author_signing_pub, 41); + msgBuf[73] = packet.contents.recipient_count; + for (let i = 0; i < MAX_RECIPIENTS; i++) { + msgBuf.set(packet.contents.recipients[i], 74 + 32 * i); + } + msgBuf.set(packet.contents.data, 74 + 32 * MAX_RECIPIENTS); + packet.contents.signature = await this.signData(msgBuf, authorKeys.signing_priv); + return packet; + } - // Utility methods - static hashToHex(hash: Uint8Array): string { - return Array.from(hash).map(b => b.toString(16).padStart(2, '0')).join('') - } + async createPacketFromString( + input: string, + recipientCount: number, + recipients: Uint8Array[], + authorKeys: KeyPair + ): Promise<{ packet: DataPacket; error: MpClientError }> { + try { + const encoder = new TextEncoder(); + const inputBytes = encoder.encode(input); + const packet = await this.createPacketFromBytes(inputBytes, inputBytes.length, recipientCount, recipients, authorKeys); + return { packet, error: MpClientError.SUCCESS }; + } catch (err) { + return { packet: { nonce: 0, hash: new Uint8Array(32), contents: { encrypted: false, created_time_ms: 0n, author: new Uint8Array(32), author_signing_pub: new Uint8Array(32), recipient_count: 0, recipients: [], data: new Uint8Array(65536), signature: new Uint8Array(64) } }, error: MpClientError.STRING_ENCODE_FAILED }; + } + } - 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) + async runPow(packet: DataPacket, leadingZeros: number): Promise { + const hashInputLen = 4 + PACKET_SIZE - 4; + const hashInputBuffer = new Uint8Array(hashInputLen); + const contentsBuffer = hashInputBuffer.subarray(4); + contentsBuffer[0] = packet.contents.encrypted ? 1 : 0; + this.writeU64LE(contentsBuffer, packet.contents.created_time_ms, 1); + contentsBuffer.set(packet.contents.author, 9); + contentsBuffer.set(packet.contents.author_signing_pub, 41); + contentsBuffer[73] = packet.contents.recipient_count; + for (let i = 0; i < MAX_RECIPIENTS; i++) { + contentsBuffer.set(packet.contents.recipients[i], 74 + 32 * i); + } + contentsBuffer.set(packet.contents.data, 74 + 32 * MAX_RECIPIENTS); + contentsBuffer.set(packet.contents.signature, 74 + 32 * MAX_RECIPIENTS + 65536); + let nonce = 0; + while (true) { + this.writeU32LE(hashInputBuffer, nonce, 0); + const hash = await this.sha256(hashInputBuffer); + if (this.checkLeadingZeros(hash, leadingZeros)) { + packet.nonce = nonce; + packet.hash = hash; + break; + } + nonce++; + } + return packet; } - 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(':') + + async queryAuthorPackets( + authorKeys: KeyPair, + count: number, + offset: bigint + ): Promise<{ list: DataPacketList; error: MpClientError }> { + const msg = new Uint8Array(32 + 32 + 2 + 8); + msg.set(authorKeys.encryption_pub, 0); + msg.set(authorKeys.signing_pub, 32); + this.writeU16LE(msg, count, 64); + this.writeU64LE(msg, offset, 66); + const sig = await this.signData(msg, authorKeys.signing_priv); + const sendLen = 1 + 32 + 32 + 64 + 2 + 8; + const sendData = new Uint8Array(sendLen); + let off = 0; + sendData[off++] = 0x16; + sendData.set(authorKeys.encryption_pub, off); + off += 32; + sendData.set(authorKeys.signing_pub, off); + off += 32; + sendData.set(sig, off); + off += 64; + this.writeU16LE(sendData, count, off); + off += 2; + this.writeU64LE(sendData, offset, off); + const { response, respLen, error } = await this.clientRequest(sendData, sendLen); + if (error !== MpClientError.SUCCESS) { + return { list: { packets: [], count: 0, new_offset: offset }, error }; + } + if (respLen < 11 || response![0] !== 0x1d) { + return { list: { packets: [], count: 0, new_offset: offset }, error: MpClientError.INVALID_RESPONSE }; + } + const respCount = this.readU16LE(response!, 1); + const newOffset = this.readU64LE(response!, 3); + const expectedLen = 11 + respCount * PACKET_SIZE; + if (respLen !== expectedLen) { + return { list: { packets: [], count: 0, new_offset: offset }, error: MpClientError.INVALID_RESPONSE }; + } + const packets: DataPacket[] = []; + let packetCount = 0; + for (let i = 0; i < respCount; i++) { + const { packet, success } = this.s2dataPacket(PACKET_SIZE, response!.slice(11 + i * PACKET_SIZE)); + if (success) { + packets.push(packet); + packetCount++; + } + } + return { list: { packets, count: packetCount, new_offset: newOffset }, error: MpClientError.SUCCESS }; } - return 'N/A' - } } -export default MoonpasteClient \ No newline at end of file +export { + MpClientError, + RelayNeighbor, + RelayInfo, + DataPacket, + DataPacketList, + KeyPair, + MoonPasteClient, +}; \ No newline at end of file -- cgit v1.2.3