diff options
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | Makefile | 19 | ||||
-rw-r--r-- | README.md | 11 | ||||
-rw-r--r-- | requirements.txt | 10 | ||||
-rw-r--r-- | term.py | 131 | ||||
-rw-r--r-- | tor-site-tester.py | 221 |
6 files changed, 397 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..842f55d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.swp +tordata/ +logs/ +__pycache__/ + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..56c9dbc --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +all: dirs requirements.txt + +requirements.txt: venv + . venv/bin/activate; \ + pip install --upgrade pip; \ + pip install -r requirements.txt; \ + +venv: + pip3 install virtualenv + python3 -m virtualenv venv + +dirs: + mkdir -p tordata + mkdir -p logs + +clean: + rm -rf venv/ + rm -rf logs/ + rm -rf tordata/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..b6ece51 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ + + +You need to have `python3`, `pip`, `make` and `tor` installed (e.g. `apt/dnf install python3 python3-pip make tor`). + +``` +make +source venv/bin/activate +python tor-site-tester.py -h +``` + +Defaults to use ports 7000 and up, and 9000 and up. Needs two ports for each country. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9a04eca --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +certifi==2022.9.14 +cffi==1.15.1 +charset-normalizer==2.1.1 +cryptography==38.0.1 +idna==3.4 +pycparser==2.21 +PySocks==1.7.1 +requests==2.28.1 +stem==1.8.0 +urllib3==1.26.12 @@ -0,0 +1,131 @@ +from threading import Thread, Lock +from time import sleep +from datetime import datetime +from queue import Queue +import sys + +class Term: + spinner = ["/", "-", "\\", "|", "/", "-", "\\", "|"] + logfile = f"logs/{datetime.now().isoformat()}.log" + term = sys.stdout + + _th: Thread + _process_name = None + _process_state = None + _process_spin = False + _print_lock = Lock() + _logfile = None + + @staticmethod + def begin(): + Term._th = Thread(target=Term._term_thread) + Term._th.daemon = True + Term._th.start() + Term._logfile = open(Term.logfile, "a") + + @staticmethod + def _term_thread(): + spin_i = 0 + while True: + sleep(0.2) + spin_i += 1 + with Term._print_lock: + if Term._process_name is not None: + Term.mov_ls() + Term.write("\u001b[36m[") + Term.write(Term.spinner[spin_i % len(Term.spinner)] if Term._process_spin else " ") + Term.write(f"] {Term._process_name}") + if Term._process_state is not None: + Term.write(f"\u001b[35m - {Term._process_state}") + + Term.write("\u001b[0m") + Term.flush() + + @staticmethod + def flush(): Term.term.flush() + + @staticmethod + def write(s: str): Term.term.write(s) + + @staticmethod + def mov_ls(): Term.write(f"\u001b[2K\r") + + @staticmethod + def newln(): Term.write("\n") + + @staticmethod + def log(logmsg: str): + logmsg = str(logmsg) + with Term._print_lock: + Term._logfile.write(logmsg + "\n") + Term._logfile.flush() + Term.mov_ls() + if Term._process_name is not None: + Term.write(" ") + Term.write(logmsg) + Term.newln() + Term.flush() + + @staticmethod + def proc_start(procname: str, spin: bool = False): + with Term._print_lock: + Term._logfile.write(f"start [{procname}]\n") + Term._logfile.flush() + if Term._process_name is not None: + Term.proc_end("New process started (old may or may no actually have ended)", result=1) + if spin: + Term._process_spin = True + Term._process_name = procname + + @staticmethod + def proc_state(state: str): + with Term._print_lock: + Term._logfile.write(f"[{Term._process_name}] {state}\n") + Term._logfile.flush() + if Term._process_name is not None: + Term._process_state = state + else: + Term.log(f"State for unknown process: {state}") + + @staticmethod + def proc_end(msg = None, result=0): + """ Result 0=success, 1=error, 2=warning. """ + colour = ["\u001b[32m", "\u001b[31m", "\u001b[33m"] + with Term._print_lock: + if Term._process_name is None: + raise Exception("No process to end...") + Term._logfile.write(f"end [{Term._process_name}] {msg}\n") + Term._logfile.flush() + Term.mov_ls() + Term.write(colour[result]) + symb = " " + if result == 0: symb = "+" + if result == 1: symb = "-" + if result == 2: symb = "*" + Term.write(f"[{symb}] ") + Term.write("\u001b[0m") + Term.write(Term._process_name) + if msg is not None: + Term.write(f" - {msg}") + Term.write("\u001b[0m") + Term.newln() + Term.flush() + Term._process_name = None + Term._process_state = None + Term._process_spin = False + + @staticmethod + def proc_spin(): Term._process_spin = True + + @staticmethod + def proc_nospin(): Term._process_spin = False + + +if __name__ == "__main__": + Term.begin() + sleep(2) + Term.proc_start("TEST") + Term.log("LGOGGGG") + sleep(2) + Term.proc_spin() + Term.log("LGOGGGG") diff --git a/tor-site-tester.py b/tor-site-tester.py new file mode 100644 index 0000000..ac1d112 --- /dev/null +++ b/tor-site-tester.py @@ -0,0 +1,221 @@ +import requests +from stem import Signal, StreamStatus +from stem.control import Controller, EventType +import stem.process +from dataclasses import dataclass +from typing import Any +from argparse import ArgumentParser +import functools +from time import perf_counter, sleep + +from term import Term + + +@dataclass +class TORProc: + torproc: any + port: int + exit_node: str + + def __del__(self): + if self.torproc is not None: + self.torproc.kill() + if self.controller is not None: + self.controller.close() + + def __post_init__(self): + self.last_exit_node = None + self.controller = Controller.from_port(port=self.port + 2000) + self.controller.connect() + self.controller.authenticate() + self.stream_listener = functools.partial(self.stream_event, self.controller) + self.controller.add_event_listener(self.stream_listener, EventType.STREAM) + + def stream_event(self, controller, event): + if event.status == StreamStatus.SUCCEEDED:# and (event.circ_id == 1): + try: + circ = controller.get_circuit(event.circ_id) + exit_fingerprint = circ.path[-1][0] + exit_relay = controller.get_network_status(exit_fingerprint) + locale = controller.get_info("ip-to-country/%s" % exit_relay.address, 'unknown') + self.last_exit_node = (locale, exit_relay.address, exit_relay.fingerprint, exit_relay.nickname) + except Exception as e: Term.log(e) + + @property + def proxies(self): + return { + 'http': f'socks5h://127.0.0.1:{self.port}', + 'https': f'socks5h://127.0.0.1:{self.port}' + } + + def get_proxy_info(self): + info = requests.get('https://www.ipinfo.io', proxies=self.proxies).json() + remote_info = ("Could not get second opinion on exit node...", None, None, None) + if ("ip" in info) and ("country" in info) and ("region" in info): + remote_info = ( + f"Remote reported as {info['country']}, {info['region']} (ip {info['ip']})", + info["country"].lower() == self.exit_node.lower(), + info["country"], + info["ip"], + info["region"] + ) + + local_info = ( + f"Exit node \"{self.last_exit_node[3]}\" {self.last_exit_node[2]} in {self.last_exit_node[0]} (ip {self.last_exit_node[1]})", + self.last_exit_node[0] == self.exit_node.lower(), + *self.last_exit_node + ) + status = 1 + if remote_info[1] and local_info[1]: status = 0 + elif (remote_info[1] is None) and local_info[1]: status = 2 + return status, remote_info, local_info + + +class TORConnections: + + + def __init__(self, start_port = 7000): + self._tor_procs = [] + self._start_port = start_port + self._cport = self._start_port + + + def start_tor_proc(self, exit_node): + port = self._cport + self._cport += 1 + torproc = stem.process.launch_tor_with_config( + config = { + "ControlPort": str(port + 2000), + "SocksPort": str(port), + "ExitNodes": "{" + exit_node + "}", + "DataDirectory": "tordata/tor" + str(port) + }, + init_msg_handler = Term.proc_state, + take_ownership = True + ) + Term.proc_state("Attaching controller to TOR process") + torproc = TORProc(torproc, port, exit_node) + self._tor_procs.append(torproc) + return torproc + + + def close_tor_procs(self): + for proc in self._tor_procs: + proc.torproc.kill() + + + def get_country_proc(self, exit_node): + for proc in self._tor_procs: + if proc.exit_node == exit_node: + return proc + raise Exception("No TOR proxy with that exit country running, are you sure you have started it?") + + def remove_tor_proc(self, torproc: TORProc): + del self._tor_procs[self._tor_procs.index(torproc)] + + +def do_request(url, proxies = None): + t1 = perf_counter() + try: + res = requests.get(url, proxies=proxies) + return (res.status_code, perf_counter() - t1) + except requests.ConnectionError as e: + return (e, perf_counter() - t1) + + +def test_sites(tor: TORConnections, sites: list[str], countries: list[str]): + results = {site: dict() for site in sites} + for country in countries: + Term.proc_start(f"Start TOR Proxy with endpoint in {country}", spin=True) + try: + torproc = tor.start_tor_proc(country) + Term.proc_state("Checking connection endpoint") + status, r_info, l_info = torproc.get_proxy_info() + Term.proc_end(msg=f"{l_info[0]} | {r_info[0]}", result=status) + except Exception as e: + Term.proc_end(msg=f"Could not start proxy ({e}).", result=1) + continue + + Term.proc_start(f"Test sites via {country}", spin=True) + for i, site in enumerate(sites): + Term.proc_state(f"Site {i}/{len(sites)}") + results[site][country] = do_request(site, torproc.proxies) + sleep(1) + Term.proc_end() + + tor.remove_tor_proc(torproc) + + Term.proc_start("Test sites without proxy", spin=True) + for i, site in enumerate(sites): + Term.proc_state(f"Site {i}/{len(sites)}") + results[site][""] = do_request(site) + Term.proc_end() + return results + + +def pp_results(countries: list[str], results: dict[str, dict[str, Any]], fn = None): + countries.insert(0, "") + tbl = [["site"]] + for country in countries: + tbl[0].append(f"{country}") + tbl[0].append(f"{country} time") + for site, res in results.items(): + row = ["" for _ in range(len(tbl[0]))] + row[0] = site + for country, res in res.items(): + row[tbl[0].index(f"{country}")] = str(res[0]) + row[tbl[0].index(f"{country} time")] = f"{res[1]:7.5f}" + tbl.append(tuple(row)) + if fn is not None: + with open(fn, "w") as f: + for row in tbl: + f.write(", ".join(row)) + f.write("\n") + table(tbl) + + +def table(tbl): + w = [0 for _ in range(len(tbl[0]))] + for row in tbl: + for i, cell in enumerate(row): + w[i] = max(len(str(cell)), w[i]) + + print("+" + "+".join("-" * (x+2) for x in w) + "+") + for j, row in enumerate(tbl): + for i, cell in enumerate(row): + c = cell + if c == "200": c = f"\u001b[32m{c}\u001b[0m" + if c == "403": c = f"\u001b[31m{c}\u001b[0m" + print(f"| {c:{w[i]}} ", end="") + print("|") + if j == 0: + print("+" + "+".join("-" * (x+2) for x in w) + "+") + print("+" + "+".join("-" * (x+2) for x in w) + "+") + +if __name__ == "__main__": + parser = ArgumentParser() + parser.add_argument("--port-start", type=int, default=7000) + parser.add_argument("-c", "--countries", type=str, required=True) + parser.add_argument("-s", "--sites-file", type=str, required=True) + parser.add_argument("-o", "--out-file", type=str, default=None) + args = parser.parse_args() + + Term.begin() + + Term.proc_start("Read sites", spin=True) + with open(args.sites_file, "r") as f: + sites = [x.strip() for x in f.readlines() if x.strip() != ""] + Term.proc_end() + + tor = TORConnections(args.port_start) + try: + countries = args.countries.split(",") + results = test_sites(tor, sites, countries) + pp_results(countries, results, args.out_file) + except KeyboardInterrupt: + Term.log("Interrupt received, exiting") + except Exception as e: + print(e) + raise e + finally: + tor.close_tor_procs() |