432 lines
16 KiB
Python
432 lines
16 KiB
Python
from threading import Thread
|
|
import distutils
|
|
import os
|
|
import platform
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import zipfile
|
|
|
|
from Crypto.PublicKey import RSA
|
|
import ox
|
|
import stem
|
|
from stem import Signal
|
|
from stem.control import Controller
|
|
|
|
import settings
|
|
import state
|
|
import utils
|
|
|
|
import logging
|
|
logging.getLogger('stem').setLevel(logging.ERROR)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TorDaemon(Thread):
|
|
installing = False
|
|
running = True
|
|
ended = False
|
|
p = None
|
|
|
|
def __init__(self):
|
|
self._status = []
|
|
Thread.__init__(self)
|
|
self.daemon = True
|
|
self.start()
|
|
|
|
def create_torrc(self):
|
|
defaults = os.path.join(settings.data_path, 'torrc-defaults')
|
|
torrc = os.path.join(settings.data_path, 'torrc')
|
|
if not os.path.exists(defaults):
|
|
with open(defaults, 'w') as fd:
|
|
fd.write('''
|
|
AvoidDiskWrites 1
|
|
# Where to send logging messages. Format is minSeverity[-maxSeverity]
|
|
# (stderr|stdout|syslog|file FILENAME).
|
|
Log notice stdout
|
|
SocksPort 9830
|
|
ControlPort 9831
|
|
#CookieAuthentication 1
|
|
'''.strip())
|
|
tor_data = os.path.join(settings.data_path, 'TorData')
|
|
if sys.platform == 'win32':
|
|
tor_data = os.path.normpath(tor_data).replace('\\', '/')
|
|
if not os.path.exists(torrc):
|
|
with open(torrc, 'w') as fd:
|
|
fd.write('''
|
|
DataDirectory {tor_data}
|
|
DirReqStatistics 0
|
|
'''.strip().format(tor_data=tor_data))
|
|
else:
|
|
with open(torrc, 'r') as fd:
|
|
data = fd.read()
|
|
modified_data = re.sub('DataDirectory.*?TorData',
|
|
'DataDirectory {tor_data}'.format(tor_data=tor_data), data)
|
|
if data != modified_data:
|
|
with open(torrc, 'w') as fd:
|
|
fd.write(modified_data)
|
|
|
|
return defaults, torrc
|
|
|
|
def run(self):
|
|
defaults, torrc = self.create_torrc()
|
|
tor_data = os.path.join(settings.data_path, 'TorData')
|
|
tor = get_tor()
|
|
if not tor:
|
|
self._status.append('No tor binary found. Please install TorBrowser or tor')
|
|
self.installing = True
|
|
install_tor()
|
|
self.installing = False
|
|
tor = get_tor()
|
|
if tor:
|
|
if sys.platform.startswith('linux') and (
|
|
'TorBrowser' in tor or tor.startswith(os.path.dirname(settings.base_dir))
|
|
):
|
|
env = {
|
|
'LD_LIBRARY_PATH': os.path.dirname(tor)
|
|
}
|
|
else:
|
|
env = None
|
|
cmd = [tor, '--defaults-torrc', defaults, '-f', torrc, 'DataDirectory', tor_data]
|
|
cmd += get_geoip(tor)
|
|
cwd = os.path.dirname(tor)
|
|
while self.running:
|
|
if sys.platform == 'win32':
|
|
cwd = settings.data_path
|
|
startupinfo = subprocess.STARTUPINFO()
|
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
startupinfo.wShowWindow = subprocess.SW_HIDE
|
|
self.p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1,
|
|
universal_newlines=True, start_new_session=True, env=env, cwd=cwd, startupinfo=startupinfo)
|
|
else:
|
|
self.p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1,
|
|
universal_newlines=True, start_new_session=True, env=env, cwd=cwd)
|
|
for line in self.p.stdout:
|
|
self._status.append(line)
|
|
logger.debug(line)
|
|
self.p.communicate()
|
|
time.sleep(0.5)
|
|
self.ended = True
|
|
self.running = False
|
|
self.p = None
|
|
|
|
def kill(self):
|
|
self.running = False
|
|
if self.p:
|
|
self.p.kill()
|
|
|
|
def status(self, max_lines=50):
|
|
return ''.join(self._status[-max_lines:])
|
|
|
|
class Tor(object):
|
|
_shutdown = False
|
|
connected = False
|
|
controller = None
|
|
daemon = None
|
|
socks_port = 9150
|
|
|
|
def __init__(self):
|
|
if not self.connect():
|
|
self.reconnect()
|
|
|
|
def connect(self):
|
|
self.connected = False
|
|
self.dir = os.path.join(settings.data_path, 'tor')
|
|
connected = False
|
|
for port in (9831, 9151):
|
|
try:
|
|
self.controller = Controller.from_port('127.0.0.1', port)
|
|
connected = True
|
|
break
|
|
except stem.SocketError:
|
|
pass
|
|
if not connected:
|
|
if not self.daemon:
|
|
logger.debug("Start tor")
|
|
self.daemon = TorDaemon()
|
|
return self.connect()
|
|
elif self.daemon.ended:
|
|
logger.debug("Try starting tor again")
|
|
self.daemon = TorDaemon()
|
|
return self.connect()
|
|
if not self.daemon.installing:
|
|
logger.debug("Failed to connect to tor")
|
|
return False
|
|
try:
|
|
self.controller.authenticate()
|
|
except stem.connection.MissingPassword:
|
|
logger.debug("TOR requires a password")
|
|
return False
|
|
except stem.connection.PasswordAuthFailed:
|
|
logger.debug("invalid tor password")
|
|
return False
|
|
self.controller.add_event_listener(self.event_listener)
|
|
self.controller.add_status_listener(self.status_listener)
|
|
self.connected = True
|
|
port = self.controller.get_conf('SocksPort')
|
|
if port:
|
|
self.socks_port = int(port.split(' ')[0])
|
|
self.publish()
|
|
state.online = self.is_online()
|
|
return True
|
|
|
|
def reconnect(self):
|
|
if not self.connect():
|
|
if state.main:
|
|
state.main.call_later(1, self.reconnect)
|
|
|
|
def status_listener(self, controller, status, timestamp):
|
|
if status == 'Closed':
|
|
if not self._shutdown:
|
|
self.connected = False
|
|
state.online = False
|
|
self.reconnect()
|
|
else:
|
|
logger.debug('unknonw change %s', status)
|
|
|
|
def event_listener(self, event):
|
|
logger.debug('EVENT', event)
|
|
|
|
def shutdown(self):
|
|
self._shutdown = True
|
|
try:
|
|
self.depublish()
|
|
if self.daemon and self.controller:
|
|
self.controller.signal(Signal.SHUTDOWN)
|
|
if self.controller:
|
|
#self.controller.remove_event_listener(self.connection_change)
|
|
self.controller.close()
|
|
if self.daemon:
|
|
self.daemon.kill()
|
|
except:
|
|
logger.debug('shutdown exception', exc_info=True)
|
|
pass
|
|
self.connected = False
|
|
|
|
def publish(self):
|
|
if not self.connected:
|
|
return False
|
|
controller = self.controller
|
|
if controller.get_version() >= stem.version.Requirement.ADD_ONION:
|
|
private_key, public_key = utils.load_pem_key(settings.ca_key_path)
|
|
key_type, key_content = utils.get_onion_key(private_key)
|
|
ports = {9851: settings.server['node_port']}
|
|
if settings.preferences.get('enableReadOnlyService'):
|
|
ports[80] = settings.server['public_port']
|
|
controller.remove_ephemeral_hidden_service(settings.USER_ID)
|
|
response = controller.create_ephemeral_hidden_service(
|
|
ports,
|
|
key_type=key_type, key_content=key_content,
|
|
detached=True
|
|
)
|
|
if response.is_ok():
|
|
if response.service_id != settings.USER_ID:
|
|
logger.error("Something is wrong with tor id %s vs %s", response.service_id, settings.USER_ID)
|
|
logger.debug('published node as https://%s.onion:%s',
|
|
settings.USER_ID, settings.server_defaults['node_port'])
|
|
if settings.preferences.get('enableReadOnlyService'):
|
|
logger.debug('published readonly version as hidden servers: http://%s.onion', settings.USER_ID)
|
|
else:
|
|
logger.debug('failed to publish node to tor')
|
|
else:
|
|
controller.remove_hidden_service(self.dir)
|
|
result = controller.create_hidden_service(
|
|
self.dir,
|
|
settings.server_defaults['node_port'],
|
|
target_port=settings.server['node_port']
|
|
)
|
|
logger.debug('published node as https://%s:%s', result.hostname, settings.server_defaults['node_port'])
|
|
if settings.preferences.get('enableReadOnlyService'):
|
|
logger.error('can not publish read-only version, please update TOR')
|
|
|
|
|
|
def depublish(self):
|
|
if not self.connected:
|
|
return False
|
|
if self.controller:
|
|
try:
|
|
self.controller.remove_hidden_service(self.dir)
|
|
except:
|
|
logger.debug('self.controller.remove_hidden_service fail', exc_info=True)
|
|
state.online = False
|
|
|
|
def is_online(self):
|
|
return self.connected and self.controller.is_alive() and utils.can_connect_dns()
|
|
|
|
def torbrowser_url(sys_platform=None):
|
|
import re
|
|
from ox.cache import read_url
|
|
if not sys_platform:
|
|
sys_platform = sys.platform
|
|
|
|
if sys_platform.startswith('linux'):
|
|
machine = platform.machine()
|
|
if machine not in ('x86_64', 'i386', 'i686'):
|
|
return
|
|
|
|
base_url = 'https://dist.torproject.org/torbrowser/'
|
|
data = read_url(base_url, timeout=3*24*60*60).decode()
|
|
versions = []
|
|
for r in (
|
|
re.compile('href="(\d+\.\d+\.\d+/)"'),
|
|
re.compile('href="(\d+\.\d+/)"'),
|
|
):
|
|
versions += r.findall(data)
|
|
if not versions:
|
|
return None
|
|
current = sorted(versions)[-1]
|
|
url = base_url + current
|
|
if sys_platform.startswith('linux'):
|
|
if platform.architecture()[0] == '64bit':
|
|
osname = 'linux-x86_64'
|
|
else:
|
|
osname = 'linux-x86_32'
|
|
ext = 'xz'
|
|
elif sys_platform == 'darwin':
|
|
osname = 'macos'
|
|
ext = 'dmg'
|
|
elif sys_platform == 'win32':
|
|
osname = 'windows-x86_64-portable'
|
|
ext = 'exe'
|
|
else:
|
|
logger.debug('no way to get torbrowser url for %s', sys.platform)
|
|
return None
|
|
data = read_url(url).decode()
|
|
r = re.compile('href="(.*?{osname}.*?{ext})"'.format(osname=osname, ext=ext)).findall(data)
|
|
if not r:
|
|
r = re.compile('href="(.*?{ext})"'.format(ext=ext)).findall(data)
|
|
torbrowser = sorted(r)[-1]
|
|
url += torbrowser
|
|
return url
|
|
|
|
def get_tor():
|
|
if sys.platform == 'darwin':
|
|
for path in (
|
|
os.path.join(settings.base_dir, '..', 'platform_darwin64', 'tor', 'tor'),
|
|
'/Applications/TorBrowser.app/TorBrowser/Tor/tor',
|
|
os.path.join(settings.base_dir, '..', 'tor', 'TorBrowser.app', 'TorBrowser', 'Tor', 'tor')
|
|
):
|
|
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
return path
|
|
elif sys.platform == 'win32':
|
|
paths = [
|
|
os.path.join(settings.base_dir, '..', 'platform_win32', 'tor', 'tor.exe')
|
|
]
|
|
for exe in (
|
|
os.path.join('Tor Browser', 'Browser', 'TorBrowser', 'Tor', 'tor.exe'),
|
|
os.path.join('Tor Browser', 'Browser', 'TorBrowser', 'Tor', 'Tor', 'tor.exe'),
|
|
):
|
|
for prefix in (
|
|
os.path.join(os.path.expanduser('~'), 'Desktop'),
|
|
os.path.join('C:', 'Program Files'),
|
|
os.path.join('C:', 'Program Files (x86)'),
|
|
):
|
|
path = os.path.join(prefix, exe)
|
|
paths.append(path)
|
|
paths.append(os.path.join(settings.base_dir, '..', 'tor', 'Tor', 'tor.exe'))
|
|
for path in paths:
|
|
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
return os.path.normpath(path)
|
|
elif sys.platform.startswith('linux'):
|
|
for path in (
|
|
os.path.join(settings.base_dir, '..', 'platform_linux64', 'tor', 'tor'),
|
|
os.path.join(settings.base_dir, '..', 'platform_linux32', 'tor', 'tor'),
|
|
os.path.join(settings.base_dir, '..', 'platform_linux_armv7l', 'tor', 'tor'),
|
|
os.path.join(settings.base_dir, '..', 'platform_linux_aarch64', 'tor', 'tor'),
|
|
):
|
|
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
return os.path.normpath(path)
|
|
start = os.path.expanduser('~/.local/share/applications/start-tor-browser.desktop')
|
|
if os.path.exists(start):
|
|
with open(start) as fd:
|
|
e = [line for line in fd.read().split('\n') if line.startswith('Exec')]
|
|
if e:
|
|
try:
|
|
base = os.path.dirname(e[0].split('"')[1])
|
|
path = os.path.join(base, 'TorBrowser', 'Tor', 'tor')
|
|
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
return path
|
|
path = os.path.join(base, 'TorBrowser', 'Tor', 'Tor', 'tor')
|
|
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
return path
|
|
except:
|
|
pass
|
|
local_tor = os.path.normpath(os.path.join(settings.base_dir, '..',
|
|
'tor', 'tor-browser_en-US', 'Browser', 'TorBrowser', 'Tor', 'tor'))
|
|
if os.path.exists(local_tor):
|
|
return local_tor
|
|
return distutils.spawn.find_executable('tor')
|
|
|
|
def get_geoip(tor):
|
|
geo = []
|
|
for tordir in (
|
|
os.path.normpath(os.path.join(settings.base_dir, '..', 'platform', 'tor')),
|
|
os.path.join(os.path.dirname(os.path.dirname(tor)), 'Data', 'Tor')
|
|
):
|
|
gepipfile = os.path.join(tordir, 'geoip')
|
|
gepipv6file = os.path.join(tordir, 'geoip6')
|
|
if os.path.exists(gepipfile):
|
|
geo += ['GeoIPFile', gepipfile]
|
|
if os.path.exists(gepipv6file):
|
|
geo += ['GeoIPv6File', gepipv6file]
|
|
if geo:
|
|
break
|
|
return geo
|
|
|
|
def install_tor():
|
|
import tarfile
|
|
import update
|
|
import shutil
|
|
# only install if tor can not be found
|
|
if get_tor():
|
|
logger.debug('found existing tor installation')
|
|
return
|
|
url = torbrowser_url()
|
|
target = os.path.normpath(os.path.join(settings.base_dir, '..', 'tor'))
|
|
if url:
|
|
logger.debug('downloading and installing tor')
|
|
if sys.platform.startswith('linux'):
|
|
ox.makedirs(target)
|
|
try:
|
|
tar_file = os.path.join(target, os.path.basename(url))
|
|
update.get(url, tar_file)
|
|
tar = tarfile.open(tar_file)
|
|
tar.extractall(target)
|
|
desktop = os.path.join(target, tar.members[0].path, 'start-tor-browser.desktop')
|
|
tar.close()
|
|
subprocess.call([desktop, '--register-app'], cwd=os.path.dirname(desktop))
|
|
os.unlink(tar_file)
|
|
except:
|
|
logger.debug('tor installation failed', exc_info=True)
|
|
elif sys.platform == 'darwin':
|
|
ox.makedirs(target)
|
|
dmg = os.path.join(target, os.path.basename(url))
|
|
name = 'TorBrowser.app'
|
|
if os.access('/Applications', os.W_OK):
|
|
target = os.path.join('/Applications', name)
|
|
else:
|
|
target = os.path.join(target, name)
|
|
if not os.path.exists(target):
|
|
try:
|
|
update.get(url, dmg)
|
|
r = subprocess.check_output(['hdiutil', 'mount', dmg])
|
|
volume = r.decode().strip().split('\t')[-1]
|
|
app = os.path.join(volume, name)
|
|
shutil.copytree(app, target)
|
|
subprocess.call(['hdiutil', 'unmount', volume])
|
|
os.unlink(dmg)
|
|
except:
|
|
logger.debug('tor installation failed', exc_info=True)
|
|
elif sys.platform == 'win32':
|
|
try:
|
|
ox.makedirs(target)
|
|
zipf = os.path.join(target, os.path.basename(url))
|
|
update.get(url, zipf)
|
|
f = zipfile.ZipFile(zipf)
|
|
f.extractall(target)
|
|
os.unlink(zipf)
|
|
except:
|
|
logger.debug('tor installation failed', exc_info=True)
|
|
else:
|
|
logger.debug('no way to install TorBrowser on %s so far', sys.platform)
|