Add support for multiple services per address

pull/15/head
Christophe Mehay 7 years ago
parent a939d3620f
commit 27dd14ab33

@ -1 +1,4 @@
keys/
*.egg-info
.tox/
__cache__

107
.gitignore vendored

@ -0,0 +1,107 @@
# Created by https://www.gitignore.io/api/python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# dotenv
.env
# virtualenv
.venv
venv/
ENV/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# End of https://www.gitignore.io/api/python
# more
key/

@ -0,0 +1,8 @@
sudo: false
language: python
python:
- "3.4"
- "3.5"
- "3.6"
install: pip install tox-travis
script: tox

@ -22,13 +22,17 @@ RUN apk add --no-cache git libevent-dev openssl-dev gcc make automake ca-cer
apk del git libevent-dev openssl-dev make automake python3-dev gcc autoconf musl-dev coreutils && \
apk add --no-cache libevent openssl
RUN mkdir -p /etc/tor/
RUN pip install pyentrypoint==0.5.0
# Delete me
RUN pip install 'Jinja2>=2.8' 'pycrypto'
ADD assets/entrypoint-config.yml /
ADD assets/onions /usr/local/src/onions
ADD assets/torrc /var/local/tor/torrc.tpl
RUN mkdir -p /etc/tor/
RUN pip install pyentrypoint==0.5.0
RUN cd /usr/local/src/onions && python3 setup.py install

@ -11,7 +11,6 @@ pre_conf_commands:
- onions --setup-hosts
post_conf_commands:
- timeout -t 3 tor > /dev/null || true
- onions
- chown -R tor:tor $HOME

