Forking what is left of ZeroNet and hopefully adding an AT Proto Frontend/Proxy
at main 308 lines 12 kB view raw
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 18from gevent.lock import RLock 19from Debug import Debug 20from Plugin import PluginManager 21 22 23@PluginManager.acceptPlugins 24class TorManager(object): 25 def __init__(self, fileserver_ip=None, fileserver_port=None): 26 self.privatekeys = {} # Onion: Privatekey 27 self.site_onions = {} # Site address: Onion 28 self.tor_exe = "tools/tor/tor.exe" 29 self.has_meek_bridges = os.path.isfile("tools/tor/PluggableTransports/meek-client.exe") 30 self.tor_process = None 31 self.log = logging.getLogger("TorManager") 32 self.start_onions = None 33 self.conn = None 34 self.lock = RLock() 35 self.starting = True 36 self.connecting = True 37 self.status = None 38 self.event_started = gevent.event.AsyncResult() 39 40 if config.tor == "disable": 41 self.enabled = False 42 self.start_onions = False 43 self.setStatus("Disabled") 44 else: 45 self.enabled = True 46 self.setStatus("Waiting") 47 48 if fileserver_port: 49 self.fileserver_port = fileserver_port 50 else: 51 self.fileserver_port = config.fileserver_port 52 53 self.ip, self.port = config.tor_controller.rsplit(":", 1) 54 self.port = int(self.port) 55 56 self.proxy_ip, self.proxy_port = config.tor_proxy.rsplit(":", 1) 57 self.proxy_port = int(self.proxy_port) 58 59 def start(self): 60 self.log.debug("Starting (Tor: %s)" % config.tor) 61 self.starting = True 62 try: 63 if not self.connect(): 64 raise Exception(self.status) 65 self.log.debug("Tor proxy port %s check ok" % config.tor_proxy) 66 except Exception as err: 67 if sys.platform.startswith("win") and os.path.isfile(self.tor_exe): 68 self.log.info("Starting self-bundled Tor, due to Tor proxy port %s check error: %s" % (config.tor_proxy, err)) 69 # Change to self-bundled Tor ports 70 self.port = 49051 71 self.proxy_port = 49050 72 if config.tor == "always": 73 socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, "127.0.0.1", self.proxy_port) 74 self.enabled = True 75 if not self.connect(): 76 self.startTor() 77 else: 78 self.log.info("Disabling Tor, because error while accessing Tor proxy at port %s: %s" % (config.tor_proxy, err)) 79 self.enabled = False 80 81 def setStatus(self, status): 82 self.status = status 83 if "main" in sys.modules: # import main has side-effects, breaks tests 84 import main 85 if "ui_server" in dir(main): 86 main.ui_server.updateWebsocket() 87 88 def startTor(self): 89 if sys.platform.startswith("win"): 90 try: 91 self.log.info("Starting Tor client %s..." % self.tor_exe) 92 tor_dir = os.path.dirname(self.tor_exe) 93 startupinfo = subprocess.STARTUPINFO() 94 startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 95 cmd = r"%s -f torrc --defaults-torrc torrc-defaults --ignore-missing-torrc" % self.tor_exe 96 if config.tor_use_bridges: 97 cmd += " --UseBridges 1" 98 99 self.tor_process = subprocess.Popen(cmd, cwd=tor_dir, close_fds=True, startupinfo=startupinfo) 100 for wait in range(1, 3): # Wait for startup 101 time.sleep(wait * 0.5) 102 self.enabled = True 103 if self.connect(): 104 if self.isSubprocessRunning(): 105 self.request("TAKEOWNERSHIP") # Shut down Tor client when controll connection closed 106 break 107 # Terminate on exit 108 atexit.register(self.stopTor) 109 except Exception as err: 110 self.log.error("Error starting Tor client: %s" % Debug.formatException(str(err))) 111 self.enabled = False 112 self.starting = False 113 self.event_started.set(False) 114 return False 115 116 def isSubprocessRunning(self): 117 return self.tor_process and self.tor_process.pid and self.tor_process.poll() is None 118 119 def stopTor(self): 120 self.log.debug("Stopping...") 121 try: 122 if self.isSubprocessRunning(): 123 self.request("SIGNAL SHUTDOWN") 124 except Exception as err: 125 self.log.error("Error stopping Tor: %s" % err) 126 127 def connect(self): 128 if not self.enabled: 129 return False 130 self.site_onions = {} 131 self.privatekeys = {} 132 133 return self.connectController() 134 135 def connectController(self): 136 if "socket_noproxy" in dir(socket): # Socket proxy-patched, use non-proxy one 137 conn = socket.socket_noproxy(socket.AF_INET, socket.SOCK_STREAM) 138 else: 139 conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 140 141 self.log.debug("Connecting to Tor Controller %s:%s" % (self.ip, self.port)) 142 self.connecting = True 143 try: 144 with self.lock: 145 conn.connect((self.ip, self.port)) 146 147 # Auth cookie file 148 res_protocol = self.send("PROTOCOLINFO", conn) 149 cookie_match = re.search('COOKIEFILE="(.*?)"', res_protocol) 150 151 if config.tor_password: 152 res_auth = self.send('AUTHENTICATE "%s"' % config.tor_password, conn) 153 elif cookie_match: 154 cookie_file = cookie_match.group(1).encode("ascii").decode("unicode_escape") 155 if not os.path.isfile(cookie_file) and self.tor_process: 156 # Workaround for tor client cookie auth file utf8 encoding bug (https://github.com/torproject/stem/issues/57) 157 cookie_file = os.path.dirname(self.tor_exe) + "\\data\\control_auth_cookie" 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