Add send message function

main
Rebel Zhang 2025-09-28 10:45:08 +08:00
parent 34e73a020b
commit ee12742fc0
6 changed files with 92 additions and 52 deletions

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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))

View File

@ -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()

View File

@ -1 +1,3 @@
#!/usr/bin/python3
import ressenger_common