openmedialibrary/oml/tor.py
2024-06-10 15:01:46 +01:00

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.top_dir, 'platform_darwin64', 'tor', 'tor'),
'/Applications/TorBrowser.app/TorBrowser/Tor/tor',
os.path.join(settings.top_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.top_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.top_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.top_dir, 'platform_linux64', 'tor', 'tor'),
os.path.join(settings.top_dir, 'platform_linux32', 'tor', 'tor'),
os.path.join(settings.top_dir, 'platform_linux_armv7l', 'tor', 'tor'),
os.path.join(settings.top_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.top_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.top_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.top_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)