aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjakobst1n <jakob.stendahl@outlook.com>2022-09-23 21:19:26 +0000
committerjakob <jakob@jakobstendahl.no>2022-09-23 22:10:45 +0000
commit71e9f98a372971275befeae1515393c1b283dbe6 (patch)
tree1b411ac66779c717bf9ceba10e2c0695ab76abfd
downloadtor-site-tester-71e9f98a372971275befeae1515393c1b283dbe6.tar.gz
tor-site-tester-71e9f98a372971275befeae1515393c1b283dbe6.zip
Initial commit
-rw-r--r--.gitignore5
-rw-r--r--Makefile19
-rw-r--r--README.md11
-rw-r--r--requirements.txt10
-rw-r--r--term.py131
-rw-r--r--tor-site-tester.py221
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
diff --git a/term.py b/term.py
new file mode 100644
index 0000000..59c02c7
--- /dev/null
+++ b/term.py
@@ -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()