tor-private-network/bin/request
2026-10-07 11:29:30 -06:00

88 lines
3.6 KiB
Python
Executable file

#!/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)