openmedialibrary/oml/tor.py

414 lines
15 KiB
Python
Raw Normal View History

2016-06-24 11:18:14 +00:00
from threading import Thread
import distutils
import os
2016-06-24 11:18:14 +00:00
import platform
2016-01-18 07:32:11 +00:00
import re
import subprocess
2016-06-24 11:18:14 +00:00
import sys
import time
import zipfile
2016-06-24 11:18:14 +00:00
from Crypto.PublicKey import RSA
2016-01-15 13:04:07 +00:00
import ox
import stem
2016-02-07 13:52:56 +00:00
from stem import Signal
from stem.control import Controller
2016-06-24 11:18:14 +00:00
import settings
import state
2015-12-02 21:05:23 +00:00
import utils
2015-11-29 14:56:38 +00:00
import logging
2016-01-02 17:03:49 +00:00
logging.getLogger('stem').setLevel(logging.ERROR)
2016-01-05 05:38:36 +00:00
logger = logging.getLogger(__name__)
class TorDaemon(Thread):
installing = False
running = True
2019-01-20 13:07:21 +00:00
p = None
def __init__(self):
self._status = []
Thread.__init__(self)
self.daemon = True
self.start()
def create_torrc(self):
2016-01-18 06:34:20 +00:00
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())
2016-02-01 06:44:31 +00:00
tor_data = os.path.join(settings.data_path, 'TorData')
2019-01-18 11:45:11 +00:00
if sys.platform == 'win32':
2019-01-20 13:07:21 +00:00
tor_data = os.path.normpath(tor_data).replace('\\', '/')
if not os.path.exists(torrc):
with open(torrc, 'w') as fd:
fd.write('''
2016-02-01 06:44:31 +00:00
DataDirectory {tor_data}
DirReqStatistics 0
2016-02-01 06:44:31 +00:00
'''.strip().format(tor_data=tor_data))
2016-01-18 07:32:11 +00:00
else:
with open(torrc, 'r') as fd:
data = fd.read()
2016-02-01 06:44:31 +00:00
modified_data = re.sub('DataDirectory.*?TorData',
'DataDirectory {tor_data}'.format(tor_data=tor_data), data)
2016-01-18 07:32:11 +00:00
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()
2016-02-01 06:44:31 +00:00
tor_data = os.path.join(settings.data_path, 'TorData')
2016-01-10 13:43:10 +00:00
tor = get_tor()
if not tor:
self._status.append('No tor binary found. Please install TorBrowser or tor')
self.installing = True
2016-01-15 13:04:07 +00:00
install_tor()
self.installing = False
2016-01-15 13:04:07 +00:00
tor = get_tor()
if tor:
2016-02-23 07:05:52 +00:00
if sys.platform.startswith('linux') and (
'TorBrowser' in tor or tor.startswith(os.path.dirname(settings.base_dir))
):
2016-01-31 18:33:53 +00:00
env = {
'LD_LIBRARY_PATH': os.path.dirname(tor)
}
else:
env = None
2016-02-01 06:44:31 +00:00
cmd = [tor, '--defaults-torrc', defaults, '-f', torrc, 'DataDirectory', tor_data]
2016-02-23 07:05:52 +00:00
cmd += get_geoip(tor)
2016-02-01 11:09:54 +00:00
cwd = os.path.dirname(tor)
while self.running:
2016-02-07 13:52:56 +00:00
if sys.platform == 'win32':
2016-02-23 11:57:15 +00:00
cwd = settings.data_path
2016-02-07 13:52:56 +00:00
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.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
2016-01-18 06:34:20 +00:00
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()
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
2019-01-21 06:55:48 +00:00
port = self.controller.get_conf('SocksPort')
if port:
self.socks_port = int(port.split(' ')[0])
self.publish()
2015-12-02 21:05:23 +00:00
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):
2016-02-07 13:52:56 +00:00
logger.debug('EVENT', event)
def shutdown(self):
self._shutdown = True
try:
2016-01-02 17:03:49 +00:00
self.depublish()
2016-02-07 13:52:56 +00:00
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:
2016-01-24 09:13:03 +00:00
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:
with open(settings.ssl_key_path, 'rb') as fd:
private_key = fd.read()
key_content = RSA.importKey(private_key).exportKey().decode()
key_content = ''.join(key_content.strip().split('\n')[1:-1])
ports = {9851: settings.server['node_port']}
2019-01-17 10:30:22 +00:00
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='RSA1024', key_content=key_content,
detached=True)
if response.is_ok():
logger.debug('published node as https://%s.onion:%s',
settings.USER_ID, settings.server_defaults['node_port'])
2019-01-17 10:30:22 +00:00
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'])
2019-01-17 10:30:22 +00:00
if settings.preferences.get('enableReadOnlyService'):
logger.error('can not publish read-only version, please update TOR')
2016-01-02 17:03:49 +00:00
def depublish(self):
if not self.connected:
return False
if self.controller:
2016-01-02 09:38:08 +00:00
try:
self.controller.remove_hidden_service(self.dir)
except:
2016-01-24 09:13:03 +00:00
logger.debug('self.controller.remove_hidden_service fail', exc_info=True)
state.online = False
def is_online(self):
2015-12-02 21:05:23 +00:00
return self.connected and self.controller.is_alive() and utils.can_connect_dns()
2016-01-05 05:38:36 +00:00
def torbrowser_url(sys_platform=None):
2016-01-05 05:38:36 +00:00
import re
from ox.cache import read_url
if not sys_platform:
sys_platform = sys.platform
2016-01-05 05:38:36 +00:00
2016-06-24 11:18:14 +00:00
if sys_platform.startswith('linux'):
machine = platform.machine()
if machine not in ('x86_64', 'i386', 'i686'):
return
2016-01-05 05:38:36 +00:00
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)
current = sorted(versions)[-1]
2016-01-05 05:38:36 +00:00
url = base_url + current
language = '.*?en'
if sys_platform.startswith('linux'):
2016-01-10 13:43:10 +00:00
if platform.architecture()[0] == '64bit':
osname = 'linux64'
else:
osname = 'linux32'
2016-01-05 05:38:36 +00:00
ext = 'xz'
elif sys_platform == 'darwin':
2016-01-05 05:38:36 +00:00
osname = 'osx64'
ext = 'dmg'
elif sys_platform == 'win32':
language = ''
osname = ''
ext = 'zip'
2016-01-05 05:38:36 +00:00
else:
logger.debug('no way to get torbrowser url for %s', sys.platform)
return None
r = re.compile('href="(.*?{osname}{language}.*?{ext})"'.format(osname=osname,language=language,ext=ext))
2016-01-05 05:38:36 +00:00
torbrowser = sorted(r.findall(read_url(url).decode()))[-1]
url += torbrowser
return url
2016-01-10 13:43:10 +00:00
def get_tor():
if sys.platform == 'darwin':
for path in (
2016-02-23 07:05:52 +00:00
os.path.join(settings.base_dir, '..', 'platform_darwin64', 'tor', 'tor'),
2016-01-10 13:43:10 +00:00
'/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':
2016-02-23 07:05:52 +00:00
paths = [
os.path.join(settings.base_dir, '..', 'platform_win32', 'tor', 'tor.exe')
]
exe = os.path.join('Tor Browser', 'Browser', 'TorBrowser', '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)'),
2016-01-10 13:43:10 +00:00
):
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:
2016-01-10 13:43:10 +00:00
if os.path.isfile(path) and os.access(path, os.X_OK):
2016-02-23 07:05:52 +00:00
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'),
2016-02-23 07:05:52 +00:00
):
if os.path.isfile(path) and os.access(path, os.X_OK):
return os.path.normpath(path)
2016-01-10 13:43:10 +00:00
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
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
2016-01-10 13:43:10 +00:00
return distutils.spawn.find_executable('tor')
2016-02-23 07:05:52 +00:00
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
2016-01-10 13:43:10 +00:00
def install_tor():
import tarfile
import update
import shutil
# only install if tor can not be found
if get_tor():
2016-01-15 13:04:07 +00:00
logger.debug('found existing tor installation')
return
2016-01-10 13:43:10 +00:00
url = torbrowser_url()
2016-01-12 15:00:51 +00:00
target = os.path.normpath(os.path.join(settings.base_dir, '..', 'tor'))
2016-01-10 13:43:10 +00:00
if url:
2016-01-15 13:04:07 +00:00
logger.debug('downloading and installing tor')
2016-01-10 13:43:10 +00:00
if sys.platform.startswith('linux'):
2016-01-15 13:04:07 +00:00
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:
2016-01-24 09:13:03 +00:00
logger.debug('tor installation failed', exc_info=True)
2016-01-10 13:43:10 +00:00
elif sys.platform == 'darwin':
2016-01-15 13:04:07 +00:00
ox.makedirs(target)
2016-01-10 13:43:10 +00:00
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):
2016-01-15 13:04:07 +00:00
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:
2016-01-24 09:13:03 +00:00
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)
2016-01-10 13:43:10 +00:00
else:
logger.debug('no way to install TorBrowser on %s so far', sys.platform)