sedi-relay/relay/views.py

198 lines
5.8 KiB
Python
Raw Normal View History

2022-11-27 03:16:14 +00:00
import aputils
2022-11-20 10:50:14 +00:00
import asyncio
2022-05-06 07:04:51 +00:00
import logging
import subprocess
import traceback
from pathlib import Path
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
from .misc import DotDict, Message, Response
2022-05-06 07:04:51 +00:00
from .processors import run_processor
2022-11-07 12:54:32 +00:00
routes = []
version = __version__
2022-11-07 12:54:32 +00:00
if Path(__file__).parent.parent.joinpath('.git').exists():
try:
commit_label = subprocess.check_output(["git", "rev-parse", "HEAD"]).strip().decode('ascii')
version = f'{__version__} {commit_label}'
2022-11-09 10:58:35 +00:00
except:
pass
2022-11-09 10:58:35 +00:00
2022-11-07 12:54:32 +00:00
def register_route(method, path):
def wrapper(func):
routes.append([method, path, func])
return func
return wrapper
@register_route('GET', '/')
2022-05-06 07:04:51 +00:00
async def home(request):
targets = '<br>'.join(request.database.hostnames)
note = request.config.note
count = len(request.database.hostnames)
host = request.config.host
2022-11-07 12:54:32 +00:00
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
2022-11-09 10:58:35 +00:00
return Response.new(text, ctype='html')
2022-05-06 07:04:51 +00:00
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(
host = request.config.host,
2022-11-27 03:16:14 +00:00
pubkey = request.database.signer.pubkey
2022-11-07 10:30:13 +00:00
)
2022-05-06 07:04:51 +00:00
2022-11-09 10:58:35 +00:00
return Response.new(data, ctype='activity')
2022-05-06 07:04:51 +00:00
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):
config = request.config
database = request.database
2022-05-06 07:04:51 +00:00
## reject if missing signature header
if not request.signature:
2022-05-06 07:04:51 +00:00
logging.verbose('Actor missing signature header')
raise HTTPUnauthorized(body='missing signature')
try:
request['message'] = await request.json(loads=Message.new_from_json)
## reject if there is no message
if not request.message:
logging.verbose('empty message')
return Response.new_error(400, 'missing message', 'json')
2022-11-07 10:30:13 +00:00
## reject if there is no actor in the message
if 'actor' not in request.message:
logging.verbose('actor not in message')
return Response.new_error(400, 'no actor in message', 'json')
2022-05-06 07:04:51 +00:00
except:
## this code should hopefully never get called
2022-05-06 07:04:51 +00:00
traceback.print_exc()
logging.verbose('Failed to parse inbox message')
2022-11-09 10:58:35 +00:00
return Response.new_error(400, 'failed to parse message', 'json')
2022-05-06 07:04:51 +00:00
request['actor'] = await request.app.client.get(request.signature.keyid, sign_headers=True)
2022-05-06 07:04:51 +00:00
## reject if actor is empty
if not request.actor:
2022-11-20 10:12:11 +00:00
## ld signatures aren't handled atm, so just ignore it
2022-11-22 23:09:25 +00:00
if request['message'].type == 'Delete':
2022-11-20 10:12:11 +00:00
logging.verbose(f'Instance sent a delete which cannot be handled')
return Response.new(status=202)
logging.verbose(f'Failed to fetch actor: {request.signature.keyid}')
2022-11-09 10:58:35 +00:00
return Response.new_error(400, 'failed to fetch actor', 'json')
2022-05-06 07:04:51 +00:00
request['instance'] = request.database.get_inbox(request['actor'].inbox)
2022-05-06 07:04:51 +00:00
## reject if the actor isn't whitelisted while the whiltelist is enabled
if config.whitelist_enabled and not config.is_whitelisted(request.actor.domain):
logging.verbose(f'Rejected actor for not being in the whitelist: {request.actor.id}')
2022-11-09 10:58:35 +00:00
return Response.new_error(403, 'access denied', 'json')
2022-05-06 07:04:51 +00:00
## reject if actor is banned
if request.config.is_banned(request.actor.domain):
logging.verbose(f'Ignored request from banned actor: {actor.id}')
2022-11-09 10:58:35 +00:00
return Response.new_error(403, 'access denied', 'json')
2022-05-06 07:04:51 +00:00
## reject if the signature is invalid
2022-11-27 03:16:14 +00:00
try:
await request.actor.signer.validate_aiohttp_request(request)
except aputils.SignatureValidationError as e:
logging.verbose(f'signature validation failed for: {actor.id}')
2022-11-27 03:16:14 +00:00
logging.debug(str(e))
return Response.new_error(401, str(e), 'json')
2022-05-06 07:04:51 +00:00
## reject if activity type isn't 'Follow' and the actor isn't following
if request.message.type != 'Follow' and not database.get_inbox(request.actor.domain):
logging.verbose(f'Rejected actor for trying to post while not following: {request.actor.id}')
2022-11-09 10:58:35 +00:00
return Response.new_error(401, 'access denied', 'json')
2022-05-06 07:04:51 +00:00
logging.debug(f">> payload {request.message.to_json(4)}")
2022-05-06 07:04:51 +00:00
2022-11-20 10:50:14 +00:00
asyncio.ensure_future(run_processor(request))
2022-11-09 10:58:35 +00:00
return Response.new(status=202)
2022-05-06 07:04:51 +00:00
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):
try:
subject = request.query['resource']
except KeyError:
return Response.new_error(400, 'missing \'resource\' query key', 'json')
2022-05-06 07:04:51 +00:00
if subject != f'acct:relay@{request.config.host}':
2022-11-09 10:58:35 +00:00
return Response.new_error(404, 'user not found', 'json')
2022-05-06 07:04:51 +00:00
data = aputils.Webfinger.new(
handle = 'relay',
domain = request.config.host,
actor = request.config.actor
)
2022-05-06 07:04:51 +00:00
2022-11-09 10:58:35 +00:00
return Response.new(data, ctype='json')
2022-05-06 07:04:51 +00:00
@register_route('GET', '/nodeinfo/{version:\d.\d\.json}')
async def nodeinfo(request):
2022-11-09 11:11:16 +00:00
niversion = request.match_info['version'][:3]
data = dict(
name = 'activityrelay',
version = version,
protocols = ['activitypub'],
open_regs = not request.config.whitelist_enabled,
users = 1,
metadata = {'peers': request.database.hostnames}
)
if niversion == '2.1':
data['repo'] = 'https://git.pleroma.social/pleroma/relay'
return Response.new(aputils.Nodeinfo.new(**data), ctype='json')
2022-05-06 07:04:51 +00:00
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 = aputils.WellKnownNodeinfo.new_template(request.config.host)
2022-11-09 10:58:35 +00:00
return Response.new(data, ctype='json')
2022-05-06 07:04:51 +00:00
2022-11-07 12:54:32 +00:00
@register_route('GET', '/stats')
2022-05-06 07:04:51 +00:00
async def stats(request):
2022-11-09 10:58:35 +00:00
return Response.new(STATS, ctype='json')