mirror of
https://github.com/cmehay/docker-tor-hidden-service.git
synced 2025-04-22 06:49:10 +00:00
Add support for multiple services per address
This commit is contained in:
parent
a939d3620f
commit
27dd14ab33
13 changed files with 890 additions and 81 deletions
|
@ -1 +1,4 @@
|
||||||
keys/
|
keys/
|
||||||
|
*.egg-info
|
||||||
|
.tox/
|
||||||
|
__cache__
|
||||||
|
|
107
.gitignore
vendored
Normal file
107
.gitignore
vendored
Normal file
|
@ -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/
|
8
.travis.yml
Normal file
8
.travis.yml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
sudo: false
|
||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- "3.4"
|
||||||
|
- "3.5"
|
||||||
|
- "3.6"
|
||||||
|
install: pip install tox-travis
|
||||||
|
script: tox
|
10
Dockerfile
10
Dockerfile
|
@ -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 del git libevent-dev openssl-dev make automake python3-dev gcc autoconf musl-dev coreutils && \
|
||||||
apk add --no-cache libevent openssl
|
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/entrypoint-config.yml /
|
||||||
ADD assets/onions /usr/local/src/onions
|
ADD assets/onions /usr/local/src/onions
|
||||||
ADD assets/torrc /var/local/tor/torrc.tpl
|
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
|
RUN cd /usr/local/src/onions && python3 setup.py install
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ pre_conf_commands:
|
||||||
- onions --setup-hosts
|
- onions --setup-hosts
|
||||||
|
|
||||||
post_conf_commands:
|
post_conf_commands:
|
||||||
- timeout -t 3 tor > /dev/null || true
|
|
||||||
- onions
|
- onions
|
||||||
- chown -R tor:tor $HOME
|
- chown -R tor:tor $HOME
|
||||||
|
|
||||||
|
|
|
@ -12,11 +12,9 @@ import argparse
|
||||||
from jinja2 import Environment
|
from jinja2 import Environment
|
||||||
from jinja2 import FileSystemLoader
|
from jinja2 import FileSystemLoader
|
||||||
|
|
||||||
import socket
|
from .Service import ServicesGroup, Service
|
||||||
|
|
||||||
from Crypto.PublicKey import RSA
|
import logging
|
||||||
from hashlib import sha1
|
|
||||||
from base64 import b32encode
|
|
||||||
|
|
||||||
|
|
||||||
class Setup(object):
|
class Setup(object):
|
||||||
|
@ -25,18 +23,6 @@ class Setup(object):
|
||||||
torrc = '/etc/tor/torrc'
|
torrc = '/etc/tor/torrc'
|
||||||
torrc_template = '/var/local/tor/torrc.tpl'
|
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):
|
def _add_host(self, host):
|
||||||
if host not in self.setup:
|
if host not in self.setup:
|
||||||
self.setup[host] = {}
|
self.setup[host] = {}
|
||||||
|
@ -44,7 +30,9 @@ class Setup(object):
|
||||||
def _get_ports(self, host, ports):
|
def _get_ports(self, host, ports):
|
||||||
self._add_host(host)
|
self._add_host(host)
|
||||||
if 'ports' not in self.setup[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 = [
|
ports_l = [
|
||||||
[
|
[
|
||||||
int(v) if not v.startswith('unix:') else v
|
int(v) if not v.startswith('unix:') else v
|
||||||
|
@ -53,18 +41,82 @@ class Setup(object):
|
||||||
]
|
]
|
||||||
for port in ports_l:
|
for port in ports_l:
|
||||||
assert len(port) == 2
|
assert len(port) == 2
|
||||||
if port not in self.setup[host]['ports']:
|
if port not in self.setup[host]['ports'][host]:
|
||||||
self.setup[host]['ports'].append(port)
|
self.setup[host]['ports'][host].append(port)
|
||||||
|
|
||||||
def _get_key(self, host, key):
|
def _get_key(self, host, key):
|
||||||
self._add_host(host)
|
self._add_host(host)
|
||||||
assert len(key) > 800
|
assert len(key) > 800
|
||||||
self.setup[host]['key'] = key
|
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 = (
|
match_map = (
|
||||||
(r'([A-Z0-9]*)_PORTS', self._get_ports),
|
(r'([A-Z0-9]*)_PORTS', self._set_ports),
|
||||||
(r'([A-Z0-9]*)_KEY', self._get_key),
|
(r'([A-Z0-9]*)_KEY', self._set_key),
|
||||||
)
|
)
|
||||||
for key, val in os.environ.items():
|
for key, val in os.environ.items():
|
||||||
for reg, call in match_map:
|
for reg, call in match_map:
|
||||||
|
@ -72,39 +124,39 @@ class Setup(object):
|
||||||
if m:
|
if m:
|
||||||
call(m.groups()[0].lower(), val)
|
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):
|
def _get_setup_from_links(self):
|
||||||
containers = DockerLinks().to_containers()
|
containers = DockerLinks().to_containers()
|
||||||
if not containers:
|
if not containers:
|
||||||
return
|
return
|
||||||
for container in containers:
|
for container in containers:
|
||||||
host = container.names[0]
|
host = container.names[0]
|
||||||
self._add_host(host)
|
self.add_new_service(host=host)
|
||||||
for link in container.links:
|
for link in container.links:
|
||||||
if link.protocol != 'tcp':
|
if link.protocol != 'tcp':
|
||||||
continue
|
continue
|
||||||
port_map = os.environ.get('PORT_MAP')
|
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,
|
exposed=port_map or link.port,
|
||||||
internal=link.port,
|
internal=link.port,
|
||||||
))
|
))
|
||||||
|
|
||||||
def _set_keys(self):
|
def apply_conf(self):
|
||||||
for link, conf in self.setup.items():
|
self._write_keys()
|
||||||
if 'key' in conf:
|
self._write_torrc()
|
||||||
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 _write_keys(self):
|
||||||
|
for service in self.services:
|
||||||
|
service.write_key()
|
||||||
|
|
||||||
|
def _write_torrc(self):
|
||||||
env = Environment(loader=FileSystemLoader('/'))
|
env = Environment(loader=FileSystemLoader('/'))
|
||||||
temp = env.get_template(self.torrc_template)
|
temp = env.get_template(self.torrc_template)
|
||||||
with open(self.torrc, mode='w') as f:
|
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,
|
env=os.environ,
|
||||||
type=type,
|
type=type,
|
||||||
int=int))
|
int=int))
|
||||||
|
@ -114,58 +166,94 @@ class Setup(object):
|
||||||
try:
|
try:
|
||||||
self._get_setup_from_env()
|
self._get_setup_from_env()
|
||||||
self._get_setup_from_links()
|
self._get_setup_from_links()
|
||||||
self._set_keys()
|
self.check_services()
|
||||||
self._set_conf()
|
self.apply_conf()
|
||||||
except:
|
except BaseException:
|
||||||
raise Exception('Something wrongs with setup')
|
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):
|
class Onions(Setup):
|
||||||
"""Onions"""
|
"""Onions"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.services = []
|
||||||
if 'HIDDEN_SERVICE_DIR' in os.environ:
|
if 'HIDDEN_SERVICE_DIR' in os.environ:
|
||||||
self.hidden_service_dir = os.environ['HIDDEN_SERVICE_DIR']
|
self.hidden_service_dir = os.environ['HIDDEN_SERVICE_DIR']
|
||||||
|
|
||||||
def _get_port_from_service(self, service, filename):
|
def torrc_parser(self):
|
||||||
|
|
||||||
with open(filename, 'r') as hostfile:
|
def parse_dir(line):
|
||||||
onion = str(hostfile.read()).strip()
|
_, 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
|
||||||
|
|
||||||
with open(self.torrc, 'r') as torfile:
|
def parse_port(line, service_group):
|
||||||
self.onions[service] = []
|
_, port_from, dest = line.split()
|
||||||
for line in torfile.readlines():
|
service_host, port = dest.split(':')
|
||||||
find = '# PORT {name}'.format(name=service)
|
ports_str = '{port_from}:{dest}'
|
||||||
if line.startswith(find):
|
name = service_host
|
||||||
self.onions[service].append(
|
ports_param = ports_str.format(port_from=port_from,
|
||||||
'{onion}:{port}'.format(
|
dest=port)
|
||||||
onion=onion,
|
if port.startswith('/'):
|
||||||
port=line[len(find):].strip()
|
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)
|
||||||
|
|
||||||
def get_onions(self):
|
if not os.path.exists(self.torrc):
|
||||||
self.onions = {}
|
return
|
||||||
for root, dirs, _ in os.walk(self.hidden_service_dir,
|
with open(self.torrc, 'r') as f:
|
||||||
topdown=False):
|
for line in f.readlines():
|
||||||
for service in dirs:
|
if line.startswith('HiddenServiceDir'):
|
||||||
filename = "{root}{service}/hostname".format(
|
service_group = parse_dir(line)
|
||||||
service=service,
|
if line.startswith('HiddenServicePort'):
|
||||||
root=root
|
parse_port(line, service_group)
|
||||||
)
|
|
||||||
self._get_port_from_service(service, filename)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if not self.onions:
|
if not self.services:
|
||||||
return 'No onion site'
|
return 'No onion site'
|
||||||
return '\n'.join(['%s: %s' % (service, ', '.join(onion))
|
return '\n'.join([str(service) for service in self.services])
|
||||||
for (service, onion) in self.onions.items()])
|
|
||||||
|
|
||||||
def to_json(self):
|
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():
|
def main():
|
||||||
|
logging.basicConfig()
|
||||||
parser = argparse.ArgumentParser(description='Display onion sites',
|
parser = argparse.ArgumentParser(description='Display onion sites',
|
||||||
prog='onions')
|
prog='onions')
|
||||||
parser.add_argument('--json', dest='json', action='store_true',
|
parser.add_argument('--json', dest='json', action='store_true',
|
||||||
|
@ -179,8 +267,9 @@ def main():
|
||||||
if args.setup:
|
if args.setup:
|
||||||
onions.setup_hosts()
|
onions.setup_hosts()
|
||||||
return
|
return
|
||||||
onions.get_onions()
|
onions.torrc_parser()
|
||||||
if args.json:
|
if args.json:
|
||||||
|
logging.getLogger().setLevel(logging.ERROR)
|
||||||
print(onions.to_json())
|
print(onions.to_json())
|
||||||
else:
|
else:
|
||||||
print(onions)
|
print(onions)
|
||||||
|
|
169
assets/onions/onions/Service.py
Normal file
169
assets/onions/onions/Service.py
Normal file
|
@ -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 .Onions import Onions, main
|
||||||
|
from .Service import ServicesGroup, Service, Ports
|
||||||
|
|
|
@ -6,7 +6,7 @@ from setuptools import setup
|
||||||
setup(
|
setup(
|
||||||
name='onions',
|
name='onions',
|
||||||
|
|
||||||
version='0.2',
|
version='0.4',
|
||||||
|
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ setup(
|
||||||
"Topic :: System :: Installation/Setup",
|
"Topic :: System :: Installation/Setup",
|
||||||
],
|
],
|
||||||
|
|
||||||
install_requires=['pyentrypoint',
|
install_requires=['pyentrypoint==0.5.0',
|
||||||
'Jinja2>=2.8',
|
'Jinja2>=2.8',
|
||||||
'pycrypto',],
|
'pycrypto',],
|
||||||
|
|
||||||
|
|
399
assets/onions/tests/onions_test.py
Normal file
399
assets/onions/tests/onions_test.py
Normal file
|
@ -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)
|
19
assets/torrc
19
assets/torrc
|
@ -1,9 +1,14 @@
|
||||||
{% for service, conf in setup.items() %}
|
{% for service_group in services %}
|
||||||
HiddenServiceDir /var/lib/tor/hidden_service/{{service}}
|
HiddenServiceDir /var/lib/tor/hidden_service/{{service_group.name}}
|
||||||
{% for ports in conf['ports'] %}
|
{% for service in service_group.services %}
|
||||||
{% set map = ports[1] if type(ports[1]) != int else '{service}:{port}'.format(service=service, port=ports[1]) %}
|
{% for port in service.ports %}
|
||||||
# PORT {{service}} {{ports[0]}}
|
{% if port.is_socket %}
|
||||||
HiddenServicePort {{ports[0]}} {{map}}
|
HiddenServicePort {{port.port_from}} {{port.dest}}
|
||||||
|
{% endif %}
|
||||||
|
{% if not port.is_socket %}
|
||||||
|
HiddenServicePort {{port.port_from}} {{service.host}}:{{port.dest}}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
@ -12,3 +17,5 @@ ORPort 9001
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
SocksPort 0
|
SocksPort 0
|
||||||
|
|
||||||
|
# useless line for Jinja bug
|
||||||
|
|
|
@ -9,6 +9,7 @@ services:
|
||||||
links:
|
links:
|
||||||
- hello
|
- hello
|
||||||
- world
|
- world
|
||||||
|
- again
|
||||||
environment:
|
environment:
|
||||||
# Set mapping ports
|
# Set mapping ports
|
||||||
HELLO_PORTS: 80:80,800:80,8888:80
|
HELLO_PORTS: 80:80,800:80,8888:80
|
||||||
|
@ -32,6 +33,12 @@ services:
|
||||||
|
|
||||||
WORLD_PORTS: 8000:80
|
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
|
# Keep keys in volumes
|
||||||
volumes:
|
volumes:
|
||||||
- tor-keys:/var/lib/tor/hidden_service/
|
- tor-keys:/var/lib/tor/hidden_service/
|
||||||
|
@ -44,6 +51,10 @@ services:
|
||||||
image: tutum/hello-world
|
image: tutum/hello-world
|
||||||
hostname: world
|
hostname: world
|
||||||
|
|
||||||
|
again:
|
||||||
|
image: tutum/hello-world
|
||||||
|
hostname: again
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
tor-keys:
|
tor-keys:
|
||||||
driver: local
|
driver: local
|
||||||
|
|
12
tox.ini
Normal file
12
tox.ini
Normal file
|
@ -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…
Add table
Reference in a new issue