diff --git a/Dockerfile b/Dockerfile index 55db022..34f9fbc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,28 @@ FROM python:3-alpine -WORKDIR /workdir + +# install build deps for pycryptodome and other c-based python modules RUN apk add alpine-sdk autoconf automake libtool gcc -ADD requirements.txt /workdir/ -RUN pip3 install -r requirements.txt +# add env var to let the relay know it's in a container +ENV DOCKER_RUNNING=true -ADD . /workdir/ +# setup various container properties +VOLUME ["/data"] CMD ["python", "-m", "relay"] +EXPOSE 8080/tcp +WORKDIR /opt/activityrelay -VOLUME ["/workdir/data"] +# install and update important python modules +RUN pip3 install -U setuptools wheel pip + +# only copy necessary files +COPY relay ./relay +COPY LICENSE . +COPY README.md . +COPY requirements.txt . +COPY setup.cfg . +COPY setup.py . +COPY .git ./.git + +# install relay deps +RUN pip3 install -r requirements.txt diff --git a/README.md b/README.md index 8264009..cbe4b90 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ in this package as the `LICENSE` file. You need at least Python 3.6 (latest version of 3.x recommended) to make use of this software. It simply will not run on older Python versions. -Download the project and install with pip (`pip3 install .`). +Download the project and install with pip (`pip3 install -r requirements.txt`). -Run `activityrelay setup` and answer the prompts or copy `relay.yaml.example` to `relay.yaml` +Run `python3 -m relay setup` and answer the prompts or copy `relay.yaml.example` to `relay.yaml` and edit it as appropriate: $ cp relay.yaml.example relay.yaml @@ -25,7 +25,7 @@ and edit it as appropriate: Finally, you can launch the relay: - $ activityrelay run + $ python3 -m relay run It is suggested to run this under some sort of supervisor, such as runit, daemontools, s6 or systemd. Configuration of the supervisor is not covered here, as it is different @@ -60,17 +60,12 @@ You can perform a few management tasks such as peering or depeering other relays This will show the available management tasks: - $ activityrelay --help + $ python3 -m relay --help When following remote relays, you should use the `/actor` endpoint as you would in Pleroma and other LitePub-compliant software. ## Docker -You can run ActivityRelay with docker. Edit `relay.yaml` so that the database -location is set to `./data/relay.jsonld` and then build and run the docker -image : - - $ docker volume create activityrelay-data - $ docker build -t activityrelay . - $ docker run -d -p 8080:8080 -v activityrelay-data:/workdir/data activityrelay +You can run ActivityRelay with docker via the docker management script: `docker.sh`. +Run it without arguments to see the list of commands. diff --git a/docker.sh b/docker.sh new file mode 100755 index 0000000..d372dbf --- /dev/null +++ b/docker.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +case $1 in + install) + docker build -f Dockerfile -t activityrelay . && \ + docker volume create activityrelay-data && \ + docker run -it -p 8080:8080 -v activityrelay-data:/data --name activityrelay activityrelay + ;; + + uninstall) + docker stop activityrelay && \ + docker container rm activityrelay && \ + docker volume rm activityrelay-data && \ + docker image rm activityrelay + ;; + + start) + docker start activityrelay + ;; + + stop) + docker stop activityrelay + ;; + + manage) + shift + docker exec -it activityrelay python3 -m relay "$@" + ;; + + shell) + docker exec -it activityrelay bash + ;; + + rescue) + docker run -it --rm --entrypoint bash -v activityrelay-data:/data activityrelay + ;; + + edit) + if [ -z ${EDITOR} ]; then + echo "EDITOR environmental variable not set" + exit + fi + + CONFIG="/tmp/relay-$(date +"%T").yaml" + + docker cp activityrelay:/data/relay.yaml $CONFIG && \ + $EDITOR $CONFIG && \ + + docker cp $CONFIG activityrelay:/data/relay.yaml && \ + rm $CONFIG + ;; + + *) + COLS="%-22s %s\n" + + echo "Valid commands:" + printf "$COLS" "- start" "Run the relay in the background" + printf "$COLS" "- stop" "Stop the relay" + printf "$COLS" "- manage [args]" "Run a relay management command" + printf "$COLS" "- edit" "Edit the relay's config in \$EDITOR" + printf "$COLS" "- shell" "Drop into a bash shell on the running container" + printf "$COLS" "- rescue" "Drop into a bash shell on a temp container with the data volume mounted" + printf "$COLS" "- install" "Build the image, create a new container and volume, and run relay setup" + printf "$COLS" "- uninstall" "Delete the relay image, container, and volume" + ;; +esac diff --git a/relay/config.py b/relay/config.py index fd4ee34..45fcf40 100644 --- a/relay/config.py +++ b/relay/config.py @@ -60,11 +60,15 @@ class RelayConfig(DotDict): } - def __init__(self, path): - self._path = Path(path).expanduser().resolve() + def __init__(self, path, is_docker): + if is_docker: + path = '/data/relay.yaml' + + self._isdocker = is_docker + self._path = Path(path).expanduser() super().__init__({ - 'db': f'{self._path.stem}.jsonld', + 'db': str(self._path.parent.joinpath(f'{self._path.stem}.jsonld')), 'listen': '0.0.0.0', 'port': 8080, 'note': 'Make a note about your instance here.', @@ -81,6 +85,9 @@ class RelayConfig(DotDict): def __setitem__(self, key, value): + if self._isdocker and key in ['db', 'listen', 'port']: + return + if key in ['blocked_instances', 'blocked_software', 'whitelist']: assert isinstance(value, (list, set, tuple)) @@ -234,7 +241,7 @@ class RelayConfig(DotDict): self[key] = value - if self.host == 'example.com': + if self.host.endswith('example.com'): return False return True diff --git a/relay/manage.py b/relay/manage.py index 25227e4..213ecdb 100644 --- a/relay/manage.py +++ b/relay/manage.py @@ -3,6 +3,7 @@ import asyncio import click import json import logging +import os import platform from aiohttp.web import AppRunner, TCPSite @@ -11,14 +12,15 @@ from cachetools import LRUCache from . import app, misc, views from .config import DotDict, RelayConfig from .database import RelayDatabase -from .misc import follow_remote_actor, unfollow_remote_actor +from .misc import check_open_port, follow_remote_actor, unfollow_remote_actor @click.group('cli', context_settings={'show_default': True}, invoke_without_command=True) @click.option('--config', '-c', default='relay.yaml', help='path to the relay\'s config') @click.pass_context def cli(ctx, config): - app['config'] = RelayConfig(config) + app['is_docker'] = bool(os.environ.get('DOCKER_RUNNING')) + app['config'] = RelayConfig(config, app['is_docker']) if not app['config'].load(): app['config'].save() @@ -33,7 +35,11 @@ def cli(ctx, config): app['cache'][key] = LRUCache(app['config'][key]) if not ctx.invoked_subcommand: - relay_run.callback() + if app['config'].host.endswith('example.com'): + relay_setup.callback() + + else: + relay_run.callback() @cli.command('list') @@ -239,7 +245,7 @@ def relay_setup(): config.save() - if click.confirm('Relay all setup! Would you like to run it now?'): + if not app['is_docker'] and click.confirm('Relay all setup! Would you like to run it now?'): relay_run.callback() @@ -247,7 +253,9 @@ def relay_setup(): def relay_run(): 'Run the relay' - if app['config'].host.endswith('example.com'): + config = app['config'] + + if config.host.endswith('example.com'): return click.echo('Relay is not set up. Please edit your relay config or run "activityrelay setup".') vers_split = platform.python_version().split('.') @@ -262,6 +270,9 @@ def relay_run(): click.echo('Warning: PyCrypto is old and should be replaced with pycryptodome') return click.echo(pip_command) + if not check_open_port(config.listen, config.port): + return click.echo(f'Error: A server is already running on port {config.port}') + # web pages app.router.add_get('/', views.home) diff --git a/relay/misc.py b/relay/misc.py index 6a51e07..90c30e0 100644 --- a/relay/misc.py +++ b/relay/misc.py @@ -2,6 +2,7 @@ import asyncio import base64 import json import logging +import socket import traceback from Crypto.Hash import SHA, SHA256, SHA512 @@ -28,6 +29,18 @@ def build_signing_string(headers, used_headers): return '\n'.join(map(lambda x: ': '.join([x.lower(), headers[x]]), used_headers)) +def check_open_port(host, port): + if host == '0.0.0.0': + host = '127.0.0.1' + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + return s.connect_ex((host , port)) != 0 + + except socket.error as e: + return False + + def create_signature_header(headers): headers = {k.lower(): v for k, v in headers.items()} used_headers = headers.keys() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9c558e3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +.