From eca131347c4561f3102a9e2c75a0d01cd5714488 Mon Sep 17 00:00:00 2001 From: Aleteoryx Date: Wed, 8 Apr 2026 13:06:24 -0400 Subject: [PATCH] nanobnc --- README.md | 34 ++++++++++++++++++ nanobnc.py | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++ ntalk.tcl | 8 ++++- 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100755 nanobnc.py diff --git a/README.md b/README.md index 57e7db1c36cd954751ed92151f988e297bbf4075..9abef9afe2c7b24859a123a17bc97196cfd7bba8 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,11 @@ the connection is done entirely over $sok, so set that to whatever. if you want to include e.g. tcltls, go ahead. the config file is saved to/read from ~/.config/ntalk/cscript.tcl. +there is a `connect` proc available to the config, that automatically sets up the socket +with the right encoding and line endings. it is called the same as `socket`. it is +provided to make interacting with nanobnc.py easier. its use is not mandatory, ntalk will +ensure the socket is setup right later. + right click on any sixel in the chatlog, and you can save it under a custom name. saved sixels can be accessed through the "sixels" option in the top menu. slashes are interpreted, so naming one e.g. "faces / :D" will create a submenu called "faces". @@ -61,6 +66,35 @@ consists of the following scripts: - `9talk/motd addr`: checks for the most recent MOTD of $addr, and echos it. the most recent MOTD and last-seen message id are stored in $home/lib/9talk/$addr.motd and $home/lib/9talk/$addr.id, respectively. +## `nanobnc.py`: simple nanochat bouncer + +this script should be run behind a connection broker, like socat. it assumes a client is +attached to stdio. run it like: + +``` +socat TCP-LISTEN:22344,fork EXEC:'python nanobnc.py [timeout]' +``` + +timeout is in seconds. it defaults to 1800, 30 minutes. + +when a client connects, it should send a line of the form ` `. if +the password matches the one passed as an argument, the script will connect to the +upstream nanochat server at addr:port. data is blindly copied between the client and the +upstream, until either the client and server are idle for timeout seconds, or until the +client sends QUIT, at which point the script gracefully disconnects from the upstream +server, and exits + +to use this with ntalk, replace the `set sok ...` line in your config with the following: + +``` +set sok [connect ] +puts $sok " " +``` + +as this script is essentially an open TCP proxy, be very careful with your choice of +password, and with who you allow to access the server. + + ## `scrollbackup.sh`: simple backup script just sends HIST and QUIT and logs it to a file whose name indicates diff --git a/nanobnc.py b/nanobnc.py new file mode 100755 index 0000000000000000000000000000000000000000..fd846d5caced530b1ef3bdc2463b5e0b86b89c61 --- /dev/null +++ b/nanobnc.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python + +import socket +import threading +from sys import exit, argv, stderr, stdout, stdin +from signal import alarm, signal, SIGALRM + +argv0 = 'nanobnc.py' +sok = None + +def usage(): + print(f'usage: {argv0} passphrase [timeout]') + exit(1) + +def handle_args(): + global argv0, argv + if len(argv) > 0: + argv0 = argv[0] + argv = argv[1:] + if len(argv) not in (1, 2): + usage() + + passphrase = argv[0] + if len(argv) == 1: + timeout = 1800 + else: + timeout = int(argv[1]) + + return passphrase, timeout + +def handle_req(passphrase): + req = input().split(maxsplit=2) + if len(req) != 3: + print('bad # of connection args') + exit(3) + host, port, pw = req + + if pw != passphrase: + print('bad passphrase') + print(f'bad passphrase trying to connect to {host}:{port}', file=stderr) + exit(4) + + try: + port = int(port) + except: + print('bad port') + exit(3) + + return host, port + +def shutdown(): + if sok is not None: + try: + sok.sendall(b'QUIT\n') + sok.shutdown(socket.SHUT_RDWR) + except: + pass + +def onalarm(sig, stk): + try: + print('timeout!') + stdout.flush() + except: + pass + try: + print('timeout!', file=stderr) + stderr.flush() + except: + pass + + shutdown() + exit(2) + +signal(SIGALRM, onalarm) + +passphrase, timeout = handle_args() +alarm(timeout) +host, port = handle_req(passphrase) + +try: + sok = socket.create_connection((host, port)) +except Exception as e: + print(f'connection failed: {e}') + exit(5) + +def toserver(): + while (s := stdin.readline()) != '': + if s == 'QUIT\n': + break + sok.sendall(s.encode()) + shutdown() + exit(0) + +def toclient(): + fp = sok.makefile('r', encoding='ascii', errors='ignore') + while (s := fp.readline()) != '': + stdout.write(s) + stdout.flush() + shutdown() + exit(0) + +t = threading.Thread(target=toclient) +t.start() +toserver() diff --git a/ntalk.tcl b/ntalk.tcl index 71a77bc6d6a6ea789e3a384a199431388014b9b8..c3764aa69f1d0d396ace9286829d74023faba09b 100755 --- a/ntalk.tcl +++ b/ntalk.tcl @@ -502,6 +502,12 @@ bind .buffer { ### SERVER MANAGEMENT ### +proc connect {addr port} { + set sok [socket $addr $port] + fconfigure $sok -translation lf -blocking false -encoding iso8859-1 + return $sok +} + proc gencscript {{server localhost} {port 44322}} { set server [list $server] set port [list $port] @@ -516,7 +522,7 @@ proc gencscript {{server localhost} {port 44322}} { set server $server set port $port }] - append ret {set sok [socket $server $port]} + append ret {set sok [connect $server $port]} return [string trim $ret] }