88 lines
3.6 KiB
Python
Executable file
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)
|