sedi-relay/relay/views.py

210 lines
5.8 KiB
Python
Raw Normal View History

2022-05-06 07:04:51 +00:00
import logging
import subprocess
import traceback
2022-11-07 12:54:32 +00:00
from aiohttp.web import HTTPForbidden, HTTPUnauthorized, Response, json_response, route
2022-05-06 07:04:51 +00:00
2022-11-07 12:54:32 +00:00
from . import __version__, misc
2022-05-06 07:04:51 +00:00
from .http_debug import STATS
2022-11-07 10:30:13 +00:00
from .misc import Message
2022-05-06 07:04:51 +00:00
from .processors import run_processor
2022-11-07 12:54:32 +00:00
routes = []
def register_route(method, path):
def wrapper(func):
routes.append([method, path, func])
return func
return wrapper
2022-05-06 07:04:51 +00:00
try:
commit_label = subprocess.check_output(["git", "rev-parse", "HEAD"]).strip().decode('ascii')
2022-06-06 12:37:08 +00:00
version = f'{__version__} {commit_label}'
2022-05-06 07:04:51 +00:00
except:
2022-06-06 12:37:08 +00:00
version = __version__
2022-05-06 07:04:51 +00:00
2022-11-07 12:54:32 +00:00
@register_route('GET', '/')
2022-05-06 07:04:51 +00:00
async def home(request):
2022-11-07 12:54:32 +00:00
targets = '<br>'.join(request.app.database.hostnames)
note = request.app.config.note
count = len(request.app.database.hostnames)
host = request.app.config.host
text = f"""
2022-05-06 07:04:51 +00:00
<html><head>
<title>ActivityPub Relay at {host}</title>
<style>
p {{ color: #FFFFFF; font-family: monospace, arial; font-size: 100%; }}
body {{ background-color: #000000; }}
a {{ color: #26F; }}
a:visited {{ color: #46C; }}
a:hover {{ color: #8AF; }}
</style>
</head>
<body>
<p>This is an Activity Relay for fediverse instances.</p>
<p>{note}</p>
<p>You may subscribe to this relay with the address: <a href="https://{host}/actor">https://{host}/actor</a></p>
<p>To host your own relay, you may download the code at this address: <a href="https://git.pleroma.social/pleroma/relay">https://git.pleroma.social/pleroma/relay</a></p>
<br><p>List of {count} registered instances:<br>{targets}</p>
2022-11-07 12:54:32 +00:00
</body></html>"""
2022-05-06 07:04:51 +00:00
return Response(
status = 200,
content_type = 'text/html',
charset = 'utf-8',
text = text
)
2022-11-07 12:54:32 +00:00
@register_route('GET', '/inbox')
@register_route('GET', '/actor')
2022-05-06 07:04:51 +00:00
async def actor(request):
2022-11-07 10:30:13 +00:00
data = Message.new_actor(
2022-11-07 12:54:32 +00:00
host = request.app.config.host,
pubkey = request.app.database.pubkey
2022-11-07 10:30:13 +00:00
)
2022-05-06 07:04:51 +00:00
return json_response(data, content_type='application/activity+json')
2022-11-07 12:54:32 +00:00
@register_route('POST', '/inbox')
@register_route('POST', '/actor')
2022-05-06 07:04:51 +00:00
async def inbox(request):
2022-11-07 12:54:32 +00:00
config = request.app.config
database = request.app.database
2022-05-06 07:04:51 +00:00
## reject if missing signature header
if 'signature' not in request.headers:
logging.verbose('Actor missing signature header')
raise HTTPUnauthorized(body='missing signature')
## read message and get actor id and domain
try:
2022-11-07 10:30:13 +00:00
data = await request.json(loads=Message.new_from_json)
if 'actor' not in data:
raise KeyError('actor')
2022-05-06 07:04:51 +00:00
## reject if there is no actor in the message
2022-05-06 07:04:51 +00:00
except KeyError:
logging.verbose('actor not in data')
raise HTTPUnauthorized(body='no actor in message')
except:
traceback.print_exc()
logging.verbose('Failed to parse inbox message')
raise HTTPUnauthorized(body='failed to parse message')
2022-11-07 10:30:13 +00:00
actor = await misc.request(data.actorid)
2022-05-06 07:04:51 +00:00
## reject if actor is empty
if not actor:
2022-11-07 10:30:13 +00:00
logging.verbose(f'Failed to fetch actor: {data.actorid}')
2022-05-06 07:04:51 +00:00
raise HTTPUnauthorized('failed to fetch actor')
## reject if the actor isn't whitelisted while the whiltelist is enabled
2022-11-07 10:30:13 +00:00
elif config.whitelist_enabled and not config.is_whitelisted(data.domain):
logging.verbose(f'Rejected actor for not being in the whitelist: {data.actorid}')
2022-05-06 07:04:51 +00:00
raise HTTPForbidden(body='access denied')
## reject if actor is banned
2022-11-07 12:54:32 +00:00
if request.app['config'].is_banned(data.domain):
2022-11-07 10:30:13 +00:00
logging.verbose(f'Ignored request from banned actor: {data.actorid}')
2022-05-06 07:04:51 +00:00
raise HTTPForbidden(body='access denied')
## reject if software used by actor is banned
if len(config.blocked_software):
2022-11-07 10:30:13 +00:00
software = await misc.fetch_nodeinfo(data.domain)
2022-05-06 07:04:51 +00:00
if config.is_banned_software(software):
logging.verbose(f'Rejected actor for using specific software: {software}')
raise HTTPForbidden(body='access denied')
## reject if the signature is invalid
2022-11-07 10:30:13 +00:00
if not (await misc.validate_signature(data.actorid, request)):
logging.verbose(f'signature validation failed for: {data.actorid}')
2022-05-06 07:04:51 +00:00
raise HTTPUnauthorized(body='signature check failed, signature did not match key')
## reject if activity type isn't 'Follow' and the actor isn't following
2022-11-07 10:30:13 +00:00
if data['type'] != 'Follow' and not database.get_inbox(data.domain):
logging.verbose(f'Rejected actor for trying to post while not following: {data.actorid}')
2022-05-06 07:04:51 +00:00
raise HTTPUnauthorized(body='access denied')
logging.debug(f">> payload {data}")
2022-11-07 10:30:13 +00:00
await run_processor(request, actor, data, software)
2022-05-06 07:04:51 +00:00
return Response(body=b'{}', content_type='application/activity+json')
2022-11-07 12:54:32 +00:00
@register_route('GET', '/.well-known/webfinger')
2022-05-06 07:04:51 +00:00
async def webfinger(request):
subject = request.query['resource']
2022-11-07 12:54:32 +00:00
if subject != f'acct:relay@{request.app.config.host}':
2022-05-06 07:04:51 +00:00
return json_response({'error': 'user not found'}, status=404)
data = {
'subject': subject,
2022-11-07 12:54:32 +00:00
'aliases': [request.app.config.actor],
2022-05-06 07:04:51 +00:00
'links': [
2022-11-07 12:54:32 +00:00
{'href': request.app.config.actor, 'rel': 'self', 'type': 'application/activity+json'},
{'href': request.app.config.actor, 'rel': 'self', 'type': 'application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"'}
2022-05-06 07:04:51 +00:00
]
}
return json_response(data)
@register_route('GET', '/nodeinfo/{version:\d.\d\.json}')
2022-05-06 07:04:51 +00:00
async def nodeinfo_2_0(request):
version = request.match_info['version'][:3]
2022-05-06 07:04:51 +00:00
data = {
'openRegistrations': True,
'protocols': ['activitypub'],
'services': {
'inbound': [],
'outbound': []
},
'software': {
'name': 'activityrelay',
2022-06-06 12:37:08 +00:00
'version': version
2022-05-06 07:04:51 +00:00
},
'usage': {
'localPosts': 0,
'users': {
'total': 1
}
},
'metadata': {
2022-11-07 12:54:32 +00:00
'peers': request.app.database.hostnames
2022-05-06 07:04:51 +00:00
},
'version': version
2022-05-06 07:04:51 +00:00
}
if version == '2.1':
data['software']['repository'] = 'https://git.pleroma.social/pleroma/relay'
2022-05-06 07:04:51 +00:00
return json_response(data)
2022-11-07 12:54:32 +00:00
@register_route('GET', '/.well-known/nodeinfo')
2022-05-06 07:04:51 +00:00
async def nodeinfo_wellknown(request):
data = WKNodeinfo.new(
v20 = f'https://{request.app.config.host}/nodeinfo/2.0.json',
v21 = f'https://{request.app.config.host}/nodeinfo/2.1.json'
)
2022-05-06 07:04:51 +00:00
return json_response(data)
2022-11-07 12:54:32 +00:00
@register_route('GET', '/stats')
2022-05-06 07:04:51 +00:00
async def stats(request):
return json_response(STATS)