Compare commits

..

7 commits

Author SHA1 Message Date
Izalia Mae 7d37ec8145 remove await from push_message calls and reject non-system actors 2022-12-04 04:40:40 -05:00
Izalia Mae 9f58c88e9f Fix NameError when getting nodeinfo software name in processors 2022-12-04 04:16:50 -05:00
Izalia Mae 6b86bb7d98 remove leftover semaphore property 2022-12-04 02:13:13 -05:00
Izalia Mae 90234a9724 move apkeys out of RelayConfig and rename relay_software_names 2022-12-04 01:20:17 -05:00
Izalia Mae b0851c0652 remove http_debug 2022-12-04 01:15:28 -05:00
Izalia Mae eab8a31001 document new commands 2022-12-04 01:12:58 -05:00
Izalia Mae 3b89aa5e84 sort out cli
added `whitelist import` command which adds all current inboxes to the whitelist
added `config list`
fixed a few errors
2022-12-04 01:09:45 -05:00
8 changed files with 98 additions and 121 deletions

View file

@ -26,11 +26,18 @@ Run the setup wizard to configure your relay.
## Config ## Config
List the current configuration key/value pairs Manage the relay config
activityrelay config activityrelay config
### List
List the current config key/value pairs
activityrelay config list
### Set ### Set
Set a value for a config option Set a value for a config option
@ -111,6 +118,13 @@ Remove a domain from the whitelist.
activityrelay whitelist remove <domain> activityrelay whitelist remove <domain>
### Import
Add all current inboxes to the whitelist
activityrelay whitelist import
## Instance ## Instance
Manage the instance ban list. Manage the instance ban list.

View file

@ -59,11 +59,6 @@ class Application(web.Application):
return self['database'] return self['database']
@property
def semaphore(self):
return self['semaphore']
@property @property
def uptime(self): def uptime(self):
if not self['starttime']: if not self['starttime']:
@ -102,9 +97,6 @@ class Application(web.Application):
return logging.error(f'A server is already running on port {self.config.port}') return logging.error(f'A server is already running on port {self.config.port}')
for route in routes: for route in routes:
if route[1] == '/stats' and logging.DEBUG < logging.root.level:
continue
self.router.add_route(*route) self.router.add_route(*route)
logging.info(f'Starting webserver at {self.config.host} ({self.config.listen}:{self.config.port})') logging.info(f'Starting webserver at {self.config.host} ({self.config.listen}:{self.config.port})')
@ -210,4 +202,3 @@ setattr(web.Request, 'signature', property(request_signature))
setattr(web.Request, 'config', property(lambda self: self.app.config)) setattr(web.Request, 'config', property(lambda self: self.app.config))
setattr(web.Request, 'database', property(lambda self: self.app.database)) setattr(web.Request, 'database', property(lambda self: self.app.database))
setattr(web.Request, 'semaphore', property(lambda self: self.app.semaphore))

View file

@ -9,23 +9,22 @@ from urllib.parse import urlparse
from .misc import DotDict, boolean from .misc import DotDict, boolean
relay_software_names = [ RELAY_SOFTWARE = [
'activityrelay', # https://git.pleroma.social/pleroma/relay 'activityrelay', # https://git.pleroma.social/pleroma/relay
'aoderelay', # https://git.asonix.dog/asonix/relay 'aoderelay', # https://git.asonix.dog/asonix/relay
'feditools-relay' # https://git.ptzo.gdn/feditools/relay 'feditools-relay' # https://git.ptzo.gdn/feditools/relay
] ]
APKEYS = [
'host',
'whitelist_enabled',
'blocked_software',
'blocked_instances',
'whitelist'
]
class RelayConfig(DotDict): class RelayConfig(DotDict):
apkeys = {
'host',
'whitelist_enabled',
'blocked_software',
'blocked_instances',
'whitelist'
}
def __init__(self, path): def __init__(self, path):
DotDict.__init__(self, {}) DotDict.__init__(self, {})
@ -243,7 +242,7 @@ class RelayConfig(DotDict):
'workers': self.workers, 'workers': self.workers,
'json_cache': self.json_cache, 'json_cache': self.json_cache,
'timeout': self.timeout, 'timeout': self.timeout,
'ap': {key: self[key] for key in self.apkeys} 'ap': {key: self[key] for key in APKEYS}
} }
with open(self._path, 'w') as fd: with open(self._path, 'w') as fd:

View file

