#!/usr/bin/env python3 """Quake Live RCON over ZMQ. Runs inside the container (through ./rcon), so the password never leaves the server machine. ./rcon status one command ./rcon interactive prompt (Ctrl+D or 'exit' to quit) Protocol (id's zmq_rcon.py): DEALER socket, ZMQ PLAIN auth user 'rcon', zap_domain 'rcon', first message 'register', then each message is a console command; console output comes back. """ import os, re, sys, uuid import zmq PW = open("/ql/secrets/rcon-password.txt").read().strip() HOST = "tcp://127.0.0.1:" + os.environ.get("QL_RCON_PORT", "28960") COLOR = re.compile(r"\^\d") def connect(): s = zmq.Context.instance().socket(zmq.DEALER) s.plain_username = b"rcon" s.plain_password = PW.encode() s.zap_domain = b"rcon" s.setsockopt(zmq.IDENTITY, uuid.uuid4().hex.encode()) s.setsockopt(zmq.LINGER, 0) s.connect(HOST) s.send(b"register") return s def drain(s, first_wait, quiet=0.6): out, wait = [], first_wait while s.poll(int(wait * 1000)): out.append(s.recv().decode(errors="replace")) wait = quiet return "".join(out) def clean(text): # never show the password, whatever the server echoes lines = [COLOR.sub("", l) for l in text.splitlines() if "zmq_rcon_password" not in l and PW not in l] return "\n".join(lines).strip() def run(s, cmd): drain(s, first_wait=0.3, quiet=0.2) # drop console chatter that arrived in between s.send(cmd.encode()) out = clean(drain(s, first_wait=3.0)) return out or "(no output: ok, or server down / map loading / wrong password)" def main(argv): s = connect() if len(argv) > 1: print(run(s, " ".join(argv[1:]))); return 0 print("Quake Live rcon - server commands (status, addbot hunter 5, kick allbots, map thunderstruck lgvamp, say hi). Ctrl+D to quit.") while True: try: line = input("ql> ").strip() except (EOFError, KeyboardInterrupt): print(); return 0 if line in ("exit", "quit"): return 0 if line: print(run(s, line)) if __name__ == "__main__": sys.exit(main(sys.argv))