#!/usr/bin/env python3 """Fetch the lab page and report the circuit that carried this request.""" import re import socket import subprocess import sys import time def reply(reader): """Read one control reply, including GETINFO's dot-terminated data.""" rows = [] while True: line = reader.readline().rstrip("\r\n") if not line: raise RuntimeError("Tor closed its control connection") if line.startswith("650 "): continue rows.append(line) if line.startswith("250+"): while True: line = reader.readline().rstrip("\r\n") if line == ".": break if not line: raise RuntimeError("Incomplete circuit list") rows.append(line) elif line.startswith("250 "): return rows elif not line.startswith("250-"): raise RuntimeError(line) def main(): marker = sys.argv[1] if len(sys.argv) == 2 else "tor-lab-01" if len(sys.argv) > 2 or not re.fullmatch(r"[A-Za-z0-9-]{1,64}", marker): raise RuntimeError("Use: request [word-with-letters-numbers-or-hyphens]") with socket.create_connection(("127.0.0.1", 9051), timeout=5) as control: control.settimeout(5) reader = control.makefile("r", encoding="utf-8", newline="") for command in ("AUTHENTICATE", "SETEVENTS STREAM"): control.sendall((command + "\r\n").encode()) reply(reader) for attempt in range(2): result = subprocess.run( ["curl", "--fail", "--silent", "--show-error", "--max-time", "30", "--socks5-hostname", "127.0.0.1:9050", f"http://10.77.0.80/?{marker}"], capture_output=True, text=True, timeout=35, check=False, ) # A failed SOCKS handshake never established a successful stream. # Retry it once while the private network finishes starting. if result.returncode != 97 or attempt == 1: break print("SOCKS connection failed; retrying once in 3 seconds.", file=sys.stderr) time.sleep(3) if result.returncode: raise RuntimeError(result.stderr.strip() + "; check the client bootstrap log and retry") while True: line = reader.readline().strip() if not line: raise RuntimeError("No stream event received") fields = line.split() if (len(fields) >= 6 and fields[:2] == ["650", "STREAM"] and fields[3] == "SUCCEEDED" and fields[5] == "10.77.0.80:80"): stream_id, circuit_id = fields[2], fields[4] break control.sendall(b"SETEVENTS\r\n") reply(reader) control.sendall(b"GETINFO circuit-status\r\n") for line in reply(reader): fields = line.split() if len(fields) >= 3 and fields[:2] == [circuit_id, "BUILT"]: names = [hop.replace("~", "=").split("=")[-1] for hop in fields[2].split(",")] print(result.stdout.strip()) print(f"Marker: {marker}") print(f"Stream {stream_id} used circuit {circuit_id}: " + " -> ".join(names)) print("Positions: guard -> middle -> exit") return raise RuntimeError("The used circuit closed before it could be listed; retry") if __name__ == "__main__": try: main() except (OSError, RuntimeError, subprocess.TimeoutExpired) as error: print(f"request: {error}", file=sys.stderr) sys.exit(1)