mirror of
https://git.pleroma.social/pleroma/relay.git
synced 2024-11-23 23:17:58 +00:00
Compare commits
7 commits
f7e1c6b0b8
...
7d37ec8145
Author | SHA1 | Date | |
---|---|---|---|
7d37ec8145 | |||
9f58c88e9f | |||
6b86bb7d98 | |||
90234a9724 | |||
b0851c0652 | |||
eab8a31001 | |||
3b89aa5e84 |
|
@ -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.
|
||||||
|
|
|
@ -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))
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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]
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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}')
|
||||||
|
|
|
@ -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')
|
|
||||||
|
|
Loading…
Reference in a new issue