openmedialibrary/oml/tor.py
2016-02-01 12:14:31 +05:30

365 lines
13 KiB
Python

import os
import re
import sys
import subprocess
import time
from threading import Thread
import distutils
import zipfile
import ox
import stem
from stem.control import Controller
from Crypto.PublicKey import RSA
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
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 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 'TorBrowser' in tor and sys.platform.startswith('linux'):
env = {
'LD_LIBRARY_PATH': os.path.dirname(tor)
}
else:
env = None
cmd = [tor, '--defaults-torrc', defaults, '-f', torrc, 'DataDirectory', tor_data]
cmd += self.geoip(tor)
while self.running:
self.p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1,
universal_newlines=True, start_new_session=True, env=env)
for line in self.p.stdout:
self._status.append(line)
logger.debug(line)
time.sleep(0.5)
self.p = None
def geoip(self, tor):
geo = []
tordir = os.path.dirname(os.path.dirname(tor))
gepipfile = os.path.join(tordir, 'Data', 'Tor', 'geoip')
gepipv6file = os.path.join(tordir, 'Data', 'Tor', 'geoip6')
if os.path.exists(gepipfile):
geo += ['GeoIPFile', gepipfile]
if os.path.exists(gepipv6file):
geo += ['GeoIPv6File', gepipv6file]
return geo
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()
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
self.socks_port = int(self.controller.get_conf('SocksPort').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):
print('EVENT', event)
def shutdown(self):
self._shutdown = True
try:
self.depublish()
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):
logger.debug("publish tor node")
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']}
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'])
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'])
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
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]
url = base_url + current
language = '.*?en'
if sys_platform.startswith('linux'):
import platform
if platform.architecture()[0] == '64bit':
osname = 'linux64'
else:
osname = 'linux32'
ext = 'xz'
elif sys_platform == 'darwin':
osname = 'osx64'
ext = 'dmg'
elif sys_platform == 'win32':
language = ''
osname = ''
ext = 'zip'
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))
torbrowser = sorted(r.findall(read_url(url).decode()))[-1]
url += torbrowser
return url
def get_tor():
if sys.platform == 'darwin':
for path in (
'/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 = []
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)'),
):
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 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
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 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)