summaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
authorGravatar moon <moon@coffin.ir> 2025-09-14 19:54:04 +0330
committerGravatar moon <moon@coffin.ir> 2025-09-14 19:54:04 +0330
commitb4a0211a0fbd4f9d4928f03db33762f14d780d22 (patch)
treebcb631597b2e51be8e266370bf1c90e45b7e36b0 /client
parentbetter (independent) client (diff)
better py client
Diffstat (limited to 'client')
-rw-r--r--client/3rd-party/client.py1106
1 files changed, 401 insertions, 705 deletions
diff --git a/client/3rd-party/client.py b/client/3rd-party/client.py
index 52675ad..bb78192 100644
--- a/client/3rd-party/client.py
+++ b/client/3rd-party/client.py
@@ -1,188 +1,48 @@
-# ai generated (i don't like python)
-
-import ctypes
-import os
-import platform
-from ctypes import (
- Structure, Union, POINTER, c_uint8, c_uint16, c_uint32, c_uint64,
- c_bool, c_int, c_char, c_void_p, c_size_t, CFUNCTYPE, byref, cast
-)
-from typing import List, Optional, Tuple, Union as PyUnion
+# ai-generated, do not trust (i don't like python)
+
+import asyncio
+import aiohttp
+import hashlib
+from ecdsa import SigningKey, NIST256p
+from typing import List, Tuple, Optional, Dict
from dataclasses import dataclass
-from enum import IntEnum
-import sys
-
-# Type aliases
-byte = c_uint8
-hash_t = byte * 32
-public_key = byte * 32
-private_key = byte * 32
-signature_t = byte * 64
-ipv4 = byte * 4
-ipv6 = byte * 16
-
-# Constants
+import struct
+
+MAX_BUFFER = 1 << 18
+MAX_BODY = 1 << 22
MAX_RECIPIENTS = 32
-MOTD_SIZE = 128
-DATA_SIZE = 65536
-SIGN_PUBLICKEYBYTES = 32
-SIGN_SECRETKEYBYTES = 64
+PACKET_SIZE = 4 + 32 + (1 + 8 + 32 + 32 + 1 + (32 * MAX_RECIPIENTS) + 65536 + 64)
+RELAY_NEIGHBOR_WIRE_LEN = 1 + 1 + 1 + 4 + 16 + 2
-class ClientError(IntEnum):
+class MpClientError:
SUCCESS = 0
NETWORK_FAIL = 1
- INVALID_ADDRESS = 2
- CONNECTION_FAILED = 3
- SEND_FAILED = 4
- RECV_FAILED = 5
- SERVER_BUSY = 6
- INVALID_RESPONSE = 7
- DESERIALIZE_FAILED = 8
- ALLOC_FAILED = 9
- FILE_NOT_FOUND = 10
- FILE_READ_FAILED = 11
- BAD_ARGUMENT = 12
-
-# C Structure definitions
-class RelayNeighbor(Structure):
- _fields_ = [
- ('active', c_bool),
- ('has_ipv4', c_bool),
- ('has_ipv6', c_bool),
- ('v4_addr', ipv4),
- ('v6_addr', ipv6)
- ]
-
-class RelayCapabilities(Structure):
- _fields_ = [
- ('retention_hours', c_uint16),
- ('can_store', c_bool),
- ('leading_zeros', byte),
- ('accepts_plain', c_bool)
- ]
-
-class RelayStatistics(Structure):
- _fields_ = [
- ('neighbors', c_uint16),
- ('held_packets', c_uint64),
- ('quick_connect', RelayNeighbor * 4)
- ]
-
-class RelayInfo(Structure):
- _fields_ = [
- ('major_version', byte),
- ('minor_version', byte),
- ('motd', c_char * MOTD_SIZE),
- ('capabilities', RelayCapabilities),
- ('statistics', RelayStatistics)
- ]
-
-class DataPacketContents(Structure):
- _fields_ = [
- ('encrypted', c_bool),
- ('created_time_ms', c_uint64),
- ('author', public_key),
- ('author_signing_pub', public_key),
- ('recipient_count', c_uint8),
- ('recipients', public_key * MAX_RECIPIENTS),
- ('data', byte * DATA_SIZE),
- ('signature', signature_t)
- ]
-
-class DataPacket(Structure):
- _fields_ = [
- ('nonce', c_uint32),
- ('hash', hash_t),
- ('contents', DataPacketContents)
- ]
-
-class KeyPair(Structure):
- _fields_ = [
- ('encryption_pub', public_key),
- ('encryption_priv', private_key),
- ('signing_pub', byte * SIGN_PUBLICKEYBYTES),
- ('signing_priv', byte * SIGN_SECRETKEYBYTES)
- ]
-
-class DataPacketList(Structure):
- _fields_ = [
- ('packets', POINTER(DataPacket)),
- ('count', c_uint16),
- ('new_offset', c_uint64)
- ]
-
-# Python dataclasses for easier usage
+ CONNECTION_FAILED = 2
+ SERVER_BUSY = 3
+ INVALID_RESPONSE = 4
+ DESERIALIZE_FAILED = 5
+ STRING_ENCODE_FAILED = 6
+ BAD_ARGUMENT = 7
+
@dataclass
-class PyRelayNeighbor:
+class RelayNeighbor:
active: bool
has_ipv4: bool
has_ipv6: bool
v4_addr: bytes
v6_addr: bytes
-
- @classmethod
- def from_c(cls, c_neighbor: RelayNeighbor) -> 'PyRelayNeighbor':
- return cls(
- active=c_neighbor.active,
- has_ipv4=c_neighbor.has_ipv4,
- has_ipv6=c_neighbor.has_ipv6,
- v4_addr=bytes(c_neighbor.v4_addr),
- v6_addr=bytes(c_neighbor.v6_addr)
- )
-
- def format_ip(self) -> str:
- if self.has_ipv4:
- return '.'.join(str(b) for b in self.v4_addr)
- elif self.has_ipv6:
- parts = []
- for i in range(0, 16, 2):
- word = (self.v6_addr[i] << 8) | self.v6_addr[i + 1]
- parts.append(f'{word:x}')
- return ':'.join(parts)
- return 'N/A'
-
-@dataclass
-class PyRelayCapabilities:
- retention_hours: int
- can_store: bool
- leading_zeros: int
- accepts_plain: bool
+ port: int
@dataclass
-class PyRelayStatistics:
- neighbors: int
- held_packets: int
- quick_connect: List[PyRelayNeighbor]
-
-@dataclass
-class PyRelayInfo:
+class RelayInfo:
major_version: int
minor_version: int
- motd: str
- capabilities: PyRelayCapabilities
- statistics: PyRelayStatistics
-
- @classmethod
- def from_c(cls, c_info: RelayInfo) -> 'PyRelayInfo':
- return cls(
- major_version=c_info.major_version,
- minor_version=c_info.minor_version,
- motd=c_info.motd.decode('utf-8', errors='ignore').rstrip('\x00'),
- capabilities=PyRelayCapabilities(
- retention_hours=c_info.capabilities.retention_hours,
- can_store=c_info.capabilities.can_store,
- leading_zeros=c_info.capabilities.leading_zeros,
- accepts_plain=c_info.capabilities.accepts_plain
- ),
- statistics=PyRelayStatistics(
- neighbors=c_info.statistics.neighbors,
- held_packets=c_info.statistics.held_packets,
- quick_connect=[PyRelayNeighbor.from_c(n) for n in c_info.statistics.quick_connect]
- )
- )
+ motd: bytes
+ capabilities: Dict[str, any]
+ statistics: Dict[str, any]
@dataclass
-class PyDataPacketContents:
+class DataPacketContents:
encrypted: bool
created_time_ms: int
author: bytes
@@ -193,558 +53,394 @@ class PyDataPacketContents:
signature: bytes
@dataclass
-class PyDataPacket:
+class DataPacket:
nonce: int
hash: bytes
- contents: PyDataPacketContents
-
- @classmethod
- def from_c(cls, c_packet: DataPacket) -> 'PyDataPacket':
- recipients = []
- for i in range(c_packet.contents.recipient_count):
- recipients.append(bytes(c_packet.contents.recipients[i]))
-
- return cls(
- nonce=c_packet.nonce,
- hash=bytes(c_packet.hash),
- contents=PyDataPacketContents(
- encrypted=c_packet.contents.encrypted,
- created_time_ms=c_packet.contents.created_time_ms,
- author=bytes(c_packet.contents.author),
- author_signing_pub=bytes(c_packet.contents.author_signing_pub),
- recipient_count=c_packet.contents.recipient_count,
- recipients=recipients,
- data=bytes(c_packet.contents.data),
- signature=bytes(c_packet.contents.signature)
- )
- )
-
- def to_c(self) -> DataPacket:
- c_packet = DataPacket()
- c_packet.nonce = self.nonce
-
- for i, b in enumerate(self.hash[:32]):
- c_packet.hash[i] = b
-
- c_packet.contents.encrypted = self.contents.encrypted
- c_packet.contents.created_time_ms = self.contents.created_time_ms
- c_packet.contents.recipient_count = self.contents.recipient_count
-
- for i, b in enumerate(self.contents.author[:32]):
- c_packet.contents.author[i] = b
-
- for i, b in enumerate(self.contents.author_signing_pub[:32]):
- c_packet.contents.author_signing_pub[i] = b
-
- for i, recipient in enumerate(self.contents.recipients[:MAX_RECIPIENTS]):
- for j, b in enumerate(recipient[:32]):
- c_packet.contents.recipients[i][j] = b
-
- for i, b in enumerate(self.contents.data[:DATA_SIZE]):
- c_packet.contents.data[i] = b
-
- for i, b in enumerate(self.contents.signature[:64]):
- c_packet.contents.signature[i] = b
-
- return c_packet
+ contents: DataPacketContents
@dataclass
-class PyKeyPair:
+class DataPacketList:
+ packets: List[DataPacket]
+ count: int
+ new_offset: int
+
+@dataclass
+class KeyPair:
encryption_pub: bytes
encryption_priv: bytes
signing_pub: bytes
signing_priv: bytes
-
- @classmethod
- def from_c(cls, c_keypair: KeyPair) -> 'PyKeyPair':
- return cls(
- encryption_pub=bytes(c_keypair.encryption_pub),
- encryption_priv=bytes(c_keypair.encryption_priv),
- signing_pub=bytes(c_keypair.signing_pub),
- signing_priv=bytes(c_keypair.signing_priv)
+
+class MoonPasteClient:
+ def __init__(self):
+ self.ip: Optional[str] = None
+ self.port: Optional[int] = None
+
+ def connect(self, ip: str, port: int) -> None:
+ self.ip = ip
+ self.port = port
+
+ def _check_connected(self) -> bool:
+ return self.ip is not None and self.port is not None
+
+ def _read_u16_le(self, buffer: bytes, offset: int) -> int:
+ return struct.unpack_from('<H', buffer, offset)[0]
+
+ def _write_u16_le(self, buffer: bytearray, value: int, offset: int) -> None:
+ struct.pack_into('<H', buffer, offset, value)
+
+ def _write_u32_le(self, buffer: bytearray, value: int, offset: int) -> None:
+ struct.pack_into('<I', buffer, offset, value)
+
+ def _read_u32_le(self, buffer: bytes, offset: int) -> int:
+ return struct.unpack_from('<I', buffer, offset)[0]
+
+ def _read_u64_le(self, buffer: bytes, offset: int) -> int:
+ return struct.unpack_from('<Q', buffer, offset)[0]
+
+ def _write_u64_le(self, buffer: bytearray, value: int, offset: int) -> None:
+ struct.pack_into('<Q', buffer, offset, value)
+
+ def _check_leading_zeros(self, hash_bytes: bytes, required_zeros: int) -> bool:
+ if required_zeros == 0:
+ return True
+ full_bytes = required_zeros // 8
+ remaining_bits = required_zeros % 8
+ for i in range(full_bytes):
+ if hash_bytes[i] != 0:
+ return False
+ if remaining_bits > 0:
+ mask = 0xFF << (8 - remaining_bits)
+ if hash_bytes[full_bytes] & mask != 0:
+ return False
+ return True
+
+ async def _sha256(self, data: bytes) -> bytes:
+ return hashlib.sha256(data).digest()
+
+ async def generate_key_pair(self) -> KeyPair:
+ enc_key = SigningKey.generate(curve=NIST256p)
+ sign_key = SigningKey.generate(curve=NIST256p)
+ return KeyPair(
+ encryption_pub=enc_key.verifying_key.to_string()[:32],
+ encryption_priv=enc_key.to_string()[:32],
+ signing_pub=sign_key.verifying_key.to_string()[:32],
+ signing_priv=sign_key.to_string()[:32]
)
-
- def to_c(self) -> KeyPair:
- c_keypair = KeyPair()
-
- for i, b in enumerate(self.encryption_pub[:32]):
- c_keypair.encryption_pub[i] = b
- for i, b in enumerate(self.encryption_priv[:32]):
- c_keypair.encryption_priv[i] = b
- for i, b in enumerate(self.signing_pub[:32]):
- c_keypair.signing_pub[i] = b
- for i, b in enumerate(self.signing_priv[:64]):
- c_keypair.signing_priv[i] = b
-
- return c_keypair
-@dataclass
-class PyDataPacketList:
- packets: List[PyDataPacket]
- count: int
- new_offset: int
+ async def _sign_data(self, data: bytes, private_key: bytes) -> bytes:
+ sk = SigningKey.from_string(private_key, curve=NIST256p)
+ return sk.sign(data, hashfunc=hashlib.sha256)[:64]
-class MoonpasteClientError(Exception):
- '''Exception raised for moonpaste client errors'''
- def __init__(self, error_code: ClientError, message: str = ''):
- self.error_code = error_code
- self.message = message or f'Client error: {error_code.name}'
- super().__init__(self.message)
-
-def find_library() -> str:
- '''Find the moonpaste client library'''
- system = platform.system().lower()
-
- if system == 'linux':
- lib_names = ['libmpclient.so']
- elif system == 'darwin':
- lib_names = ['libmpclient.dylib']
- elif system == 'windows':
- lib_names = ['libmpclient.dll']
- else:
- lib_names = ['libmpclient.so', 'libmpclient.dylib', 'libmpclient.dll']
-
- search_paths = [
- './',
- './lib/',
- '../lib/',
- '/usr/local/lib/',
- '/usr/lib/',
- os.environ.get('MOONPASTE_LIB_PATH', '')
- ]
-
- search_paths = [p for p in search_paths if p]
-
- for search_path in search_paths:
- for lib_name in lib_names:
- full_path = os.path.join(search_path, lib_name)
- if os.path.exists(full_path):
- return full_path
-
- raise FileNotFoundError(f'Could not find moonpaste client library. Searched for: {lib_names}')
-
-class MoonpasteFFI:
- '''Low-level FFI wrapper for the moonpaste client library'''
-
- def __init__(self):
- lib_path = find_library()
- self.lib = ctypes.CDLL(lib_path)
- self._setup_function_signatures()
-
- def _setup_function_signatures(self):
- '''Setup C function signatures'''
-
- # mp_get_server_info
- self.lib.mp_get_server_info.argtypes = [
- ctypes.c_char_p, # ip
- c_int, # port
- POINTER(RelayInfo) # out_info
- ]
- self.lib.mp_get_server_info.restype = c_int
-
- # mp_get_server_neighbors
- self.lib.mp_get_server_neighbors.argtypes = [
- ctypes.c_char_p, # ip
- c_int, # port
- byte, # count
- POINTER(POINTER(RelayNeighbor)), # out_neighbors
- POINTER(byte) # out_count
- ]
- self.lib.mp_get_server_neighbors.restype = c_int
-
- # mp_push_packet
- self.lib.mp_push_packet.argtypes = [
- ctypes.c_char_p, # ip
- c_int, # port
- DataPacket, # packet
- POINTER(c_bool) # out_success
- ]
- self.lib.mp_push_packet.restype = c_int
-
- # mp_get_packet
- self.lib.mp_get_packet.argtypes = [
- ctypes.c_char_p, # ip
- c_int, # port
- hash_t, # packet_hash
- POINTER(DataPacket) # out_packet
- ]
- self.lib.mp_get_packet.restype = c_int
-
- # mp_get_firehose_packets
- self.lib.mp_get_firehose_packets.argtypes = [
- ctypes.c_char_p, # ip
- c_int, # port
- c_uint64, # offset
- c_uint16, # count
- POINTER(DataPacketList) # out_list
- ]
- self.lib.mp_get_firehose_packets.restype = c_int
-
- # mp_create_packet_from_bytes
- self.lib.mp_create_packet_from_bytes.argtypes = [
- POINTER(byte), # input
- c_size_t, # input_len
- byte, # recipient_count
- POINTER(hash_t), # recipients
- KeyPair # author_keys
- ]
- self.lib.mp_create_packet_from_bytes.restype = DataPacket
-
- # mp_create_packet_from_file
- self.lib.mp_create_packet_from_file.argtypes = [
- ctypes.c_char_p, # filename
- byte, # recipient_count
- POINTER(hash_t), # recipients
- KeyPair, # author_keys
- POINTER(DataPacket) # out_packet
- ]
- self.lib.mp_create_packet_from_file.restype = c_int
-
- # mp_run_pow
- self.lib.mp_run_pow.argtypes = [
- DataPacket, # packet
- byte # leading_zeros
- ]
- self.lib.mp_run_pow.restype = DataPacket
-
- # mp_generate_keypair
- self.lib.mp_generate_keypair.argtypes = []
- self.lib.mp_generate_keypair.restype = KeyPair
-
- # mp_query_author_packets
- self.lib.mp_query_author_packets.argtypes = [
- ctypes.c_char_p, # ip
- c_int, # port
- KeyPair, # author_keys
- c_uint16, # count
- c_uint64, # offset
- POINTER(DataPacketList) # out_list
- ]
- self.lib.mp_query_author_packets.restype = c_int
-
-class MoonpasteClient:
- '''High-level Python client for moonpaste'''
-
- def __init__(self):
- self.ffi = MoonpasteFFI()
-
- def get_server_info(self, ip: str, port: int) -> PyRelayInfo:
- '''Get server information and capabilities'''
- info = RelayInfo()
-
- result = self.ffi.lib.mp_get_server_info(
- ip.encode('utf-8'),
- port,
- byref(info)
+ def _s2relay_neighbor(self, stream: bytes) -> Tuple[RelayNeighbor, bool]:
+ if len(stream) != RELAY_NEIGHBOR_WIRE_LEN:
+ return RelayNeighbor(False, False, False, b'\x00' * 4, b'\x00' * 16, 0), False
+ return RelayNeighbor(
+ active=stream[0] != 0,
+ has_ipv4=stream[1] != 0,
+ has_ipv6=stream[2] != 0,
+ v4_addr=stream[3:7],
+ v6_addr=stream[7:23],
+ port=self._read_u16_le(stream, 23)
+ ), True
+
+ def _s2relay_info(self, stream: bytes) -> Tuple[RelayInfo, bool]:
+ expected_len = 1 + 1 + 128 + 2 + 1 + 1 + 1 + 2 + 8 + (4 * RELAY_NEIGHBOR_WIRE_LEN)
+ if len(stream) != expected_len:
+ return RelayInfo(0, 0, b'\x00' * 128, {'retention_hours': 0, 'can_store': False, 'leading_zeros': 0, 'accepts_plain': False}, {'neighbors': 0, 'held_packets': 0, 'quick_connect': []}), False
+ off = 0
+ info = RelayInfo(
+ major_version=stream[off],
+ minor_version=stream[off + 1],
+ motd=stream[off + 2:off + 130],
+ capabilities={
+ 'retention_hours': self._read_u16_le(stream, off + 130),
+ 'can_store': stream[off + 132] != 0,
+ 'leading_zeros': stream[off + 133],
+ 'accepts_plain': stream[off + 134] != 0
+ },
+ statistics={
+ 'neighbors': self._read_u16_le(stream, off + 135),
+ 'held_packets': self._read_u64_le(stream, off + 137),
+ 'quick_connect': []
+ }
)
-
- if result != ClientError.SUCCESS:
- raise MoonpasteClientError(ClientError(result))
-
- return PyRelayInfo.from_c(info)
-
- def get_neighbors(self, ip: str, port: int, count: int = 5) -> List[PyRelayNeighbor]:
- '''Get a list of neighbors from the server'''
- neighbors_ptr = POINTER(RelayNeighbor)()
- out_count = byte()
-
- result = self.ffi.lib.mp_get_server_neighbors(
- ip.encode('utf-8'),
- port,
- count,
- byref(neighbors_ptr),
- byref(out_count)
+ off += 145
+ for _ in range(4):
+ neighbor, success = self._s2relay_neighbor(stream[off:off + RELAY_NEIGHBOR_WIRE_LEN])
+ if not success:
+ return RelayInfo(0, 0, b'\x00' * 128, {'retention_hours': 0, 'can_store': False, 'leading_zeros': 0, 'accepts_plain': False}, {'neighbors': 0, 'held_packets': 0, 'quick_connect': []}), False
+ info.statistics['quick_connect'].append(neighbor)
+ off += RELAY_NEIGHBOR_WIRE_LEN
+ return info, True
+
+ def _s2data_packet(self, stream: bytes) -> Tuple[DataPacket, bool]:
+ if len(stream) != PACKET_SIZE:
+ return DataPacket(0, b'\x00' * 32, DataPacketContents(False, 0, b'\x00' * 32, b'\x00' * 32, 0, [], b'\x00' * 65536, b'\x00' * 64)), False
+ off = 0
+ packet = DataPacket(
+ nonce=self._read_u32_le(stream, off),
+ hash=stream[off + 4:off + 36],
+ contents=DataPacketContents(
+ encrypted=stream[off + 36] != 0,
+ created_time_ms=self._read_u64_le(stream, off + 37),
+ author=stream[off + 45:off + 77],
+ author_signing_pub=stream[off + 77:off + 109],
+ recipient_count=stream[off + 109],
+ recipients=[],
+ data=stream[off + 110 + 32 * MAX_RECIPIENTS:off + 110 + 32 * MAX_RECIPIENTS + 65536],
+ signature=stream[off + 110 + 32 * MAX_RECIPIENTS + 65536:off + 110 + 32 * MAX_RECIPIENTS + 65536 + 64]
+ )
)
-
- if result != ClientError.SUCCESS:
- raise MoonpasteClientError(ClientError(result))
-
+ off += 110
+ for _ in range(MAX_RECIPIENTS):
+ packet.contents.recipients.append(stream[off:off + 32])
+ off += 32
+ return packet, True
+
+ def _data_packet2s(self, data: DataPacket) -> Optional[bytes]:
+ if data.contents.recipient_count > MAX_RECIPIENTS:
+ return None
+ stream = bytearray(PACKET_SIZE)
+ off = 0
+ self._write_u32_le(stream, data.nonce, off)
+ off += 4
+ stream[off:off + 32] = data.hash
+ off += 32
+ stream[off] = 1 if data.contents.encrypted else 0
+ off += 1
+ self._write_u64_le(stream, data.contents.created_time_ms, off)
+ off += 8
+ stream[off:off + 32] = data.contents.author
+ off += 32
+ stream[off:off + 32] = data.contents.author_signing_pub
+ off += 32
+ stream[off] = data.contents.recipient_count
+ off += 1
+ for i in range(MAX_RECIPIENTS):
+ stream[off:off + 32] = data.contents.recipients[i] if i < len(data.contents.recipients) else b'\x00' * 32
+ off += 32
+ stream[off:off + 65536] = data.contents.data
+ off += 65536
+ stream[off:off + 64] = data.contents.signature
+ return bytes(stream)
+
+ async def _client_request(self, data: bytes, data_len: int) -> Tuple[Optional[bytes], int, int]:
+ if not self._check_connected():
+ return None, 0, MpClientError.CONNECTION_FAILED
+ is_ipv6 = ':' in self.ip
+ url = f"http://[{self.ip}]:{self.port}/" if is_ipv6 else f"http://{self.ip}:{self.port}/"
+ async with aiohttp.ClientSession() as session:
+ try:
+ async with session.post(url, data=data[:data_len], headers={'Content-Length': str(data_len), 'Connection': 'close'}) as response:
+ if response.status == 418:
+ return None, 0, MpClientError.SERVER_BUSY
+ if response.status != 200:
+ return None, 0, MpClientError.NETWORK_FAIL
+ response_data = await response.read()
+ content_length = int(response.headers.get('Content-Length', '0'))
+ if content_length > 0 and len(response_data) != content_length:
+ return None, 0, MpClientError.RECV_FAILED
+ if len(response_data) > MAX_BODY:
+ return None, 0, MpClientError.INVALID_RESPONSE
+ return response_data, len(response_data), MpClientError.SUCCESS
+ except aiohttp.ClientError:
+ return None, 0, MpClientError.CONNECTION_FAILED
+
+ async def get_server_info(self) -> Tuple[RelayInfo, int]:
+ data = b'\x10'
+ response, resp_len, error = await self._client_request(data, len(data))
+ if error != MpClientError.SUCCESS:
+ return RelayInfo(0, 0, b'\x00' * 128, {'retention_hours': 0, 'can_store': False, 'leading_zeros': 0, 'accepts_plain': False}, {'neighbors': 0, 'held_packets': 0, 'quick_connect': []}), error
+ if resp_len < 1 or response[0] != 0x17:
+ return RelayInfo(0, 0, b'\x00' * 128, {'retention_hours': 0, 'can_store': False, 'leading_zeros': 0, 'accepts_plain': False}, {'neighbors': 0, 'held_packets': 0, 'quick_connect': []}), MpClientError.INVALID_RESPONSE
+ info, success = self._s2relay_info(response[1:resp_len])
+ return info, MpClientError.SUCCESS if success else MpClientError.DESERIALIZE_FAILED
+
+ async def get_server_neighbors(self, count: int) -> Tuple[List[RelayNeighbor], int, int]:
+ data = bytearray(2)
+ data[0] = 0x11
+ data[1] = count
+ response, resp_len, error = await self._client_request(data, len(data))
+ if error != MpClientError.SUCCESS:
+ return [], 0, error
+ if resp_len < 1 or response[0] != 0x18:
+ return [], 0, MpClientError.INVALID_RESPONSE
+ num_neighbors = (resp_len - 1) // RELAY_NEIGHBOR_WIRE_LEN
neighbors = []
- if out_count.value > 0 and neighbors_ptr:
- for i in range(out_count.value):
- neighbors.append(PyRelayNeighbor.from_c(neighbors_ptr[i]))
-
- # Free the allocated memory
- ctypes.c_void_p.in_dll(self.ffi.lib, 'free').argtypes = [c_void_p]
- ctypes.c_void_p.in_dll(self.ffi.lib, 'free')(cast(neighbors_ptr, c_void_p))
-
- return neighbors
-
- def push_packet(self, ip: str, port: int, packet: PyDataPacket) -> bool:
- '''Push a packet to the server'''
- c_packet = packet.to_c()
- success = c_bool()
-
- result = self.ffi.lib.mp_push_packet(
- ip.encode('utf-8'),
- port,
- c_packet,
- byref(success)
- )
-
- if result != ClientError.SUCCESS:
- raise MoonpasteClientError(ClientError(result))
-
- return success.value
-
- def get_packet(self, ip: str, port: int, packet_hash: bytes) -> PyDataPacket:
- '''Get a specific packet by its hash'''
- if len(packet_hash) != 32:
- raise ValueError('Hash must be exactly 32 bytes')
-
- c_hash = hash_t()
- for i, b in enumerate(packet_hash):
- c_hash[i] = b
-
- packet = DataPacket()
-
- result = self.ffi.lib.mp_get_packet(
- ip.encode('utf-8'),
- port,
- c_hash,
- byref(packet)
- )
-
- if result != ClientError.SUCCESS:
- raise MoonpasteClientError(ClientError(result))
-
- return PyDataPacket.from_c(packet)
-
- def get_firehose_packets(self, ip: str, port: int, offset: int = 0, count: int = 10) -> PyDataPacketList:
- '''Get a batch of packets from the server's firehose stream'''
- packet_list = DataPacketList()
-
- result = self.ffi.lib.mp_get_firehose_packets(
- ip.encode('utf-8'),
- port,
- offset,
- count,
- byref(packet_list)
- )
-
- if result != ClientError.SUCCESS:
- raise MoonpasteClientError(ClientError(result))
-
+ actual_count = 0
+ for i in range(num_neighbors):
+ neighbor, success = self._s2relay_neighbor(response[1 + i * RELAY_NEIGHBOR_WIRE_LEN:1 + (i + 1) * RELAY_NEIGHBOR_WIRE_LEN])
+ if success:
+ neighbors.append(neighbor)
+ actual_count += 1
+ return neighbors, actual_count, MpClientError.SUCCESS
+
+ async def push_packet(self, packet: DataPacket) -> Tuple[bool, int]:
+ send_data = self._data_packet2s(packet)
+ if send_data is None:
+ return False, MpClientError.BAD_ARGUMENT
+ full_data = bytearray(1 + PACKET_SIZE)
+ full_data[0] = 0x13
+ full_data[1:] = send_data
+ response, resp_len, error = await self._client_request(full_data, len(full_data))
+ if error != MpClientError.SUCCESS:
+ return False, error
+ if resp_len == 2 and response[0] == 0x1a:
+ return response[1] != 0, MpClientError.SUCCESS
+ return False, MpClientError.INVALID_RESPONSE
+
+ async def get_packet(self, packet_hash: bytes) -> Tuple[DataPacket, int]:
+ send_data = bytearray(1 + 32)
+ send_data[0] = 0x14
+ send_data[1:33] = packet_hash
+ response, resp_len, error = await self._client_request(send_data, len(send_data))
+ if error != MpClientError.SUCCESS:
+ return DataPacket(0, b'\x00' * 32, DataPacketContents(False, 0, b'\x00' * 32, b'\x00' * 32, 0, [], b'\x00' * 65536, b'\x00' * 64)), error
+ if resp_len < 1 or response[0] != 0x1b:
+ return DataPacket(0, b'\x00' * 32, DataPacketContents(False, 0, b'\x00' * 32, b'\x00' * 32, 0, [], b'\x00' * 65536, b'\x00' * 64)), MpClientError.INVALID_RESPONSE
+ packet, success = self._s2data_packet(response[1:resp_len])
+ return packet, MpClientError.SUCCESS if success else MpClientError.DESERIALIZE_FAILED
+
+ async def get_firehose_packets(self, offset: int, count: int) -> Tuple[DataPacketList, int]:
+ send_data = bytearray(1 + 1 + 8)
+ send_data[0] = 0x12
+ send_data[1] = count
+ self._write_u64_le(send_data, offset, 2)
+ response, resp_len, error = await self._client_request(send_data, len(send_data))
+ if error != MpClientError.SUCCESS:
+ return DataPacketList([], 0, offset), error
+ if resp_len < 9 or response[0] != 0x19:
+ return DataPacketList([], 0, offset), MpClientError.INVALID_RESPONSE
+ new_offset = self._read_u64_le(response, 1)
+ available_bytes = resp_len - 9
+ available_packets = available_bytes // PACKET_SIZE
packets = []
- if packet_list.count > 0 and packet_list.packets:
- for i in range(packet_list.count):
- packets.append(PyDataPacket.from_c(packet_list.packets[i]))
-
- # Free the allocated memory safely
- try:
- import ctypes.util
- libc_name = ctypes.util.find_library('c')
- if libc_name:
- libc = ctypes.CDLL(libc_name)
- libc.free.argtypes = [c_void_p]
- libc.free(cast(packet_list.packets, c_void_p))
- else:
- # Platform-specific fallback
- try:
- if sys.platform.startswith('linux'):
- libc = ctypes.CDLL('libc.so.6')
- elif sys.platform == 'darwin':
- libc = ctypes.CDLL('libc.dylib')
- elif sys.platform == 'win32':
- libc = ctypes.CDLL('msvcrt.dll')
- else:
- libc = None
-
- if libc:
- libc.free.argtypes = [c_void_p]
- libc.free(cast(packet_list.packets, c_void_p))
- except OSError:
- pass # Can't free, but better than crash
- except:
- pass # Memory will be freed when process exits
-
- return PyDataPacketList(
- packets=packets,
- count=packet_list.count,
- new_offset=packet_list.new_offset
- )
-
- def create_packet(self,
- message: PyUnion[str, bytes],
- recipients: Optional[List[bytes]] = None,
- author_keys: Optional[PyKeyPair] = None) -> PyDataPacket:
- '''Create a data packet from message content'''
-
- if isinstance(message, str):
- data = message.encode('utf-8')
- else:
- data = message
-
- if author_keys is None:
- author_keys = self.generate_keypair()
-
- c_keys = author_keys.to_c()
-
- # Convert data to ctypes array
- data_len = min(len(data), DATA_SIZE)
- c_data = (byte * data_len)(*data[:data_len])
-
- # Handle recipients
- recipient_count = 0
- c_recipients = None
-
- if recipients:
- recipient_count = min(len(recipients), MAX_RECIPIENTS)
- c_recipients = (hash_t * recipient_count)()
- for i, recipient in enumerate(recipients[:recipient_count]):
- if len(recipient) != 32:
- raise ValueError(f'Recipient {i} must be exactly 32 bytes')
- for j, b in enumerate(recipient):
- c_recipients[i][j] = b
-
- packet = self.ffi.lib.mp_create_packet_from_bytes(
- cast(c_data, POINTER(byte)),
- data_len,
- recipient_count,
- cast(c_recipients, POINTER(hash_t)) if c_recipients else None,
- c_keys
+ packet_count = 0
+ for i in range(available_packets):
+ packet, success = self._s2data_packet(response[9 + i * PACKET_SIZE:9 + (i + 1) * PACKET_SIZE])
+ if success:
+ packets.append(packet)
+ packet_count += 1
+ return DataPacketList(packets, packet_count, new_offset), MpClientError.SUCCESS
+
+ async def create_packet_from_bytes(
+ self,
+ input_bytes: bytes,
+ input_len: int,
+ recipient_count: int,
+ recipients: List[bytes],
+ author_keys: KeyPair
+ ) -> DataPacket:
+ packet = DataPacket(
+ nonce=0,
+ hash=b'\x00' * 32,
+ contents=DataPacketContents(
+ encrypted=False,
+ created_time_ms=int.from_bytes(struct.pack('<Q', int.from_bytes(b'\x00' * 8, 'little') + int(1000 * (await asyncio.get_event_loop().time()))), 'little'),
+ author=author_keys.encryption_pub,
+ author_signing_pub=author_keys.signing_pub,
+ recipient_count=min(recipient_count, MAX_RECIPIENTS),
+ recipients=[b'\x00' * 32 for _ in range(MAX_RECIPIENTS)],
+ data=b'\x00' * 65536,
+ signature=b'\x00' * 64
+ )
)
-
- return PyDataPacket.from_c(packet)
-
- def create_packet_from_file(self,
- filename: str,
- recipients: Optional[List[bytes]] = None,
- author_keys: Optional[PyKeyPair] = None) -> PyDataPacket:
- '''Create a data packet from file contents'''
-
- if author_keys is None:
- author_keys = self.generate_keypair()
-
- c_keys = author_keys.to_c()
-
- # Handle recipients
- recipient_count = 0
- c_recipients = None
-
+ copy_len = min(input_len, 65536)
+ if input_bytes and copy_len > 0:
+ packet.contents.data = input_bytes[:copy_len] + b'\x00' * (65536 - copy_len)
if recipients:
- recipient_count = min(len(recipients), MAX_RECIPIENTS)
- c_recipients = (hash_t * recipient_count)()
- for i, recipient in enumerate(recipients[:recipient_count]):
- if len(recipient) != 32:
- raise ValueError(f'Recipient {i} must be exactly 32 bytes')
- for j, b in enumerate(recipient):
- c_recipients[i][j] = b
-
- packet = DataPacket()
-
- result = self.ffi.lib.mp_create_packet_from_file(
- filename.encode('utf-8'),
- recipient_count,
- cast(c_recipients, POINTER(hash_t)) if c_recipients else None,
- c_keys,
- byref(packet)
- )
-
- if result != ClientError.SUCCESS:
- raise MoonpasteClientError(ClientError(result))
-
- return PyDataPacket.from_c(packet)
-
- def run_proof_of_work(self, packet: PyDataPacket, leading_zeros: int) -> PyDataPacket:
- '''Run Proof-of-Work on a packet'''
- c_packet = packet.to_c()
-
- result_packet = self.ffi.lib.mp_run_pow(c_packet, leading_zeros)
-
- return PyDataPacket.from_c(result_packet)
-
- def generate_keypair(self) -> PyKeyPair:
- '''Generate a new encryption and signing keypair'''
- c_keypair = self.ffi.lib.mp_generate_keypair()
- return PyKeyPair.from_c(c_keypair)
-
- # Utility methods
- @staticmethod
- def hash_to_hex(hash_bytes: bytes) -> str:
- '''Convert a hash to hexadecimal string'''
- return hash_bytes.hex()
-
- @staticmethod
- def hex_to_hash(hex_string: str) -> bytes:
- '''Convert hexadecimal string to hash bytes'''
- if len(hex_string) != 64:
- raise ValueError('Hash hex string must be exactly 64 characters')
- return bytes.fromhex(hex_string)
-
- def create_and_push_packet(self,
- ip: str,
- port: int,
- message: PyUnion[str, bytes],
- leading_zeros: int = 0,
- recipients: Optional[List[bytes]] = None,
- author_keys: Optional[PyKeyPair] = None) -> Tuple[PyDataPacket, bool]:
- '''Create a packet, run PoW, and push it to the server'''
-
- # Create the packet
- packet = self.create_packet(message, recipients, author_keys)
-
- # Run proof of work if required
- if leading_zeros > 0:
- packet = self.run_proof_of_work(packet, leading_zeros)
-
- # Push to server
- success = self.push_packet(ip, port, packet)
-
- return packet, success
-
- def query_author_packets(self, ip: str, port: int, author_keys: PyKeyPair, count: int = 10, offset: int = 0) -> PyDataPacketList:
- '''Query packets for a specific author'''
- c_keys = author_keys.to_c()
- packet_list = DataPacketList()
-
- result = self.ffi.lib.mp_query_author_packets(
- ip.encode('utf-8'),
- port,
- c_keys,
- count,
- offset,
- byref(packet_list)
- )
-
- if result != ClientError.SUCCESS:
- raise MoonpasteClientError(ClientError(result))
-
+ for i in range(packet.contents.recipient_count):
+ packet.contents.recipients[i] = recipients[i]
+ msg = bytearray(1 + 8 + 32 + 32 + 1 + 32 * MAX_RECIPIENTS + 65536)
+ msg[0] = 1 if packet.contents.encrypted else 0
+ self._write_u64_le(msg, packet.contents.created_time_ms, 1)
+ msg[9:41] = packet.contents.author
+ msg[41:73] = packet.contents.author_signing_pub
+ msg[73] = packet.contents.recipient_count
+ for i in range(MAX_RECIPIENTS):
+ msg[74 + 32 * i:74 + 32 * (i + 1)] = packet.contents.recipients[i]
+ msg[74 + 32 * MAX_RECIPIENTS:74 + 32 * MAX_RECIPIENTS + 65536] = packet.contents.data
+ packet.contents.signature = await self._sign_data(msg, author_keys.signing_priv)
+ return packet
+
+ async def create_packet_from_string(
+ self,
+ input_str: str,
+ recipient_count: int,
+ recipients: List[bytes],
+ author_keys: KeyPair
+ ) -> Tuple[DataPacket, int]:
+ try:
+ input_bytes = input_str.encode('utf-8')
+ packet = await self.create_packet_from_bytes(input_bytes, len(input_bytes), recipient_count, recipients, author_keys)
+ return packet, MpClientError.SUCCESS
+ except UnicodeEncodeError:
+ return DataPacket(0, b'\x00' * 32, DataPacketContents(False, 0, b'\x00' * 32, b'\x00' * 32, 0, [], b'\x00' * 65536, b'\x00' * 64)), MpClientError.STRING_ENCODE_FAILED
+
+ async def run_pow(self, packet: DataPacket, leading_zeros: int) -> DataPacket:
+ hash_input = bytearray(4 + PACKET_SIZE - 4)
+ contents_buffer = hash_input[4:]
+ contents_buffer[0] = 1 if packet.contents.encrypted else 0
+ self._write_u64_le(contents_buffer, packet.contents.created_time_ms, 1)
+ contents_buffer[9:41] = packet.contents.author
+ contents_buffer[41:73] = packet.contents.author_signing_pub
+ contents_buffer[73] = packet.contents.recipient_count
+ for i in range(MAX_RECIPIENTS):
+ contents_buffer[74 + 32 * i:74 + 32 * (i + 1)] = packet.contents.recipients[i]
+ contents_buffer[74 + 32 * MAX_RECIPIENTS:74 + 32 * MAX_RECIPIENTS + 65536] = packet.contents.data
+ contents_buffer[74 + 32 * MAX_RECIPIENTS + 65536:74 + 32 * MAX_RECIPIENTS + 65536 + 64] = packet.contents.signature
+ nonce = 0
+ while True:
+ self._write_u32_le(hash_input, nonce, 0)
+ hash_result = await self._sha256(hash_input)
+ if self._check_leading_zeros(hash_result, leading_zeros):
+ packet.nonce = nonce
+ packet.hash = hash_result
+ break
+ nonce += 1
+ return packet
+
+ async def query_author_packets(
+ self,
+ author_keys: KeyPair,
+ count: int,
+ offset: int
+ ) -> Tuple[DataPacketList, int]:
+ msg = bytearray(32 + 32 + 2 + 8)
+ msg[0:32] = author_keys.encryption_pub
+ msg[32:64] = author_keys.signing_pub
+ self._write_u16_le(msg, count, 64)
+ self._write_u64_le(msg, offset, 66)
+ sig = await self._sign_data(msg, author_keys.signing_priv)
+ send_data = bytearray(1 + 32 + 32 + 64 + 2 + 8)
+ off = 0
+ send_data[off] = 0x16
+ off += 1
+ send_data[off:off + 32] = author_keys.encryption_pub
+ off += 32
+ send_data[off:off + 32] = author_keys.signing_pub
+ off += 32
+ send_data[off:off + 64] = sig
+ off += 64
+ self._write_u16_le(send_data, count, off)
+ off += 2
+ self._write_u64_le(send_data, offset, off)
+ response, resp_len, error = await self._client_request(send_data, len(send_data))
+ if error != MpClientError.SUCCESS:
+ return DataPacketList([], 0, offset), error
+ if resp_len < 11 or response[0] != 0x1d:
+ return DataPacketList([], 0, offset), MpClientError.INVALID_RESPONSE
+ resp_count = self._read_u16_le(response, 1)
+ new_offset = self._read_u64_le(response, 3)
+ expected_len = 11 + resp_count * PACKET_SIZE
+ if resp_len != expected_len:
+ return DataPacketList([], 0, offset), MpClientError.INVALID_RESPONSE
packets = []
- if packet_list.count > 0 and packet_list.packets:
- for i in range(packet_list.count):
- packets.append(PyDataPacket.from_c(packet_list.packets[i]))
-
- # Free the allocated memory safely
- try:
- import ctypes.util
- libc_name = ctypes.util.find_library('c')
- if libc_name:
- libc = ctypes.CDLL(libc_name)
- libc.free.argtypes = [c_void_p]
- libc.free(cast(packet_list.packets, c_void_p))
- else:
- # Platform-specific fallback
- try:
- if sys.platform.startswith('linux'):
- libc = ctypes.CDLL('libc.so.6')
- elif sys.platform == 'darwin':
- libc = ctypes.CDLL('libc.dylib')
- elif sys.platform == 'win32':
- libc = ctypes.CDLL('msvcrt.dll')
- else:
- libc = None
-
- if libc:
- libc.free.argtypes = [c_void_p]
- libc.free(cast(packet_list.packets, c_void_p))
- except OSError:
- pass # Can't free, but better than crash
- except:
- pass # Memory will be freed when process exits
-
- return PyDataPacketList(
- packets=packets,
- count=packet_list.count,
- new_offset=packet_list.new_offset
- ) \ No newline at end of file
+ packet_count = 0
+ for i in range(resp_count):
+ packet, success = self._s2data_packet(response[11 + i * PACKET_SIZE:11 + (i + 1) * PACKET_SIZE])
+ if success:
+ packets.append(packet)
+ packet_count += 1
+ return DataPacketList(packets, packet_count, new_offset), MpClientError.SUCCESS \ No newline at end of file