@ -12,11 +12,9 @@ import argparse
from jinja2 import Environment
from jinja2 import FileSystemLoader
import socket
from .Service import ServicesGroup, Service
from Crypto.PublicKey import RSA
from hashlib import sha1
from base64 import b32encode
import logging
class Setup(object):
@ -25,18 +23,6 @@ class Setup(object):
torrc = '/etc/tor/torrc'
torrc_template = '/var/local/tor/torrc.tpl'
def onion_url_gen(self, key):
"Get onion url from private key"
# Convert private RSA to public DER
priv = RSA.importKey(key.strip())
der = priv.publickey().exportKey("DER")
# hash key, keep first half of sha1, base32 encode
onion = b32encode(sha1(der[22:]).digest()[:10])
return '{onion}.onion'.format(onion=onion.decode().lower())
def _add_host(self, host):
if host not in self.setup:
self.setup[host] = {}
@ -44,7 +30,9 @@ class Setup(object):
def _get_ports(self, host, ports):
self._add_host(host)
if 'ports' not in self.setup[host]:
self.setup[host]['ports'] = []
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
@ -53,18 +41,82 @@ class Setup(object):
]
for port in ports_l:
assert len(port) == 2
if port not in self.setup[host]['ports']:
self.setup[host]['ports'].append(port)
if port not in self.setup[host]['ports'][host]:
self.setup[host]['ports'][host].append(port)
def _get_key(self, host, key):
self._add_host(host)
assert len(key) > 800
self.setup[host]['key'] = key
def _get_setup_from_env(self):
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):
if self.find_group_by_name(name):
raise Exception('Group {name} already exists'.format(name=name))
group = ServicesGroup(name=name)
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)
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)
else:
group = 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:
self.add_new_service(host=m.groups()[0].lower(), name=val)
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)
def _setup_from_env(self):
match_map = (
(r'([A-Z0-9]*)_PORTS', self._get_ports),
(r'([A-Z0-9]*)_KEY', self._get_key),
(r'([A-Z0-9]*)_PORTS', self._set_ports),
(r'([A-Z0-9]*)_KEY', self._set_key),
)
for key, val in os.environ.items():
for reg, call in match_map:
@ -72,39 +124,39 @@ class Setup(object):
if m:
call(m.groups()[0].lower(), val)
def _get_setup_from_env(self):
self._set_service_names()
self._setup_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_host(host)
self.add_new_service(host=host)
for link in container.links:
if link.protocol != 'tcp':
continue
port_map = os.environ.get('PORT_MAP')
self._get_ports(host, '{exposed}:{internal}'.format(
self._set_ports(host, '{exposed}:{internal}'.format(
exposed=port_map or link.port,
internal=link.port,
))
def _set_keys(self):
for link, conf in self.setup.items():
if 'key' in conf:
serv_dir = os.path.join(self.hidden_service_dir, link)
os.makedirs(serv_dir, exist_ok=True)
os.chmod(serv_dir, 0o700)
with open(os.path.join(serv_dir, 'private_key'), 'w') as f:
f.write(conf['key'])
os.fchmod(f.fileno(), 0o600)
with open(os.path.join(serv_dir, 'hostname'), 'w') as f:
f.write(self.onion_url_gen(conf['key']))
def _set_conf(self):
def apply_conf(self):
self._write_keys()
self._write_torrc()
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(setup=self.setup,
f.write(temp.render(services=self.services,
env=os.environ,
type=type,
int=int))
@ -114,58 +166,94 @@ class Setup(object):
try:
self._get_setup_from_env()
self._get_setup_from_links()
self._set_keys()
self._set_conf()
except:
self.check_services()
self.apply_conf()
except BaseException:
raise Exception('Something wrongs with setup')
def check_services(self):
for group in self.services:
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(
'Cannot use socket and ports '
'in the same service'.format(
name=service.host
)
)
if len(set(dict(group)['urls'])) != len(dict(group)['urls']):
raise Exception(
'Same port for multiple services in {name} group'.format(
name=group.name
)
)
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']
def _get_port_from_service(self, service, filename):
with open(filename, 'r') as hostfile:
onion = str(hostfile.read()).strip()
with open(self.torrc, 'r') as torfile:
self.onions[service] = []
for line in torfile.readlines():
find = '# PORT {name}'.format(name=service)
if line.startswith(find):
self.onions[service].append(
'{onion}:{port}'.format(
onion=onion,
port=line[len(find):].strip()
)
)
def get_onions(self):
self.onions = {}
for root, dirs, _ in os.walk(self.hidden_service_dir,
topdown=False):
for service in dirs:
filename = "{root}{service}/hostname".format(
service=service,
root=root
)
self._get_port_from_service(service, filename)
def torrc_parser(self):
def parse_dir(line):
_, path = line.split()
group_name = os.path.basename(path)
group = (self.find_group_by_name(group_name)
or self.add_empty_group(group_name))
return group
def parse_port(line, service_group):
_, port_from, dest = line.split()
service_host, port = dest.split(':')
ports_str = '{port_from}:{dest}'
name = service_host
ports_param = ports_str.format(port_from=port_from,
dest=port)
if port.startswith('/'):
name = service_group.name
ports_param = ports_str.format(port_from=port_from,
dest=dest)
service = (service_group.get_service_by_host(name)
or Service(name))
service.add_ports(ports_param)
if service not in service_group.services:
service_group.add_service(service)
if not os.path.exists(self.torrc):
return
with open(self.torrc, 'r') as f:
for line in f.readlines():
if line.startswith('HiddenServiceDir'):
service_group = parse_dir(line)
if line.startswith('HiddenServicePort'):
parse_port(line, service_group)
def __str__(self):
if not self.onions:
if not self.services:
return 'No onion site'
return '\n'.join(['%s: %s' % (service, ', '.join(onion))
for (service, onion) in self.onions.items()])
return '\n'.join([str(service) for service in self.services])
def to_json(self):
return dumps(self.onions)
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',
@ -179,8 +267,9 @@ def main():
if args.setup:
onions.setup_hosts()
return
onions.get_onions()
onions.torrc_parser()
if args.json:
logging.getLogger().setLevel(logging.ERROR)
print(onions.to_json())
else:
print(onions)

@ -0,0 +1,169 @@
'This class define a service link'
from Crypto.PublicKey import RSA
from hashlib import sha1
from base64 import b32encode
import os
import logging
class ServicesGroup(object):
name = None
_priv_key = None
_key_in_secrets = False
hidden_service_dir = "/var/lib/tor/hidden_service/"
def __init__(self, name=None, service=None, hidden_service_dir=None):
self.hidden_service_dir = hidden_service_dir or self.hidden_service_dir
if not name and not service:
raise Exception(
'Init service group with a name or service at least'
)
self.services = []
self.name = name or service.host
if service:
self.add_service(service)
self.load_key()
if not self._priv_key:
self.gen_key()
def add_service(self, service):
if service not in self.services:
if self.get_service_by_host(service.host):
raise Exception('Duplicate service name')
self.services.append(service)
def get_service_by_host(self, host):
for service in self.services:
if host == service.host:
return service
def add_key(self, key):
if self._key_in_secrets:
logging.warning('Secret key already set, overriding')
self._priv_key = key
self._key_in_secrets = False
def __iter__(self):
yield 'name', self.name
yield 'onion', self.onion_url
yield 'urls', list(self.urls)
def __str__(self):
return '{name}: {urls}'.format(name=self.name,
urls=', '.join(self.urls))
@property
def onion_url(self):
"Get onion url from private key"
# Convert private RSA to public DER
priv = RSA.importKey(self._priv_key.strip())
der = priv.publickey().exportKey("DER")
# hash key, keep first half of sha1, base32 encode
onion = b32encode(sha1(der[22:]).digest()[:10])
return '{onion}.onion'.format(onion=onion.decode().lower())
@property
def urls(self):
for service in self.services:
for ports in service.ports:
yield '{onion}:{port}'.format(onion=self.onion_url,
port=ports.port_from)
def write_key(self, hidden_service_dir=None):
'Write key on disk and set tor service'
if not hidden_service_dir:
hidden_service_dir = self.hidden_service_dir
serv_dir = os.path.join(hidden_service_dir, self.name)
os.makedirs(serv_dir, exist_ok=True)
os.chmod(serv_dir, 0o700)
with open(os.path.join(serv_dir, 'private_key'), 'w') as f:
f.write(self._priv_key)
os.fchmod(f.fileno(), 0o600)
with open(os.path.join(serv_dir, 'hostname'), 'w') as f:
f.write(self.onion_url)
def _load_key(self, key_file):
if os.path.exists(key_file):
with open(key_file, 'r') as f:
key = f.read().encode()
if not len(key):
return
try:
rsa = RSA.importKey(key)
self._priv_key = rsa.exportKey("PEM").decode()
except BaseException:
raise('Fail to load key for {name} services'.format(
name=self.name
))
def load_key(self):
self.load_key_from_secrets()
self.load_key_from_conf()
def load_key_from_secrets(self):
'Load key from docker secret using service name'
secret_file = os.path.join('/run/secrets', self.name)
try:
self._load_key(secret_file)
self._key_in_secrets = True
except BaseException:
logging.warning('Fail to load key from secret, '
'check the key or secret name collision')
def load_key_from_conf(self, hidden_service_dir=None):
'Load key from disk if exists'
if not hidden_service_dir:
hidden_service_dir = self.hidden_service_dir
key_file = os.path.join(hidden_service_dir,
self.name,
'private_key')
self._load_key(key_file)
def gen_key(self):
'Generate new 1024 bits RSA key for hidden service'
self._priv_key = RSA.generate(
bits=1024,
).exportKey("PEM").decode()
class Ports:
port_from = None
dest = None
def __init__(self, port_from, dest):
self.port_from = int(port_from)
self.dest = dest if dest.startswith('unix:') else int(dest)
@property
def is_socket(self):
return self.dest and type(self.dest) is not int
def __iter__(self):
yield 'port_from', str(self.port_from)
yield 'dest', str(self.dest)
yield 'is_socket', self.is_socket
class Service:
def __init__(self, host):
self.host = host
self.ports = []
def add_ports(self, ports):
p = [Ports(*sp.split(':', 1)) for sp in ports.split(',')]
self.ports.extend(p)
def __iter__(self):
yield 'host', self.host
yield 'ports', [dict(p) for p in self.ports]

@ -1 +1,2 @@
from .Onions import Onions, main
from .Service import ServicesGroup, Service, Ports

@ -6,7 +6,7 @@ from setuptools import setup
setup(
name='onions',
version='0.2',
version='0.4',
packages=find_packages(),
@ -31,7 +31,7 @@ setup(
"Topic :: System :: Installation/Setup",
],
install_requires=['pyentrypoint',
install_requires=['pyentrypoint==0.5.0',
'Jinja2>=2.8',
'pycrypto',],

@ -0,0 +1,399 @@
from onions import Onions
import json
import os
import pytest
import re
from Crypto.PublicKey import RSA
from hashlib import sha1
from base64 import b32encode
def get_key_and_onion():
key = '''
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCsMP4gl6g1Q313miPhb1GnDr56ZxIWGsO2PwHM1infkbhlBakR
6DGQfpE31L1ZKTUxY0OexKbW088v8qCOfjD9Zk1i80JP4xzfWQcwFZ5yM/0fkhm3
zLXqXdEahvRthmFsS8OWusRs/04U247ryTm4k5S0Ch5OTBuvMLzQ8W0yDwIDAQAB
AoGAAZr3U5B2ZgC6E7phKUHjbf5KMlPxrDkVqAZQWvuIKmhuYqq518vlYmZ7rhyS
o1kqAMrfH4TP1WLmJJlLe+ibRk2aonR4e0GbW4x151wcJdT1V3vdWAsVSzG3+dqX
PiGT//DIe0OPSH6ecI8ftFRLODd6f5iGkF4gsUSTcVzAFgkCQQDTY67dRpOD9Ozw
oYH48xe0B9NQCw7g4NSH85jPurJXnpn6lZ6bcl8x8ioAdgLyomR7fO/dJFYLw6uV
LZLqZsVbAkEA0Iei3QcpsJnYgcQG7l5I26Sq3LwoiGRDFKRI6k0e+en9JQJgA3Ay
tsLpyCHv9jQ762F6AVXFru5DmZX40F6AXQJBAIHoKac8Xx1h4FaEuo4WPkPZ50ey
dANIx/OAhTFrp3vnMPNpDV60K8JS8vLzkx4vJBcrkXDSirqSFhkIN9grLi8CQEO2
l5MQPWBkRKK2pc2Hfj8cdIMi8kJ/1CyCwE6c5l8etR3sbIMRTtZ76nAbXRFkmsRv
La/7Syrnobngsh/vX90CQB+PSSBqiPSsK2yPz6Gsd6OLCQ9sdy2oRwFTasH8sZyl
bhJ3M9WzP/EMkAzyW8mVs1moFp3hRcfQlZHl6g1U9D8=
-----END RSA PRIVATE KEY-----
'''
onion = b32encode(
sha1(
RSA.importKey(
key.strip()
).publickey().exportKey(
"DER"
)[22:]
).digest()[:10]
).decode().lower() + '.onion'
return key.strip(), onion
def get_torrc_template():
return r'''
{% for service_group in services %}
HiddenServiceDir /var/lib/tor/hidden_service/{{service_group.name}}
{% for service in service_group.services %}
{% for port in service.ports %}
{% if port.is_socket %}
HiddenServicePort {{port.port_from}} {{port.dest}}
{% endif %}
{% if not port.is_socket %}
HiddenServicePort {{port.port_from}} {{service.host}}:{{port.dest}}
{% endif %}
{% endfor %}
{% endfor %}
{% endfor %}
{% if 'RELAY' in env %}
ORPort 9001
{% endif %}
SocksPort 0
# useless line for Jinja bug
'''.strip()
def test_ports(monkeypatch):
env = {
'SERVICE1_PORTS': '80:80',
'SERVICE2_PORTS': '80:80,81:8000',
'SERVICE3_PORTS': '80:unix://unix.socket',
}
monkeypatch.setattr(os, 'environ', env)
onion = Onions()
onion._get_setup_from_env()
assert len(os.environ) == 3
assert len(onion.services) == 3
check = 0
for service_group in onion.services:
assert len(service_group.services) == 1
service = service_group.services[0]
if service.host == 'service1':
check += 1
assert len(service.ports) == 1
assert service.ports[0].port_from == 80
assert service.ports[0].dest == 80
assert not service.ports[0].is_socket
if service.host == 'service2':
check += 3
assert len(service.ports) == 2
assert service.ports[0].port_from == 80
assert service.ports[0].dest == 80
assert service.ports[1].port_from == 81
assert service.ports[1].dest == 8000
if service.host == 'service3':
check += 6
assert len(service.ports) == 1
assert service.ports[0].port_from == 80
assert service.ports[0].dest == 'unix://unix.socket'
assert service.ports[0].is_socket
assert check == 10
def test_docker_links(fs, monkeypatch):
env = {
'HOSTNAME': 'test_env',
'COMPOSE_SERVICE1_1_PORT': 'tcp://172.17.0.2:80',
'COMPOSE_SERVICE1_1_PORT_80_TCP': 'tcp://172.17.0.2:80',
'COMPOSE_SERVICE1_1_PORT_80_TCP_ADDR': '172.17.0.2',
'COMPOSE_SERVICE1_1_PORT_80_TCP_PORT': '80',
'COMPOSE_SERVICE1_1_PORT_80_TCP_PROTO': 'tcp',
'COMPOSE_SERVICE1_1_PORT_8000_TCP': 'tcp://172.17.0.2:8000',
'COMPOSE_SERVICE1_1_PORT_8000_TCP_ADDR': '172.17.0.2',
'COMPOSE_SERVICE1_1_PORT_8000_TCP_PORT': '8000',
'COMPOSE_SERVICE1_1_PORT_8000_TCP_PROTO': 'tcp',
'COMPOSE_SERVICE1_1_NAME': '/compose_env_1/compose_service1_1',
'SERVICE1_PORT': 'tcp://172.17.0.2:80',
'SERVICE1_PORT_80_TCP': 'tcp://172.17.0.2:80',
'SERVICE1_PORT_80_TCP_ADDR': '172.17.0.2',
'SERVICE1_PORT_80_TCP_PORT': '80',
'SERVICE1_PORT_80_TCP_PROTO': 'tcp',
'SERVICE1_PORT_8000_TCP': 'tcp://172.17.0.2:8000',
'SERVICE1_PORT_8000_TCP_ADDR': '172.17.0.2',
'SERVICE1_PORT_8000_TCP_PORT': '8000',
'SERVICE1_PORT_8000_TCP_PROTO': 'tcp',
'SERVICE1_NAME': '/compose_env_1/service1',
'SERVICE1_1_PORT': 'tcp://172.17.0.2:80',
'SERVICE1_1_PORT_80_TCP': 'tcp://172.17.0.2:80',
'SERVICE1_1_PORT_80_TCP_ADDR': '172.17.0.2',
'SERVICE1_1_PORT_80_TCP_PORT': '80',
'SERVICE1_1_PORT_80_TCP_PROTO': 'tcp',
'SERVICE1_1_PORT_8000_TCP': 'tcp://172.17.0.2:8000',
'SERVICE1_1_PORT_8000_TCP_ADDR': '172.17.0.2',
'SERVICE1_1_PORT_8000_TCP_PORT': '8000',
'SERVICE1_1_PORT_8000_TCP_PROTO': 'tcp',
'SERVICE1_1_NAME': '/compose_env_1/service1_1',
}
etc_host = '''
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2 service1 bf447f22cdba compose_service1_1
172.17.0.2 service1_1 bf447f22cdba compose_service1_1
172.17.0.2 compose_service1_1 bf447f22cdba
'''.strip()
fs.CreateFile('/etc/hosts', contents=etc_host)
monkeypatch.setattr(os, 'environ', env)
onion = Onions()
onion._get_setup_from_links()
assert len(onion.services) == 1
group = onion.services[0]
assert len(group.services) == 1
service = group.services[0]
assert len(service.ports) == 2
assert set(
(port.port_from, port.dest) for port in service.ports
) == set([(80, 80), (8000, 8000)])
def test_key(monkeypatch):
key, onion_url = get_key_and_onion()
env = {
'SERVICE1_KEY': key
}
monkeypatch.setattr(os, 'environ', env)
onion = Onions()
onion._get_setup_from_env()
assert len(os.environ) == 1
assert len(onion.services) == 1
assert onion.services[0].onion_url == onion_url
def test_key_in_secret(fs, monkeypatch):
env = {
'SERVICE1_SERVICE_NAME': 'group1',
'SERVICE2_SERVICE_NAME': 'group1',
'SERVICE3_SERVICE_NAME': 'group2',
'SERVICE1_PORTS': '80:80',
'SERVICE2_PORTS': '81:80,82:8000',
'SERVICE3_PORTS': '80:unix://unix.socket',
}
monkeypatch.setattr(os, 'environ', env)
key, onion_url = get_key_and_onion()
fs.CreateFile('/run/secrets/group1', contents=key)
onion = Onions()
onion._get_setup_from_env()
group1 = onion.find_group_by_name('group1')
group2 = onion.find_group_by_name('group2')
# assert group._priv_key == key
assert group1.onion_url == onion_url
assert group2.onion_url != onion_url
def test_configuration(fs, monkeypatch):
env = {
'SERVICE1_SERVICE_NAME': 'group1',
'SERVICE2_SERVICE_NAME': 'group1',
'SERVICE3_SERVICE_NAME': 'group2',
'SERVICE1_PORTS': '80:80',
'SERVICE2_PORTS': '81:80,82:8000',
'SERVICE3_PORTS': '80:unix://unix.socket',
}
monkeypatch.setattr(os, 'environ', env)
monkeypatch.setattr(os, 'fchmod', lambda x, y: None)
key, onion_url = get_key_and_onion()
torrc_tpl = get_torrc_template()
fs.CreateFile('/var/local/tor/torrc.tpl', contents=torrc_tpl)
fs.CreateFile('/etc/tor/torrc')
onion = Onions()
onion._get_setup_from_env()
onion.apply_conf()
with open('/etc/tor/torrc', 'r') as f:
torrc = f.read()
assert 'HiddenServiceDir /var/lib/tor/hidden_service/group1' in torrc
assert 'HiddenServicePort 80 service1:80' in torrc
assert 'HiddenServicePort 81 service2:80' in torrc
assert 'HiddenServicePort 82 service2:8000' in torrc
assert 'HiddenServiceDir /var/lib/tor/hidden_service/group2' in torrc
assert 'HiddenServicePort 80 unix://unix.socket' in torrc
# Check parser
onion2 = Onions()
onion2.torrc_parser()
assert len(onion2.services) == 2
assert set(
group.name for group in onion2.services
) == set(['group1', 'group2'])
for group in onion2.services:
if group.name == 'group1':
assert len(group.services) == 2
assert set(
service.host for service in group.services
) == set(['service1', 'service2'])
for service in group.services:
if service.host == 'service1':
assert len(service.ports) == 1
assert set(
(port.port_from, port.dest) for port in service.ports
) == set([(80, 80)])
if service.host == 'service2':
assert len(service.ports) == 2
assert set(
(port.port_from, port.dest) for port in service.ports
) == set([(81, 80), (82, 8000)])
if group.name == 'group2':
assert len(group.services) == 1
assert set(
service.host for service in group.services
) == set(['group2'])
service = group.services[0]
assert len(service.ports) == 1
assert set(
(port.port_from, port.dest) for port in service.ports
) == set([(80, 'unix://unix.socket')])
def test_groups(monkeypatch):
env = {
'SERVICE1_SERVICE_NAME': 'group1',
'SERVICE2_SERVICE_NAME': 'group1',
'SERVICE3_SERVICE_NAME': 'group2',
'SERVICE1_PORTS': '80:80',
'SERVICE2_PORTS': '81:80,82:8000',
'SERVICE3_PORTS': '80:unix://unix.socket',
}
monkeypatch.setattr(os, 'environ', env)
onion = Onions()
onion._get_setup_from_env()
onion_match = r'^[a-z2-7]{16}.onion$'
assert len(os.environ) == 6
assert len(onion.services) == 2
assert set(
group.name for group in onion.services
) == set(['group1', 'group2'])
for group in onion.services:
if group.name == 'group1':
assert len(group.services) == 2
assert set(
service.host for service in group.services
) == set(['service1', 'service2'])
if group.name == 'group2':
assert len(group.services) == 1
assert set(
service.host for service in group.services
) == set(['service3'])
assert re.match(onion_match, group.onion_url)
def test_json(monkeypatch):
env = {
'SERVICE1_SERVICE_NAME': 'group1',
'SERVICE2_SERVICE_NAME': 'group1',
'SERVICE3_SERVICE_NAME': 'group2',
'SERVICE1_PORTS': '80:80',
'SERVICE2_PORTS': '81:80,82:8000',
'SERVICE3_PORTS': '80:unix://unix.socket',
}
monkeypatch.setattr(os, 'environ', env)
onion = Onions()
onion._get_setup_from_env()
onion.check_services()
jsn = json.loads(onion.to_json())
assert len(jsn) == 2
assert len(jsn['group1']) == 3
assert len(jsn['group2']) == 1
def test_output(monkeypatch):
env = {
'SERVICE1_SERVICE_NAME': 'group1',
'SERVICE2_SERVICE_NAME': 'group1',
'SERVICE3_SERVICE_NAME': 'group2',
'SERVICE1_PORTS': '80:80',
'SERVICE2_PORTS': '81:80,82:8000',
'SERVICE3_PORTS': '80:unix://unix.socket',
}
monkeypatch.setattr(os, 'environ', env)
onion = Onions()
onion._get_setup_from_env()
for item in ['group1', 'group2', '.onion', ',']:
assert item in str(onion)
def test_not_valid_share_port(monkeypatch):
env = {
'SERVICE1_SERVICE_NAME': 'group1',
'SERVICE2_SERVICE_NAME': 'group1',
'SERVICE3_SERVICE_NAME': 'group2',
'SERVICE1_PORTS': '80:80',
'SERVICE2_PORTS': '80:80,82:8000',
'SERVICE3_PORTS': '80:unix://unix.socket',
}
monkeypatch.setattr(os, 'environ', env)
onion = Onions()
onion._get_setup_from_env()
with pytest.raises(Exception) as excinfo:
onion.check_services()
assert 'Same port for multiple services' in str(excinfo.value)
def test_not_valid_no_services(monkeypatch):
env = {
'SERVICE1_SERVICE_NAME': 'group1',
'SERVICE2_SERVICE_NAME': 'group1',
'SERVICE3_SERVICE_NAME': 'group2',
}
monkeypatch.setattr(os, 'environ', env)
onion = Onions()
onion._get_setup_from_env()
with pytest.raises(Exception) as excinfo:
onion.check_services()
assert 'has not ports set' in str(excinfo.value)

@ -1,9 +1,14 @@
{% for service, conf in setup.items() %}
HiddenServiceDir /var/lib/tor/hidden_service/{{service}}
{% for ports in conf['ports'] %}
{% set map = ports[1] if type(ports[1]) != int else '{service}:{port}'.format(service=service, port=ports[1]) %}
# PORT {{service}} {{ports[0]}}
HiddenServicePort {{ports[0]}} {{map}}
{% for service_group in services %}
HiddenServiceDir /var/lib/tor/hidden_service/{{service_group.name}}
{% for service in service_group.services %}
{% for port in service.ports %}
{% if port.is_socket %}
HiddenServicePort {{port.port_from}} {{port.dest}}
{% endif %}
{% if not port.is_socket %}
HiddenServicePort {{port.port_from}} {{service.host}}:{{port.dest}}
{% endif %}
{% endfor %}
{% endfor %}
{% endfor %}
@ -12,3 +17,5 @@ ORPort 9001
{% endif %}
SocksPort 0
# useless line for Jinja bug

@ -9,6 +9,7 @@ services:
links:
- hello
- world
- again
environment:
# Set mapping ports
HELLO_PORTS: 80:80,800:80,8888:80
@ -32,6 +33,12 @@ services:
WORLD_PORTS: 8000:80
AGAIN_PORTS: 88:80
# hello and again will share the same onion_adress
AGAIN_SERVICE_NAME: foo
HELLO_SERVICE_NAME: foo
# Keep keys in volumes
volumes:
- tor-keys:/var/lib/tor/hidden_service/
@ -44,6 +51,10 @@ services:
image: tutum/hello-world
hostname: world
again:
image: tutum/hello-world
hostname: again
volumes:
tor-keys:
driver: local

@ -0,0 +1,12 @@
[tox]
envlist = py34, py35, py36
changedir=assets/onions/
setupdir=assets/onions/
skip_missing_interpreters = true
[testenv]
deps=
pytest
pyfakefs
pytest-mock
commands=pytest -v
Loading…
Cancel
Save