Forking what is left of ZeroNet and hopefully adding an AT Proto Frontend/Proxy
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