1import logging 2import re 3import socket 4import binascii 5import sys 6import os 7import time 8import random 9import subprocess 10import atexit 11 12import gevent 13 14from Config import config 15from Crypt import CryptRsa 16from Site import SiteManager 17import socks 18try: 19 from gevent.coros import RLock 20except: 21 from gevent.lock import RLock 22from util import helper 23from Debug import Debug 24from Plugin import PluginManager 25 26 27@PluginManager.acceptPlugins 28class TorManager(object): 29 def __init__(self, fileserver_ip=None, fileserver_port=None): 30 self.privatekeys = {} # Onion: Privatekey 31 self.site_onions = {} # Site address: Onion 32 self.tor_exe = "tools/tor/tor.exe" 33 self.has_meek_bridges = os.path.isfile("tools/tor/PluggableTransports/meek-client.exe") 34 self.tor_process = None 35 self.log = logging.getLogger("TorManager") 36 self.start_onions = None 37 self.conn = None 38 self.lock = RLock() 39 self.starting = True 40 self.connecting = True 41 self.event_started = gevent.event.AsyncResult() 42 43 if config.tor == "disable": 44 self.enabled = False 45 self.start_onions = False 46 self.setStatus("Disabled") 47 else: 48 self.enabled = True 49 self.setStatus("Waiting") 50 51 if fileserver_port: 52 self.fileserver_port = fileserver_port 53 else: 54 self.fileserver_port = config.fileserver_port 55 56 self.ip, self.port = config.tor_controller.rsplit(":", 1) 57 self.port = int(self.port) 58 59 self.proxy_ip, self.proxy_port = config.tor_proxy.rsplit(":", 1) 60 self.proxy_port = int(self.proxy_port) 61 62 def start(self): 63 self.log.debug("Starting (Tor: %s)" % config.tor) 64 self.starting = True 65 try: 66 if not self.connect(): 67 raise Exception("No connection") 68 self.log.debug("Tor proxy port %s check ok" % config.tor_proxy) 69 except Exception as err: 70 if sys.platform.startswith("win") and os.path.isfile(self.tor_exe): 71 self.log.info("Starting self-bundled Tor, due to Tor proxy port %s check error: %s" % (config.tor_proxy, err)) 72 # Change to self-bundled Tor ports 73 self.port = 49051 74 self.proxy_port = 49050 75 if config.tor == "always": 76 socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, "127.0.0.1", self.proxy_port) 77 self.enabled = True 78 if not self.connect(): 79 self.startTor() 80 else: 81 self.log.info("Disabling Tor, because error while accessing Tor proxy at port %s: %s" % (config.tor_proxy, err)) 82 self.enabled = False 83 84 def setStatus(self, status): 85 self.status = status 86 if "main" in sys.modules: # import main has side-effects, breaks tests 87 import main 88 if "ui_server" in dir(main): 89 main.ui_server.updateWebsocket() 90 91 def startTor(self): 92 if sys.platform.startswith("win"): 93 try: 94 self.log.info("Starting Tor client %s..." % self.tor_exe) 95 tor_dir = os.path.dirname(self.tor_exe) 96 startupinfo = subprocess.STARTUPINFO() 97 startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 98 cmd = r"%s -f torrc --defaults-torrc torrc-defaults --ignore-missing-torrc" % self.tor_exe 99 if config.tor_use_bridges: 100 cmd += " --UseBridges 1" 101 102 self.tor_process = subprocess.Popen(cmd, cwd=tor_dir, close_fds=True, startupinfo=startupinfo) 103 for wait in range(1, 3): # Wait for startup 104 time.sleep(wait * 0.5) 105 self.enabled = True 106 if self.connect(): 107 if self.isSubprocessRunning(): 108 self.request("TAKEOWNERSHIP") # Shut down Tor client when controll connection closed 109 break 110 # Terminate on exit 111 atexit.register(self.stopTor) 112 except Exception as err: 113 self.log.error("Error starting Tor client: %s" % Debug.formatException(str(err))) 114 self.enabled = False 115 self.starting = False 116 self.event_started.set(False) 117 return False 118 119 def isSubprocessRunning(self): 120 return self.tor_process and self.tor_process.pid and self.tor_process.poll() is None 121 122 def stopTor(self): 123 self.log.debug("Stopping...") 124 try: 125 if self.isSubprocessRunning(): 126 self.request("SIGNAL SHUTDOWN") 127 except Exception as err: 128 self.log.error("Error stopping Tor: %s" % err) 129 130 def connect(self): 131 if not self.enabled: 132 return False 133 self.site_onions = {} 134 self.privatekeys = {} 135 136 return self.connectController() 137 138 def connectController(self): 139 if "socket_noproxy" in dir(socket): # Socket proxy-patched, use non-proxy one 140 conn = socket.socket_noproxy(socket.AF_INET, socket.SOCK_STREAM) 141 else: 142 conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 143 144 self.log.debug("Connecting to Tor Controller %s:%s" % (self.ip, self.port)) 145 self.connecting = True 146 try: 147 with self.lock: 148 conn.connect((self.ip, self.port)) 149 150 # Auth cookie file 151 res_protocol = self.send("PROTOCOLINFO", conn) 152 cookie_match = re.search('COOKIEFILE="(.*?)"', res_protocol) 153 154 if config.tor_password: 155 res_auth = self.send('AUTHENTICATE "%s"' % config.tor_password, conn) 156 elif cookie_match: 157 cookie_file = cookie_match.group(1).encode("ascii").decode("unicode_escape") 158 auth_hex = binascii.b2a_hex(open(cookie_file, "rb").read()) 159 res_auth = self.send("AUTHENTICATE %s" % auth_hex.decode("utf8"), conn) 160 else: 161 res_auth = self.send("AUTHENTICATE", conn) 162 163 if "250 OK" not in res_auth: 164 raise Exception("Authenticate error %s" % res_auth) 165 166 # Version 0.2.7.5 required because ADD_ONION support 167 res_version = self.send("GETINFO version", conn) 168 version = re.search(r'version=([0-9\.]+)', res_version).group(1) 169 if float(version.replace(".", "0", 2)) < 207.5: 170 raise Exception("Tor version >=0.2.7.5 required, found: %s" % version) 171 172 self.setStatus("Connected (%s)" % res_auth) 173 self.event_started.set(True) 174 self.starting = False 175 self.connecting = False 176 self.conn = conn 177 except Exception as err: 178 self.conn = None 179 self.setStatus("Error (%s)" % str(err)) 180 self.log.warning("Tor controller connect error: %s" % Debug.formatException(str(err))) 181 self.enabled = False 182 return self.conn 183 184 def disconnect(self): 185 if self.conn: 186 self.conn.close() 187 self.conn = None 188 189 def startOnions(self): 190 if self.enabled: 191 self.log.debug("Start onions") 192 self.start_onions = True 193 self.getOnion("global") 194 195 # Get new exit node ip 196 def resetCircuits(self): 197 res = self.request("SIGNAL NEWNYM") 198 if "250 OK" not in res: 199 self.setStatus("Reset circuits error (%s)" % res) 200 self.log.error("Tor reset circuits error: %s" % res) 201 202 def addOnion(self): 203 if len(self.privatekeys) >= config.tor_hs_limit: 204 return random.choice([key for key in list(self.privatekeys.keys()) if key != self.site_onions.get("global")]) 205 206 result = self.makeOnionAndKey() 207 if result: 208 onion_address, onion_privatekey = result 209 self.privatekeys[onion_address] = onion_privatekey 210 self.setStatus("OK (%s onions running)" % len(self.privatekeys)) 211 SiteManager.peer_blacklist.append((onion_address + ".onion", self.fileserver_port)) 212 return onion_address 213 else: 214 return False 215 216 def makeOnionAndKey(self): 217 res = self.request("ADD_ONION NEW:RSA1024 port=%s" % self.fileserver_port) 218 match = re.search("ServiceID=([A-Za-z0-9]+).*PrivateKey=RSA1024:(.*?)[\r\n]", res, re.DOTALL) 219 if match: 220 onion_address, onion_privatekey = match.groups() 221 return (onion_address, onion_privatekey) 222 else: 223 self.setStatus("AddOnion error (%s)" % res) 224 self.log.error("Tor addOnion error: %s" % res) 225 return False 226 227 def delOnion(self, address): 228 res = self.request("DEL_ONION %s" % address) 229 if "250 OK" in res: 230 del self.privatekeys[address] 231 self.setStatus("OK (%s onion running)" % len(self.privatekeys)) 232 return True 233 else: 234 self.setStatus("DelOnion error (%s)" % res) 235 self.log.error("Tor delOnion error: %s" % res) 236 self.disconnect() 237 return False 238 239 def request(self, cmd): 240 with self.lock: 241 if not self.enabled: 242 return False 243 if not self.conn: 244 if not self.connect(): 245 return "" 246 return self.send(cmd) 247 248 def send(self, cmd, conn=None): 249 if not conn: 250 conn = self.conn 251 self.log.debug("> %s" % cmd) 252 back = "" 253 for retry in range(2): 254 try: 255 conn.sendall(b"%s\r\n" % cmd.encode("utf8")) 256 while not back.endswith("250 OK\r\n"): 257 back += conn.recv(1024 * 64).decode("utf8") 258 break 259 except Exception as err: 260 self.log.error("Tor send error: %s, reconnecting..." % err) 261 if not self.connecting: 262 self.disconnect() 263 time.sleep(1) 264 self.connect() 265 back = None 266 if back: 267 self.log.debug("< %s" % back.strip()) 268 return back 269 270 def getPrivatekey(self, address): 271 return self.privatekeys[address] 272 273 def getPublickey(self, address): 274 return CryptRsa.privatekeyToPublickey(self.privatekeys[address]) 275 276 def getOnion(self, site_address): 277 if not self.enabled: 278 return None 279 280 if config.tor == "always": # Different onion for every site 281 onion = self.site_onions.get(site_address) 282 else: # Same onion for every site 283 onion = self.site_onions.get("global") 284 site_address = "global" 285 286 if not onion: 287 with self.lock: 288 self.site_onions[site_address] = self.addOnion() 289 onion = self.site_onions[site_address] 290 self.log.debug("Created new hidden service for %s: %s" % (site_address, onion)) 291 292 return onion 293 294 # Creates and returns a 295 # socket that has connected to the Tor Network 296 def createSocket(self, onion, port): 297 if not self.enabled: 298 return False 299 self.log.debug("Creating new Tor socket to %s:%s" % (onion, port)) 300 if self.starting: 301 self.log.debug("Waiting for startup...") 302 self.event_started.get() 303 if config.tor == "always": # Every socket is proxied by default, in this mode 304 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 305 else: 306 sock = socks.socksocket() 307 sock.set_proxy(socks.SOCKS5, self.proxy_ip, self.proxy_port) 308 return sock 309