@ -1,68 +0,0 @@
import logging
import aiohttp
from collections import defaultdict
STATS = {
'requests': defaultdict(int),
'response_codes': defaultdict(int),
'response_codes_per_domain': defaultdict(lambda: defaultdict(int)),
'delivery_codes': defaultdict(int),
'delivery_codes_per_domain': defaultdict(lambda: defaultdict(int)),
'exceptions': defaultdict(int),
'exceptions_per_domain': defaultdict(lambda: defaultdict(int)),
'delivery_exceptions': defaultdict(int),
'delivery_exceptions_per_domain': defaultdict(lambda: defaultdict(int))
}
async def on_request_start(session, trace_config_ctx, params):
global STATS
logging.debug("HTTP START [%r], [%r]", session, params)
STATS['requests'][params.url.host] += 1
async def on_request_end(session, trace_config_ctx, params):
global STATS
logging.debug("HTTP END [%r], [%r]", session, params)
host = params.url.host
status = params.response.status
STATS['response_codes'][status] += 1
STATS['response_codes_per_domain'][host][status] += 1
if params.method == 'POST':
STATS['delivery_codes'][status] += 1
STATS['delivery_codes_per_domain'][host][status] += 1
async def on_request_exception(session, trace_config_ctx, params):
global STATS
logging.debug("HTTP EXCEPTION [%r], [%r]", session, params)
host = params.url.host
exception = repr(params.exception)
STATS['exceptions'][exception] += 1
STATS['exceptions_per_domain'][host][exception] += 1
if params.method == 'POST':
STATS['delivery_exceptions'][exception] += 1
STATS['delivery_exceptions_per_domain'][host][exception] += 1
def http_debug():
if logging.DEBUG >= logging.root.level:
return
trace_config = aiohttp.TraceConfig()
trace_config.on_request_start.append(on_request_start)
trace_config.on_request_end.append(on_request_end)
trace_config.on_request_exception.append(on_request_exception)
return [trace_config]

View file

@ -8,7 +8,7 @@ from urllib.parse import urlparse
from . import misc, __version__ from . import misc, __version__
from .application import Application from .application import Application
from .config import relay_software_names from .config import RELAY_SOFTWARE
app = None app = None
@ -81,13 +81,15 @@ def cli_run():
# todo: add config default command for resetting config key # todo: add config default command for resetting config key
@cli.group('config', invoke_without_command=True) @cli.group('config')
@click.pass_context def cli_config():
def cli_config(ctx): 'Manage the relay config'
'List the current relay config' pass
if ctx.invoked_subcommand:
return @cli_config.command('list')
def cli_config_list():
'List the current relay config'
click.echo('Relay Config:') click.echo('Relay Config:')
@ -312,7 +314,7 @@ def cli_software_ban(name, fetch_nodeinfo):
'Ban software. Use RELAYS for NAME to ban relays' 'Ban software. Use RELAYS for NAME to ban relays'
if name == 'RELAYS': if name == 'RELAYS':
for name in relay_software_names: for name in RELAY_SOFTWARE:
app.config.ban_software(name) app.config.ban_software(name)
app.config.save() app.config.save()
@ -321,14 +323,16 @@ def cli_software_ban(name, fetch_nodeinfo):
if fetch_nodeinfo: if fetch_nodeinfo:
nodeinfo = asyncio.run(app.client.fetch_nodeinfo(name)) nodeinfo = asyncio.run(app.client.fetch_nodeinfo(name))
if not software: if not nodeinfo:
click.echo(f'Failed to fetch software name from domain: {name}') click.echo(f'Failed to fetch software name from domain: {name}')
if config.ban_software(nodeinfo.swname): name = nodeinfo.sw_name
app.config.save()
return click.echo(f'Banned software: {nodeinfo.swname}')
click.echo(f'Software already banned: {nodeinfo.swname}') if app.config.ban_software(name):
app.config.save()
return click.echo(f'Banned software: {name}')
click.echo(f'Software already banned: {name}')
@cli_software.command('unban') @cli_software.command('unban')
@ -340,10 +344,10 @@ def cli_software_unban(name, fetch_nodeinfo):
'Ban software. Use RELAYS for NAME to unban relays' 'Ban software. Use RELAYS for NAME to unban relays'
if name == 'RELAYS': if name == 'RELAYS':
for name in relay_software_names: for name in RELAY_SOFTWARE:
app.config.unban_software(name) app.config.unban_software(name)
config.save() app.config.save()
return click.echo('Unbanned all relay software') return click.echo('Unbanned all relay software')
if fetch_nodeinfo: if fetch_nodeinfo:
@ -352,12 +356,13 @@ def cli_software_unban(name, fetch_nodeinfo):
if not nodeinfo: if not nodeinfo:
click.echo(f'Failed to fetch software name from domain: {name}') click.echo(f'Failed to fetch software name from domain: {name}')
if app.config.unban_software(nodeinfo.swname): name = nodeinfo.sw_name
if app.config.unban_software(name):
app.config.save() app.config.save()
return click.echo(f'Unbanned software: {nodeinfo.swname}') return click.echo(f'Unbanned software: {name}')
click.echo(f'Software wasn\'t banned: {nodeinfo.swname}')
click.echo(f'Software wasn\'t banned: {name}')
@cli.group('whitelist') @cli.group('whitelist')
@ -368,6 +373,8 @@ def cli_whitelist():
@cli_whitelist.command('list') @cli_whitelist.command('list')
def cli_whitelist_list(): def cli_whitelist_list():
'List all the instances in the whitelist'
click.echo('Current whitelisted domains') click.echo('Current whitelisted domains')
for domain in app.config.whitelist: for domain in app.config.whitelist:
@ -403,6 +410,14 @@ def cli_whitelist_remove(instance):
click.echo(f'Removed instance from the whitelist: {instance}') click.echo(f'Removed instance from the whitelist: {instance}')
@cli_whitelist.command('import')
def cli_whitelist_import():
'Add all current inboxes to the whitelist'
for domain in app.database.hostnames:
cli_whitelist_add.callback(domain)
def main(): def main():
cli(prog_name='relay') cli(prog_name='relay')

View file

@ -14,8 +14,6 @@ from json.decoder import JSONDecodeError
from urllib.parse import urlparse from urllib.parse import urlparse
from uuid import uuid4 from uuid import uuid4
from .http_debug import http_debug
app = None app = None

View file

@ -10,6 +10,16 @@ from .misc import Message
cache = LRUCache(1024) cache = LRUCache(1024)
def person_check(actor, software):
## pleroma and akkoma use Person for the actor type for some reason
if software in {'akkoma', 'pleroma'} and actor.id != f'https://{actor.domain}/relay':
return True
## make sure the actor is an application
elif actor.type != 'Application':
return True
async def handle_relay(request): async def handle_relay(request):
if request.message.objectid in cache: if request.message.objectid in cache:
logging.verbose(f'already relayed {request.message.objectid}') logging.verbose(f'already relayed {request.message.objectid}')
@ -50,16 +60,40 @@ async def handle_forward(request):
async def handle_follow(request): async def handle_follow(request):
nodeinfo = await request.app.client.fetch_nodeinfo(request.actor.domain) nodeinfo = await request.app.client.fetch_nodeinfo(request.actor.domain)
software = nodeinfo.swname if nodeinfo else None software = nodeinfo.sw_name if nodeinfo else None
## reject if software used by actor is banned ## reject if software used by actor is banned
if request.config.is_banned_software(software): if request.config.is_banned_software(software):
request.app.push_message(
request.actor.shared_inbox,
Message.new_response(
host = request.config.host,
actor = request.actor.id,
followid = request.message.id,
accept = False
)
)
return logging.verbose(f'Rejected follow from actor for using specific software: actor={request.actor.id}, software={software}') return logging.verbose(f'Rejected follow from actor for using specific software: actor={request.actor.id}, software={software}')
## reject if the actor is not an instance actor
if person_check(request.actor, software):
request.app.push_message(
request.actor.shared_inbox,
Message.new_response(
host = request.config.host,
actor = request.actor.id,
followid = request.message.id,
accept = False
)
)
return logging.verbose(f'Non-application actor tried to follow: {request.actor.id}')
request.database.add_inbox(request.actor.shared_inbox, request.message.id, software) request.database.add_inbox(request.actor.shared_inbox, request.message.id, software)
request.database.save() request.database.save()
await request.app.push_message( request.app.push_message(
request.actor.shared_inbox, request.actor.shared_inbox,
Message.new_response( Message.new_response(
host = request.config.host, host = request.config.host,
@ -72,7 +106,7 @@ async def handle_follow(request):
# Are Akkoma and Pleroma the only two that expect a follow back? # Are Akkoma and Pleroma the only two that expect a follow back?
# Ignoring only Mastodon for now # Ignoring only Mastodon for now
if software != 'mastodon': if software != 'mastodon':
await request.app.push_message( request.app.push_message(
request.actor.shared_inbox, request.actor.shared_inbox,
Message.new_follow( Message.new_follow(
host = request.config.host, host = request.config.host,
@ -91,7 +125,7 @@ async def handle_undo(request):
request.database.save() request.database.save()
await request.app.push_message( request.app.push_message(
request.actor.shared_inbox, request.actor.shared_inbox,
Message.new_unfollow( Message.new_unfollow(
host = request.config.host, host = request.config.host,
@ -119,7 +153,7 @@ async def run_processor(request):
nodeinfo = await request.app.client.fetch_nodeinfo(request.instance['domain']) nodeinfo = await request.app.client.fetch_nodeinfo(request.instance['domain'])
if nodeinfo: if nodeinfo:
request.instance['software'] = nodeinfo.swname request.instance['software'] = nodeinfo.sw_name
request.database.save() request.database.save()
logging.verbose(f'New "{request.message.type}" from actor: {request.actor.id}') logging.verbose(f'New "{request.message.type}" from actor: {request.actor.id}')

View file

@ -7,7 +7,6 @@ import traceback
from pathlib import Path from pathlib import Path
from . import __version__, misc from . import __version__, misc
from .http_debug import STATS
from .misc import DotDict, Message, Response from .misc import DotDict, Message, Response
from .processors import run_processor from .processors import run_processor
@ -190,8 +189,3 @@ async def nodeinfo(request):
async def nodeinfo_wellknown(request): async def nodeinfo_wellknown(request):
data = aputils.WellKnownNodeinfo.new_template(request.config.host) data = aputils.WellKnownNodeinfo.new_template(request.config.host)
return Response.new(data, ctype='json') return Response.new(data, ctype='json')
@register_route('GET', '/stats')
async def stats(request):
return Response.new(STATS, ctype='json')