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