diff --git a/relay.yaml.example b/relay.yaml.example index 1a0da3d..68f0016 100644 --- a/relay.yaml.example +++ b/relay.yaml.example @@ -20,4 +20,11 @@ ap: whitelist_enabled: false whitelist: - 'good-instance.example.com' - - 'another.good-instance.example.com' \ No newline at end of file + - 'another.good-instance.example.com' + # uncomment the lines below to prevent certain activitypub software from posting + # to the relay (all known relays by default). this uses the software name in nodeinfo + #blocked_software: + #- 'activityrelay' + #- 'aoderelay' + #- 'social.seattle.wa.us-relay' + #- 'unciarelay' diff --git a/relay/__init__.py b/relay/__init__.py index 87ab5ef..3785de6 100644 --- a/relay/__init__.py +++ b/relay/__init__.py @@ -9,9 +9,13 @@ import yaml def load_config(): with open('relay.yaml') as f: - yaml_file = yaml.load(f) - whitelist = yaml_file['ap'].get('whitelist', []) - blocked = yaml_file['ap'].get('blocked_instances', []) + options = {} + + ## Prevent a warning message for pyyaml 5.1+ + if getattr(yaml, 'FullLoader', None): + options['Loader'] = yaml.FullLoader + + yaml_file = yaml.load(f, **options) config = { 'db': yaml_file.get('db', 'relay.jsonld'), @@ -19,9 +23,10 @@ def load_config(): 'port': int(yaml_file.get('port', 8080)), 'note': yaml_file.get('note', 'Make a note about your instance here.'), 'ap': { - 'blocked_instances': [] if blocked is None else blocked, + 'blocked_software': [v.lower() for v in yaml_file['ap'].get('blocked_software', [])], + 'blocked_instances': yaml_file['ap'].get('blocked_instances', []), 'host': yaml_file['ap'].get('host', 'localhost'), - 'whitelist': [] if whitelist is None else whitelist, + 'whitelist': yaml_file['ap'].get('whitelist', []), 'whitelist_enabled': yaml_file['ap'].get('whitelist_enabled', False) } } @@ -30,7 +35,6 @@ def load_config(): CONFIG = load_config() - from .http_signatures import http_signatures_middleware diff --git a/relay/actor.py b/relay/actor.py index eb8c51b..aba72ea 100644 --- a/relay/actor.py +++ b/relay/actor.py @@ -103,6 +103,29 @@ async def push_message_to_actor(actor, message, our_key_id): logging.info('Caught %r while pushing to %r.', e, inbox) +async def fetch_nodeinfo(domain): + headers = {'Accept': 'application/json'} + nodeinfo_url = None + + wk_nodeinfo = await fetch_actor(f'https://{domain}/.well-known/nodeinfo', headers=headers) + + if not wk_nodeinfo: + return + + for link in wk_nodeinfo.get('links', ''): + if link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.0': + nodeinfo_url = link['href'] + break + + if not nodeinfo_url: + return + + nodeinfo_data = await fetch_actor(nodeinfo_url, headers=headers) + software = nodeinfo_data.get('software') + + return software.get('name') if software else None + + async def follow_remote_actor(actor_uri): actor = await fetch_actor(actor_uri) @@ -235,6 +258,7 @@ async def handle_follow(actor, data, request): following = DATABASE.get('relay-list', []) inbox = get_actor_inbox(actor) + if urlsplit(inbox).hostname in AP_CONFIG['blocked_instances']: return @@ -294,6 +318,12 @@ async def inbox(request): data = await request.json() instance = urlsplit(data['actor']).hostname + if AP_CONFIG['blocked_software']: + software = await fetch_nodeinfo(instance) + + if software and software.lower() in AP_CONFIG['blocked_software']: + raise aiohttp.web.HTTPUnauthorized(body='relays have been blocked', content_type='text/plain') + if 'actor' not in data or not request['validated']: raise aiohttp.web.HTTPUnauthorized(body='access denied', content_type='text/plain') diff --git a/relay/remote_actor.py b/relay/remote_actor.py index faa0ced..279ada7 100644 --- a/relay/remote_actor.py +++ b/relay/remote_actor.py @@ -12,13 +12,18 @@ CACHE_TTL = CONFIG.get('cache-ttl', 3600) ACTORS = TTLCache(CACHE_SIZE, CACHE_TTL) -async def fetch_actor(uri, force=False): +async def fetch_actor(uri, headers={}, force=False): if uri in ACTORS and not force: return ACTORS[uri] + new_headers = {'Accept': 'application/activity+json'} + + for k,v in headers.items(): + new_headers[k.capitalize()] = v + try: async with aiohttp.ClientSession(trace_configs=[http_debug()]) as session: - async with session.get(uri, headers={'Accept': 'application/activity+json'}) as resp: + async with session.get(uri, headers=new_headers) as resp: if resp.status != 200: return None ACTORS[uri] = (await resp.json(encoding='utf-8', content_type=None)) diff --git a/requirements.txt b/requirements.txt index 6b42e7f..b8053bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ idna==2.7 idna-ssl==1.1.0 multidict==4.3.1 pycryptodome==3.9.4 -PyYAML==3.13 +PyYAML>=5.1 simplejson==3.16.0 yarl==1.2.6 cachetools