diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..9eef6f1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +- repo: git://github.com/pre-commit/pre-commit-hooks + sha: v0.9.1 + hooks: + - id: check-added-large-files + - id: check-docstring-first + - id: check-merge-conflict + - id: check-yaml + - id: end-of-file-fixer + - id: flake8 + args: + - --exclude=__init__.py + language_version: python3 + - id: autopep8-wrapper + language_version: python3 + - id: requirements-txt-fixer + - id: trailing-whitespace +- repo: git://github.com/asottile/reorder_python_imports + sha: v0.3.5 + hooks: + - id: reorder-python-imports + language_version: python3 diff --git a/.travis.yml b/.travis.yml index 095cf5c..bd531f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,5 +4,7 @@ python: - "3.4" - "3.5" - "3.6" -install: pip install tox-travis -script: tox +install: pip install tox-travis pre-commit +script: + - pre-commit run --all-files + - tox diff --git a/README.md b/README.md index 664f58d..78ec278 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,34 @@ To increase security, it's possible to setup your service through socket between __Warning__: Due to a bug in `tor` configuration parser, it's not possible to mix network link and socket link in the same `tor` configuration. +### Group services + +Multiple services can be hosted behind the same onion address. + +```yaml +links: + - hello + - world + - hey +environment: + # Set mapping ports + HELLO_PORTS: 80:80 + + # Multiple ports can be coma separated + WORLD_PORTS: 8000:80,8888:80,22:22 + + # Socket mapping is supported + HEY_PORTS: 80:unix:/var/run/socket.sock + + # hello and world will share the same onion address + # Service name can be any string as long there is not special char + HELLO_SERVICE_NAME: foo + WORLD_SERVICE_NAME: foo + +``` + +__Warning__: Be carefull to not use the same exposed ports for grouped services. + ### Compose v2 support Links setting are required when using docker-compose v2. See `docker-compose.v2.yml` for example. diff --git a/assets/onions/onions/Onions.py b/assets/onions/onions/Onions.py index d1ef9e3..ed51ea7 100644 --- a/assets/onions/onions/Onions.py +++ b/assets/onions/onions/Onions.py @@ -1,20 +1,17 @@ #!/usr/bin/env python3 - -import os -from json import dumps - -from re import match - -from pyentrypoint import DockerLinks - import argparse +import logging +import os +import sys +from json import dumps +from re import match from jinja2 import Environment from jinja2 import FileSystemLoader +from pyentrypoint import DockerLinks -from .Service import ServicesGroup, Service - -import logging +from .Service import Service +from .Service import ServicesGroup class Setup(object): @@ -163,13 +160,10 @@ class Setup(object): def setup_hosts(self): self.setup = {} - try: - self._get_setup_from_env() - self._get_setup_from_links() - self.check_services() - self.apply_conf() - except BaseException: - raise Exception('Something wrongs with setup') + self._get_setup_from_env() + self._get_setup_from_links() + self.check_services() + self.apply_conf() def check_services(self): for group in self.services: @@ -263,15 +257,26 @@ def main(): help='Setup hosts') args = parser.parse_args() - onions = Onions() - if args.setup: - onions.setup_hosts() - return - onions.torrc_parser() + try: + onions = Onions() + if args.setup: + onions.setup_hosts() + else: + onions.torrc_parser() + except BaseException as e: + error_msg = str(e) + else: + error_msg = None if args.json: + if error_msg: + print(dumps({'error': error_msg})) + sys.exit(1) logging.getLogger().setLevel(logging.ERROR) print(onions.to_json()) else: + if error_msg: + logging.error(error_msg) + sys.exit(1) print(onions) diff --git a/assets/onions/onions/Service.py b/assets/onions/onions/Service.py index 2a3b87f..d153125 100644 --- a/assets/onions/onions/Service.py +++ b/assets/onions/onions/Service.py @@ -1,12 +1,11 @@ 'This class define a service link' +import logging +import os +import re +from base64 import b32encode +from hashlib import sha1 from Crypto.PublicKey import RSA -from hashlib import sha1 -from base64 import b32encode - -import os - -import logging class ServicesGroup(object): @@ -18,6 +17,9 @@ class ServicesGroup(object): hidden_service_dir = "/var/lib/tor/hidden_service/" def __init__(self, name=None, service=None, hidden_service_dir=None): + + name_regex = r'^[a-zA-Z0-9-_]+$' + self.hidden_service_dir = hidden_service_dir or self.hidden_service_dir if not name and not service: raise Exception( @@ -25,6 +27,10 @@ class ServicesGroup(object): ) self.services = [] self.name = name or service.host + if not re.match(name_regex, self.name): + raise Exception( + 'Group {name} has invalid name'.format(name=self.name) + ) if service: self.add_service(service) diff --git a/assets/onions/onions/__init__.py b/assets/onions/onions/__init__.py index 7ef73e4..d3b0b70 100644 --- a/assets/onions/onions/__init__.py +++ b/assets/onions/onions/__init__.py @@ -1,2 +1,5 @@ -from .Onions import Onions, main -from .Service import ServicesGroup, Service, Ports +from .Onions import main +from .Onions import Onions +from .Service import Ports +from .Service import Service +from .Service import ServicesGroup diff --git a/assets/onions/setup.py b/assets/onions/setup.py index 98d7f55..467e13c 100644 --- a/assets/onions/setup.py +++ b/assets/onions/setup.py @@ -33,7 +33,7 @@ setup( install_requires=['pyentrypoint==0.5.0', 'Jinja2>=2.8', - 'pycrypto',], + 'pycrypto', ], entry_points={ 'console_scripts': [ diff --git a/assets/onions/tests/onions_test.py b/assets/onions/tests/onions_test.py index a078659..9ac85c5 100644 --- a/assets/onions/tests/onions_test.py +++ b/assets/onions/tests/onions_test.py @@ -1,13 +1,13 @@ -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 +from hashlib import sha1 + +import pytest +from Crypto.PublicKey import RSA +from onions import Onions + def get_key_and_onion(): key = ''' @@ -40,6 +40,7 @@ bhJ3M9WzP/EMkAzyW8mVs1moFp3hRcfQlZHl6g1U9D8= return key.strip(), onion + def get_torrc_template(): return r''' {% for service_group in services %} @@ -65,6 +66,7 @@ SocksPort 0 # useless line for Jinja bug '''.strip() + def test_ports(monkeypatch): env = { 'SERVICE1_PORTS': '80:80', @@ -104,6 +106,7 @@ def test_ports(monkeypatch): assert check == 10 + def test_docker_links(fs, monkeypatch): env = { @@ -186,6 +189,7 @@ def test_key(monkeypatch): assert onion.services[0].onion_url == onion_url + def test_key_in_secret(fs, monkeypatch): env = { 'SERVICE1_SERVICE_NAME': 'group1', @@ -284,6 +288,7 @@ def test_configuration(fs, monkeypatch): (port.port_from, port.dest) for port in service.ports ) == set([(80, 'unix://unix.socket')]) + def test_groups(monkeypatch): env = { 'SERVICE1_SERVICE_NAME': 'group1', @@ -323,6 +328,7 @@ def test_groups(monkeypatch): assert re.match(onion_match, group.onion_url) + def test_json(monkeypatch): env = { 'SERVICE1_SERVICE_NAME': 'group1', @@ -345,6 +351,7 @@ def test_json(monkeypatch): assert len(jsn['group1']) == 3 assert len(jsn['group2']) == 1 + def test_output(monkeypatch): env = { 'SERVICE1_SERVICE_NAME': 'group1', @@ -363,6 +370,7 @@ def test_output(monkeypatch): for item in ['group1', 'group2', '.onion', ',']: assert item in str(onion) + def test_not_valid_share_port(monkeypatch): env = { 'SERVICE1_SERVICE_NAME': 'group1', @@ -382,6 +390,7 @@ def test_not_valid_share_port(monkeypatch): 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',