You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
docker-tor-hidden-service/onions/Onions.py

519 lines
18 KiB
Python

#!/usr/bin/env python3
7 years ago
import argparse
import logging
import os
import socket
import subprocess
7 years ago
import sys
from base64 import b64decode
from json import dumps
from re import match
from IPy import IP
from jinja2 import Environment
from jinja2 import FileSystemLoader
7 years ago
from pyentrypoint import DockerLinks
from pyentrypoint.config import envtobool
from pyentrypoint.configparser import ConfigParser
7 years ago
from .Service import Service
from .Service import ServicesGroup
class Setup(object):
hidden_service_dir = "/var/lib/tor/hidden_service/"
data_directory = "/run/tor/data"
torrc = '/etc/tor/torrc'
torrc_template = '/var/local/tor/torrc.tpl'
enable_control_port = False
control_port = 9051
control_ip_binding = IP('0.0.0.0')
control_hashed_password = None
control_socket = 'unix:/run/tor/tor_control.sock'
enable_vanguards = False
vanguards_template = '/var/local/tor/vanguards.conf.tpl'
vanguards_conf = '/etc/tor/vanguards.conf'
def _add_host(self, host):
if host not in self.setup:
self.setup[host] = {}
def _get_ports(self, host, ports):
self._add_host(host)
if 'ports' not in self.setup[host]:
self.setup[host]['ports'] = {host: []}
if host not in self.setup[host]['ports']:
self.setup[host]['ports'][host] = []
ports_l = [
[
int(v) if not v.startswith('unix:') else v
for v in sp.split(':', 1)
] for sp in ports.split(',')
]
for port in ports_l:
assert len(port) == 2
if port not in self.setup[host]['ports'][host]:
self.setup[host]['ports'][host].append(port)
def _hash_control_port_password(self, password):
self.control_hashed_password = subprocess.check_output([
'tor', '--quiet', '--hash-password', password
]).decode()
def _parse_control_port_variable(self, check_ip=True):
control_port = os.environ['TOR_CONTROL_PORT']
try:
if control_port.startswith('unix:'):
self.control_socket = control_port
return
self.control_socket = None
if ':' in control_port:
host, port = control_port.split(':')
self.control_ip_binding = IP(host) if check_ip else host
self.control_port = int(port)
return
self.control_ip_binding = (
IP(control_port) if check_ip else control_port
)
except BaseException as e:
logging.error('TOR_CONTROL_PORT environment variable error')
logging.exception(e)
def _setup_control_port(self):
if 'TOR_CONTROL_PORT' not in os.environ:
return
self.enable_control_port = True
self._parse_control_port_variable()
if os.environ.get('TOR_CONTROL_PASSWORD'):
self._hash_control_port_password(os.environ[
'TOR_CONTROL_PASSWORD'
])
if envtobool('TOR_DATA_DIRECTORY', False):
self.data_directory = os.environ['TOR_DATA_DIRECTORY']
def _setup_vanguards(self):
if not envtobool('TOR_ENABLE_VANGUARDS', False):
return
self.enable_control_port = True
self.enable_vanguards = True
os.environ.setdefault('TOR_CONTROL_PORT', self.control_socket)
self.kill_tor_on_vanguard_exit = envtobool(
'VANGUARD_KILL_TOR_ON_EXIT',
True
)
self.vanguards_state_file = os.path.join(
self.data_directory,
'vanguards.state'
)
def _get_key(self, host, key):
self._add_host(host)
assert len(key) > 800
self.setup[host]['key'] = key
def _load_keys_in_services(self):
for service in self.services:
service.load_key()
def _get_service(self, host, service):
self._add_host(host)
self.setup[host]['service'] = service
def find_group_by_service(self, service):
for group in self.services:
if service in group.services:
return group
def find_group_by_name(self, name):
for group in self.services:
if name == group.name:
return group
def find_service_by_host(self, host):
for group in self.services:
service = group.get_service_by_host(host)
if service:
return service
def add_empty_group(self, name, version=None):
if self.find_group_by_name(name):
raise Exception('Group {name} already exists'.format(name=name))
group = ServicesGroup(name=name, version=version)
self.services.append(group)
return group
def add_new_service(self,
host,
name=None,
ports=None,
key=None):
group = self.find_group_by_name(name)
if group:
service = group.get_service_by_host(host)
else:
service = self.find_service_by_host(host)
if not service:
service = Service(host=host)
if not group:
group = ServicesGroup(
service=service,
name=name,
hidden_service_dir=self.hidden_service_dir,
)
else:
group.add_service(service)
if group not in self.services:
self.services.append(group)
elif group and service not in group.services:
group.add_service(service)
else:
self.find_group_by_service(service)
if key:
group.add_key(key)
if ports:
service.add_ports(ports)
return service
def _set_service_names(self):
'Create groups for services, should be run first'
reg = r'([A-Z0-9]*)_SERVICE_NAME'
for key, val in os.environ.items():
m = match(reg, key)
if m:
try:
self.add_new_service(host=m.groups()[0].lower(), name=val)
except BaseException as e:
logging.error(f"Fail to setup from {key} environment")
logging.error(e)
def _set_ports(self, host, ports):
self.add_new_service(host=host, ports=ports)
def _set_key(self, host, key):
self.add_new_service(host=host, key=key.encode())
def _setup_from_env(self, match_map):
for reg, call in match_map:
for key, val in os.environ.items():
m = match(reg, key)
if m:
try:
call(m.groups()[0].lower(), val)
except BaseException as e:
logging.error(f"Fail to setup from {key} environment")
logging.error(e)
def _setup_keys_and_ports_from_env(self):
self._setup_from_env(
(
(r'([A-Z0-9]+)_PORTS', self._set_ports),
(r'([A-Z0-9]+)_KEY', self._set_key),
)
)
def get_or_create_empty_group(self, name, version=None):
group = self.find_group_by_name(name)
if group:
if version:
group.set_version(version)
return group
return self.add_empty_group(name, version)
def _set_group_version(self, name, version):
'Setup groups with version'
group = self.get_or_create_empty_group(name, version=version)
group.set_version(version)
def _set_group_key(self, name, key):
'Set key for service group'
group = self.get_or_create_empty_group(name)
if group.version == 3:
group.add_key(b64decode(key))
else:
group.add_key(key)
def _set_group_hosts(self, name, hosts):
'Set services for service groups'
self.get_or_create_empty_group(name)
for host_map in hosts.split(','):
host_map = host_map.strip()
port_from, host, port_dest = host_map.split(':', 2)
if host == 'unix' and port_dest.startswith('/'):
self.add_new_service(host=name, name=name, ports=host_map)
else:
ports = '{frm}:{dst}'.format(frm=port_from, dst=port_dest)
self.add_new_service(host=host, name=name, ports=ports)
def _setup_services_from_env(self):
self._setup_from_env(
(
(r'([A-Z0-9]+)_TOR_SERVICE_VERSION', self._set_group_version),
(r'([A-Z0-9]+)_TOR_SERVICE_KEY', self._set_group_key),
(r'([A-Z0-9]+)_TOR_SERVICE_HOSTS', self._set_group_hosts),
)
)
def _get_setup_from_env(self):
self._set_service_names()
self._setup_keys_and_ports_from_env()
self._setup_services_from_env()
def _get_setup_from_links(self):
containers = DockerLinks().to_containers()
if not containers:
return
for container in containers:
host = container.names[0]
self.add_new_service(host=host)
for link in container.links:
if link.protocol != 'tcp':
continue
port_map = os.environ.get('PORT_MAP')
self._set_ports(host, '{exposed}:{internal}'.format(
exposed=port_map or link.port,
internal=link.port,
))
def apply_conf(self):
self._write_keys()
self._write_torrc()
if self.enable_vanguards:
self._write_vanguards_conf()
def _write_keys(self):
for service in self.services:
service.write_key()
def _write_torrc(self):
env = Environment(loader=FileSystemLoader('/'))
temp = env.get_template(self.torrc_template)
with open(self.torrc, mode='w') as f:
f.write(temp.render(onion=self,
env=os.environ,
envtobool=envtobool,
type=type,
int=int))
def _write_vanguards_conf(self):
env = Environment(loader=FileSystemLoader('/'))
temp = env.get_template(self.vanguards_template)
with open(self.vanguards_conf, mode='w') as f:
f.write(temp.render(env=os.environ,
ConfigParser=ConfigParser,
envtobool=envtobool))
def run_vanguards(self):
self._setup_vanguards()
if not self.enable_vanguards:
return
logging.info('Vanguard enabled, starting...')
if not self.kill_tor_on_vanguard_exit:
os.execvp('vanguards', ['vanguards'])
try:
subprocess.check_call('vanguards')
except subprocess.CalledProcessError as e:
logging.error(str(e))
finally:
logging.error('Vanguards has exited, killing tor...')
os.kill(1, 2)
def resolve_control_hostname(self):
try:
addr = socket.getaddrinfo(self.control_ip_binding,
None,
socket.AF_INET,
socket.SOCK_STREAM,
socket.IPPROTO_TCP)
except socket.gaierror:
raise
return IP(addr[0][4][0])
def resolve_control_port(self):
if 'TOR_CONTROL_PORT' not in os.environ:
return
self._parse_control_port_variable(check_ip=False)
if self.control_socket:
print(os.environ['TOR_CONTROL_PORT'])
try:
ip = IP(self.control_ip_binding)
except ValueError:
ip = self.resolve_control_hostname()
print(f"{ip}:{self.control_port}")
def setup_hosts(self):
self.setup = {}
7 years ago
self._get_setup_from_env()
self._get_setup_from_links()
self._load_keys_in_services()
7 years ago
self.check_services()
self._setup_vanguards()
self._setup_control_port()
7 years ago
self.apply_conf()
def check_services(self):
to_remove = set()
for group in self.services:
try:
for service in group.services:
if not service.ports:
raise Exception(
'Service {name} has not ports set'.format(
name=service.host
)
)
if len(group.services) > 1 and [
True for p in service.ports if p.is_socket
]:
raise Exception(
f'Cannot use socket and ports '
f'in the same {service.host}'
)
if len(set(dict(group)['urls'])) != len(dict(group)['urls']):
raise Exception(
f'Same port for multiple services in '
f'{group.name} group'
)
except Exception as e:
logging.error(e)
to_remove.add(group)
for group in to_remove:
self.services.remove(group)
class Onions(Setup):
"""Onions"""
def __init__(self):
self.services = []
if 'HIDDEN_SERVICE_DIR' in os.environ:
self.hidden_service_dir = os.environ['HIDDEN_SERVICE_DIR']
if os.environ.get('TOR_DATA_DIRECTORY'):
self.data_directory = os.environ['TOR_DATA_DIRECTORY']
def torrc_parser(self):
self.torrc_dict = {}
def parse_dir(line):
_, path = line.split()
group_name = os.path.basename(path)
self.torrc_dict[group_name] = {
'services': [],
}
return group_name
def parse_port(line, name):
_, port_from, dest = line.split()
service_host, port = dest.split(':')
ports_str = '{port_from}:{dest}'
ports_param = ports_str.format(port_from=port_from,
dest=port)
if port.startswith('/'):
service_host = name
ports_param = ports_str.format(port_from=port_from,
dest=dest)
self.torrc_dict[name]['services'].append({
'host': service_host,
'ports': ports_param,
})
def parse_version(line, name):
_, version = line.split()
self.torrc_dict[name]['version'] = int(version)
def setup_services():
for name, setup in self.torrc_dict.items():
version = setup.get('version', 2)
group = (self.find_group_by_name(name)
or self.add_empty_group(name, version=version))
for service_dict in setup.get('services', []):
host = service_dict['host']
service = (group.get_service_by_host(host)
or Service(host))
service.add_ports(service_dict['ports'])
if service not in group.services:
group.add_service(service)
self._load_keys_in_services()
if not os.path.exists(self.torrc):
return
try:
with open(self.torrc, 'r') as f:
for line in f.readlines():
if line.startswith('HiddenServiceDir'):
name = parse_dir(line)
if line.startswith('HiddenServicePort'):
parse_port(line, name)
if line.startswith('HiddenServiceVersion'):
parse_version(line, name)
except BaseException:
raise Exception(
'Fail to parse torrc file. Please check the file'
)
setup_services()
def __str__(self):
if not self.services:
return 'No onion site'
return '\n'.join([str(service) for service in self.services])
def to_json(self):
service_lst = [dict(service) for service in self.services]
return dumps({
service['name']: service['urls'] for service in service_lst
})
def main():
logging.basicConfig()
parser = argparse.ArgumentParser(description='Display onion sites',
prog='onions')
parser.add_argument('--json', dest='json', action='store_true',
help='serialize to json')
parser.add_argument('--setup-hosts', dest='setup', action='store_true',
help='Setup hosts')
parser.add_argument('--run-vanguards', dest='vanguards',
action='store_true',
help='Run Vanguards in tor container')
parser.add_argument('--resolve-control-port', dest='resolve_control_port',
action='store_true',
help='Resolve ip from host if needed')
args = parser.parse_args()
logging.getLogger().setLevel(logging.WARNING)
7 years ago
try:
onions = Onions()
if args.setup:
onions.setup_hosts()
else:
onions.torrc_parser()
if args.vanguards:
onions.run_vanguards()
return
if args.resolve_control_port:
onions.resolve_control_port()
return
7 years ago
except BaseException as e:
logging.exception(e)
7 years ago
error_msg = str(e)
else:
error_msg = None
if args.json:
7 years ago
if error_msg:
print(dumps({'error': error_msg}))
sys.exit(1)
print(onions.to_json())
else:
7 years ago
if error_msg:
logging.error(error_msg)
sys.exit(1)
print(onions)
if __name__ == '__main__':
main()