From ee12742fc0e4b9211bc8438f4a83890af379fdb8 Mon Sep 17 00:00:00 2001 From: Rebel Zhang Date: Sun, 28 Sep 2025 10:45:08 +0800 Subject: [PATCH] Add send message function --- ressenger_client.py | 98 +++++++++++++++++++++++++------------ ressenger_common.py | 23 +++++++-- ressenger_cryptography.py | 2 +- ressenger_initialisation.py | 2 +- ressenger_server.py | 17 +------ ressenger_user.py | 2 + 6 files changed, 92 insertions(+), 52 deletions(-) diff --git a/ressenger_client.py b/ressenger_client.py index e0482d7..7879fe4 100755 --- a/ressenger_client.py +++ b/ressenger_client.py @@ -11,6 +11,17 @@ Address = str # Default timeout for socket operations (seconds) _DEFAULT_TIMEOUT = 10.0 +# Return codes +SUCCESS = 0 +ERR_TYPE = 1 # invalid data type (TypeError) +ERR_PAYLOAD_TOO_LARGE = 2 # payload exceeds supported length (ValueError) +ERR_SOCKET_ERROR = 3 # plain TCP connection error or timeout +ERR_SEND_FAILED = 4 # failure while sending data +ERR_SOCKS_MISSING = 5 # PySocks (socks) module not installed +ERR_INVALID_SOCKS_PORT = 6 # I2P_SOCKS_PORT environment variable not an integer +ERR_PROXY_CONNECTION = 7 # error connecting through SOCKS proxy (I2P) +ERR_UNKNOWN = 8 # unknown/unexpected error + def _prepare_payload(data: typing.Union[bytes, str]) -> bytes: """ @@ -37,7 +48,7 @@ def _pack_message(payload: bytes) -> bytes: return struct.pack("!I", length) + payload -def send_data(data: typing.Union[bytes, str], addr: Address, port: int) -> None: +def send_data(data: typing.Union[bytes, str], addr: Address, port: int) -> int: """ Send a single length-prefixed message over plain TCP to (addr, port). @@ -48,22 +59,37 @@ def send_data(data: typing.Union[bytes, str], addr: Address, port: int) -> None: Behaviour: - Connects, sends the framed message, then closes the socket. - - Raises built-in socket exceptions on failure. - - Example: - send_data(b'hello', '192.0.2.1', 12345) + - Returns 0 on success, otherwise returns an error code. """ - payload = _prepare_payload(data) - message = _pack_message(payload) + try: + payload = _prepare_payload(data) + except TypeError: + return ERR_TYPE + try: + message = _pack_message(payload) + except ValueError: + return ERR_PAYLOAD_TOO_LARGE # Create a TCP connection with a timeout - with socket.create_connection((addr, port), timeout=_DEFAULT_TIMEOUT) as sock: - # Ensure blocking mode for sendall - sock.setblocking(True) - sock.sendall(message) + try: + with socket.create_connection((addr, port), timeout=_DEFAULT_TIMEOUT) as sock: + # Ensure blocking mode for sendall + sock.setblocking(True) + try: + sock.sendall(message) + except (socket.timeout, OSError): + # Sending failed after connection + return ERR_SEND_FAILED + except (socket.timeout, OSError): + # Could not create connection (including DNS, connect, timeout) + return ERR_SOCKET_ERROR + except Exception: + return ERR_UNKNOWN + + return SUCCESS -def send_data_i2p(data: typing.Union[bytes, str], addr: Address, port: int) -> None: +def send_data_i2p(data: typing.Union[bytes, str], addr: Address, port: int) -> int: """ Send a single length-prefixed message to an I2P destination via a local i2pd SOCKS5 proxy. @@ -76,33 +102,29 @@ def send_data_i2p(data: typing.Union[bytes, str], addr: Address, port: int) -> N - Uses PySocks (socks) to create a socket that speaks SOCKS5 to the local proxy. - Uses remote DNS resolution (rdns=True) so that the proxy resolves .i2p names. - Connects, sends the framed message, then closes the socket. - - Raises ImportError if PySocks is not installed. - - Raises socket or proxy-related exceptions on failure. - - Environment variables: - - I2P_SOCKS_HOST (default '127.0.0.1') - - I2P_SOCKS_PORT (default '4447') - - Example: - send_data_i2p(b'hello', 'abcd1234xxxxxxx.b32.i2p', 12345) + - Returns 0 on success, otherwise returns an error code. """ try: import socks # PySocks; license: MIT - except Exception as e: - raise ImportError( - "PySocks is required for send_data_i2p. " - "Install it with: pip install PySocks" - ) from e + except Exception: + return ERR_SOCKS_MISSING - payload = _prepare_payload(data) - message = _pack_message(payload) + try: + payload = _prepare_payload(data) + except TypeError: + return ERR_TYPE + + try: + message = _pack_message(payload) + except ValueError: + return ERR_PAYLOAD_TOO_LARGE socks_host = os.environ.get("I2P_SOCKS_HOST", "127.0.0.1") socks_port_str = os.environ.get("I2P_SOCKS_PORT", "4447") try: socks_port = int(socks_port_str) except ValueError: - raise ValueError("I2P_SOCKS_PORT environment variable must be an integer") + return ERR_INVALID_SOCKS_PORT # Create a socksified socket. socks.socksocket has the same interface as socket.socket. s = socks.socksocket() @@ -114,11 +136,27 @@ def send_data_i2p(data: typing.Union[bytes, str], addr: Address, port: int) -> N # Connect through the proxy to the I2P destination. # When rdns=True, the proxy will perform name resolution for .i2p addresses. s.connect((addr, port)) + except (socket.timeout, OSError, Exception): + # Proxy connection / resolution failed + try: + s.close() + except Exception: + pass + return ERR_PROXY_CONNECTION + + try: # Ensure blocking mode for sendall s.setblocking(True) - s.sendall(message) + try: + s.sendall(message) + except (socket.timeout, OSError): + return ERR_SEND_FAILED + except Exception: + return ERR_UNKNOWN finally: try: s.close() except Exception: pass + + return SUCCESS diff --git a/ressenger_common.py b/ressenger_common.py index 5d36ec1..776f44d 100755 --- a/ressenger_common.py +++ b/ressenger_common.py @@ -1,9 +1,6 @@ #!/usr/bin/python3 -import ressenger_cryptography, ressenger_exceptions -import pickle, pathlib - -# class Contact(): -# def __init__(self, nickname, b32address, pub_enc, pub_sig): +import ressenger_cryptography, ressenger_exceptions, ressenger_client +import pickle, pathlib, time def load_user(password, username='default'): user_path=pathlib.Path(f'~/.ressenger/{username}').expanduser() @@ -21,3 +18,19 @@ def dump_user(password, user, username='default'): else: with open(user_path, 'wb') as file: file.write(ressenger_cryptography.encrypt_bytes(pickle.dumps(user, protocol=pickle.HIGHEST_PROTOCOL), password)) + +def send_message(text, destination_addr, destination_port, enc_pub_key, sig_pri_key, sig_pub_key): + data={'type':'text', 'sent_time'=time.time_ns(), 'data':{'content':text}} + data_encrypted, data_signature=ressenger_cryptography.encrypt(pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL), enc_pub_key, sig_pri_key) + packet={'data':data_encrypted, 'sig':data_signature, 'enc_pub':enc_pub_key, 'sig_pub':sig_pub_key} + errorno=ressenger_client.send_data_i2p(pickle.dumps(packet, protocol=pickle.HIGHEST_PROTOCOL), destination_addr, destination_port) + event={'type':'sent', 'data':data, 'dest':{'addr':destination_addr, 'port':destination_port}, 'errorno':errorno} + return event + +def send_file(filename, filebytes, destination_addr, destination_port, enc_pub_key, sig_pri_key, sig_pub_key) + data={'type':'file', 'sent_time'=time.time_ns(), 'data':{'content':filebytes, 'filename':filename}} + data_encrypted, data_signature=ressenger_cryptography.encrypt(pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL), enc_pub_key, sig_pri_key) + packet={'data':data_encrypted, 'sig':data_signature, 'enc_pub':enc_pub_key, 'sig_pub':sig_pub_key} + errorno=ressenger_client.send_data_i2p(pickle.dumps(packet, protocol=pickle.HIGHEST_PROTOCOL), destination_addr, destination_port) + event={'type':'sent', 'data':data, 'dest':{'addr':destination_addr, 'port':destination_port}, 'errorno':errorno} + return event diff --git a/ressenger_cryptography.py b/ressenger_cryptography.py index 56fb6f1..7eebdeb 100755 --- a/ressenger_cryptography.py +++ b/ressenger_cryptography.py @@ -185,7 +185,7 @@ def decrypt(encrypted_bytes: bytes, decryption_private_key_pem: bytes, verificat # # generate sender signing keypair (sender) # priv_sig, pub_sig = generate_keypair() # -# message = b"I love Ruby!" +# message = b"I love C!" # out = encrypt(message, pub_enc, priv_sig) # if out is None: # raise SystemExit("Encryption failed") diff --git a/ressenger_initialisation.py b/ressenger_initialisation.py index acaab01..da90de8 100755 --- a/ressenger_initialisation.py +++ b/ressenger_initialisation.py @@ -23,4 +23,4 @@ def initialise(password, b32address, username='default', port=5273, nick='John D enc_pri, enc_pub=ressenger_cryptography.generate_keypair() sig_pri, sig_pub=ressenger_cryptography.generate_keypair() with open(user_path, 'wb') as file: - file.write(ressenger_cryptography.encrypt_bytes(pickle.dumps({'port':port, 'b32address':b32address, 'enc_pri':enc_pri, 'enc_pub':enc_pub, 'sig_pri':sig_pri, 'sig_pub':sig_pub, 'keyring':[], 'events':{}, 'contacts':{}}, protocol=pickle.HIGHEST_PROTOCOL), password)) + file.write(ressenger_cryptography.encrypt_bytes(pickle.dumps({'port':port, 'b32address':b32address, 'enc_pri':enc_pri, 'enc_pub':enc_pub, 'sig_pri':sig_pri, 'sig_pub':sig_pub, 'contacts':{}, 'events':{}}, protocol=pickle.HIGHEST_PROTOCOL), password)) diff --git a/ressenger_server.py b/ressenger_server.py index 0fa9553..16cfc1e 100755 --- a/ressenger_server.py +++ b/ressenger_server.py @@ -1,17 +1,4 @@ -#!/usr/bin/env python3 -""" -TCP server that receives binary blobs and persists them to a cache file. -This variant ensures the cache file is removed on shutdown (where possible). - -Behaviour: - - Always start with a fresh, empty CACHE (do NOT load existing file). - - Use a dedicated CacheWriter thread to debounce and atomically write CACHE. - - On exit (signals, KeyboardInterrupt, normal interpreter exit) stop writer and remove cache file. -Note: SIGKILL (kill -9) and sudden power loss cannot be trapped; cleanup cannot run then. -""" - -# English comments throughout (British English spelling). - +#!/usr/bin/python3 import socket import threading import struct @@ -226,7 +213,7 @@ def run_server(port: int): CACHE_PATH.parent.mkdir(parents=True, exist_ok=True) with cache_lock: atomic_dump(CACHE, CACHE_PATH) - print("Initialised fresh empty CACHE on disk (existing file not loaded).") + print("Initialised fresh empty CACHE on disk.") except Exception: print("Failed to initialise cache file on disk") traceback.print_exc() diff --git a/ressenger_user.py b/ressenger_user.py index a93a4bf..c64747f 100755 --- a/ressenger_user.py +++ b/ressenger_user.py @@ -1 +1,3 @@ #!/usr/bin/python3 +import ressenger_common +