stcp: ed25519 pubkey exchange in handshake, fix instance destroy segfault, fix chat_tcp_test
- u_async.c: process immediate_queue BEFORE epoll_wait in uasync_poll - stcp_link.c: defensive NULL etcp_conn/etcp_link in stcp_link_close - utun_instance.c: move stcp_server_list_destroy_all to Phase J (before ETCP), add deferred drain in both Phase J and L - stcp.h/c: STCP_HS_ENC_CLIENT 6→38, STCP_HS_ENC_SERVER 7→39, exchange ed25519_pubkey in both handshake directions - etcp_connections.c: copy peer_ed25519 from STCP to ETCP_CONN in etcp_link_enter_ready_tcp; guard link->etcp in tcp_link_close_cb - chat_sync.c: fix lk->conn NULL dereference for TCP links - test_stcp.c: update stcp_server_create/stcp_client_connect callsites - tools/chat_tcp_test: change transport udp→tcp
This commit is contained in:
1 parent
a3783b5f55
commit
38c9f229bc
1272 files changed
+968388
-1
No files matched your search
Executable
+177
@@ -0,0 +1,177 @@
|
||||
# chatcli — uTun headless chat CLI client
|
||||
#
|
||||
# Usage:
|
||||
# chatcli [--host HOST] [--port PORT] <command> [args...]
|
||||
#
|
||||
# Environment:
|
||||
# CHATCLI_HOST — default host (default: 127.0.0.1)
|
||||
# CHATCLI_PORT — default port (default: 9999)
|
||||
#
|
||||
# Commands (see chatcli_commands.txt for full description):
|
||||
# ping alive check
|
||||
# status node info, connections, NTP
|
||||
# channels list all channels
|
||||
# members <ch_id> list members with full state
|
||||
# messages <ch_id> [count] read last messages
|
||||
# send <ch_id> <text> send text message
|
||||
# invite <ch_id> create invite link
|
||||
# connect <utun://...> join channel via invite link
|
||||
# create <name> create new channel
|
||||
# listen interactive event listener
|
||||
|
||||
import sys, os, json, socket, struct
|
||||
|
||||
DEFAULT_HOST = os.environ.get("CHATCLI_HOST", "127.0.0.1")
|
||||
DEFAULT_PORT = int(os.environ.get("CHATCLI_PORT", "9999"))
|
||||
TIMEOUT = 3
|
||||
|
||||
def _connect():
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(TIMEOUT)
|
||||
try:
|
||||
s.connect((DEFAULT_HOST, DEFAULT_PORT))
|
||||
except (socket.timeout, ConnectionRefusedError, OSError) as e:
|
||||
print(f"ERROR: cannot connect to {DEFAULT_HOST}:{DEFAULT_PORT} — {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return s
|
||||
|
||||
def _readline(s):
|
||||
buf = b""
|
||||
while True:
|
||||
try:
|
||||
ch = s.recv(1)
|
||||
except socket.timeout:
|
||||
break
|
||||
if not ch:
|
||||
break
|
||||
if ch == b"\n":
|
||||
break
|
||||
buf += ch
|
||||
return buf.decode("utf-8", errors="replace")
|
||||
|
||||
def _send(s, js):
|
||||
s.sendall(js.encode())
|
||||
|
||||
def _req(cmd, **params):
|
||||
s = _connect()
|
||||
js = {"id": 1, "cmd": cmd}
|
||||
js.update(params)
|
||||
_send(s, json.dumps(js, ensure_ascii=True) + "\n")
|
||||
resp = _readline(s)
|
||||
s.close()
|
||||
try:
|
||||
obj = json.loads(resp)
|
||||
except json.JSONDecodeError:
|
||||
print(f"RAW: {resp}")
|
||||
return
|
||||
if obj.get("ok"):
|
||||
data = obj.get("data", {})
|
||||
if isinstance(data, str):
|
||||
print(data)
|
||||
else:
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(f"ERROR: {obj.get('error', 'unknown')}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def cmd_ping():
|
||||
_req("ping")
|
||||
|
||||
def cmd_status():
|
||||
_req("status")
|
||||
|
||||
def cmd_channels():
|
||||
_req("channels")
|
||||
|
||||
def cmd_members(ch_id):
|
||||
_req("members", ch=ch_id)
|
||||
|
||||
def cmd_messages(ch_id, count=None):
|
||||
params = {"ch": ch_id}
|
||||
if count:
|
||||
params["count"] = int(count)
|
||||
_req("messages", **params)
|
||||
|
||||
def cmd_send(ch_id, text):
|
||||
_req("send", ch=ch_id, content_type="text", data=text)
|
||||
|
||||
def cmd_invite(ch_id):
|
||||
_req("invite", ch=ch_id)
|
||||
|
||||
def cmd_connect(link):
|
||||
_req("connect", link=link)
|
||||
|
||||
def cmd_create(name):
|
||||
_req("create_channel", name=name)
|
||||
|
||||
def cmd_listen():
|
||||
print(f"Listening on {DEFAULT_HOST}:{DEFAULT_PORT} (Ctrl+C to quit)")
|
||||
s = _connect()
|
||||
_send(s, json.dumps({"id": 1, "cmd": "subscribe", "enable": 1}) + "\n")
|
||||
try:
|
||||
while True:
|
||||
line = _readline(s)
|
||||
if not line:
|
||||
break
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
print(line)
|
||||
continue
|
||||
evt = obj.get("event", "")
|
||||
if evt == "msg":
|
||||
print(f"[{obj.get('ch','?')}] {obj.get('author_id','?')}: new message")
|
||||
elif evt == "members_changed":
|
||||
print(f"[{obj.get('ch','?')}] members changed")
|
||||
elif evt == "channel_updated":
|
||||
print(f"[{obj.get('ch','?')}] channel updated")
|
||||
elif evt == "invite_received":
|
||||
print(f"INVITE: ch={obj.get('ch','?')} name={obj.get('ch_name','?')} from={obj.get('from_id','?')}")
|
||||
else:
|
||||
print(json.dumps(obj, indent=2, ensure_ascii=False))
|
||||
except KeyboardInterrupt:
|
||||
print("\nDisconnected.")
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i] == "--host" and i + 1 < len(args):
|
||||
global DEFAULT_HOST; DEFAULT_HOST = args[i + 1]; i += 2
|
||||
elif args[i] == "--port" and i + 1 < len(args):
|
||||
global DEFAULT_PORT; DEFAULT_PORT = int(args[i + 1]); i += 2
|
||||
else:
|
||||
break
|
||||
args = args[i:]
|
||||
|
||||
if not args or args[0] in ("help", "--help", "-h"):
|
||||
print(__doc__)
|
||||
sys.exit(0)
|
||||
|
||||
cmd = args[0]
|
||||
try:
|
||||
if cmd == "ping": cmd_ping()
|
||||
elif cmd == "status": cmd_status()
|
||||
elif cmd == "channels": cmd_channels()
|
||||
elif cmd == "members": cmd_members(*args[1:2] if len(args) > 1 else (_die("usage: members <ch_id>"),))
|
||||
elif cmd in ("messages","msgs"): cmd_messages(*args[1:3] if len(args) > 1 else (_die("usage: messages <ch_id> [count]"),))
|
||||
elif cmd == "send": cmd_send(*args[1:3] if len(args) > 2 else (_die("usage: send <ch_id> <text>"),))
|
||||
elif cmd == "invite": cmd_invite(*args[1:2] if len(args) > 1 else (_die("usage: invite <ch_id>"),))
|
||||
elif cmd in ("connect","join"): cmd_connect(*args[1:2] if len(args) > 1 else (_die("usage: connect <utun://...>"),))
|
||||
elif cmd == "create": cmd_create(*args[1:2] if len(args) > 1 else (_die("usage: create <name>"),))
|
||||
elif cmd == "listen": cmd_listen()
|
||||
else: print(f"Unknown command: {cmd}\nUse --help for usage", file=sys.stderr); sys.exit(1)
|
||||
except socket.timeout:
|
||||
print(f"ERROR: timeout ({TIMEOUT}s) connecting to {DEFAULT_HOST}:{DEFAULT_PORT}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except ConnectionRefusedError:
|
||||
print(f"ERROR: connection refused at {DEFAULT_HOST}:{DEFAULT_PORT} — is uTun running with headless_control_bind?", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def _die(msg):
|
||||
print(msg, file=sys.stderr); sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user