source/projects/activitypub_relay/src/python/relay/views.py
Reid 'arrdem' McKenzie 70587ac360 Fmt
2022-11-20 22:39:44 -07:00

214 lines
7 KiB
Python

import logging
from aiohttp.web import HTTPUnauthorized
from relay import __version__, misc
from relay.http_debug import STATS
from relay.misc import (
DotDict,
Message,
Response,
WKNodeinfo,
)
from relay.processors import run_processor
routes = []
def register_route(method, path):
def wrapper(func):
routes.append([method, path, func])
return func
return wrapper
@register_route("GET", "/")
async def home(request):
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"""\
<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>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>
</body></html>"""
return Response.new(text, ctype="html")
@register_route("GET", "/inbox")
@register_route("GET", "/actor")
async def actor(request):
data = Message.new_actor(
host=request.app.config.host, pubkey=request.app.database.pubkey
)
return Response.new(data, ctype="activity")
@register_route("POST", "/inbox")
@register_route("POST", "/actor")
async def inbox(request):
config = request.app.config
database = request.app.database
# reject if missing signature header
if "signature" not in request.headers:
logging.debug("Actor missing signature header")
raise HTTPUnauthorized(body="missing signature")
# read message and get actor id and domain
try:
data = await request.json(loads=Message.new_from_json)
if "actor" not in data:
raise KeyError("actor")
# reject if there is no actor in the message
except KeyError:
logging.debug("actor not in data")
return Response.new_error(400, "no actor in message", "json")
except:
logging.exception("Failed to parse inbox message")
return Response.new_error(400, "failed to parse message", "json")
# FIXME: A lot of this code assumes that we're going to have access to the entire actor descriptor.
# This isn't something we actually generally need, and it's not clear how used it is.
# The original relay implementation mostly used to determine activity source domain for relaying.
# This has been refactored out, since there are other sources of this information.
# This PROBABLY means we can do without this data ever ... but here it is.
# Trying to deal with actors/visibility
if isinstance(data.object, dict) and not data.object.get("discoverable", True):
actor = DotDict({"id": "dummy-for-undiscoverable-object"})
# Normal path of looking up the actor...
else:
try:
# FIXME: Needs a cache
actor = await misc.request(data.actorid)
except:
logging.exception(f"{request.id}: {data!r}")
return
logging.debug(f"Inbox >> {data!r}")
# reject if actor is empty
if not actor:
logging.debug(f"Failed to fetch actor: {data.actorid}")
return Response.new_error(400, "failed to fetch actor", "json")
# reject if the actor isn't whitelisted while the whiltelist is enabled
elif config.whitelist_enabled and not config.is_whitelisted(data.domain):
logging.debug(
f"Rejected actor for not being in the whitelist: {data.actorid}"
)
return Response.new_error(403, "access denied", "json")
# reject if actor is banned
if request.app["config"].is_banned(data.domain):
logging.debug(f"Ignored request from banned actor: {data.actorid}")
return Response.new_error(403, "access denied", "json")
# FIXME: Needs a cache
software = await misc.fetch_nodeinfo(data.domain)
# reject if software used by actor is banned
if config.blocked_software:
if config.is_banned_software(software):
logging.debug(f"Rejected actor for using specific software: {software}")
return Response.new_error(403, "access denied", "json")
# reject if the signature is invalid
if not (await misc.validate_signature(data.actorid, request)):
logging.debug(f"signature validation failed for: {data.actorid}")
return Response.new_error(401, "signature check failed", "json")
# reject if activity type isn't 'Follow' and the actor isn't following
if data["type"] != "Follow" and not database.get_inbox(data.domain):
logging.debug(
f"Rejected actor for trying to post while not following: {data.actorid}"
)
return Response.new_error(401, "access denied", "json")
logging.debug(f">> payload {data}")
await run_processor(request, actor, data, software)
return Response.new(status=202)
@register_route("GET", "/.well-known/webfinger")
async def webfinger(request):
subject = request.query["resource"]
if subject != f"acct:relay@{request.app.config.host}":
return Response.new_error(404, "user not found", "json")
data = {
"subject": subject,
"aliases": [request.app.config.actor],
"links": [
{
"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"',
},
],
}
return Response.new(data, ctype="json")
@register_route("GET", "/nodeinfo/{version:\d.\d\.json}")
async def nodeinfo_2_0(request):
niversion = request.match_info["version"][:3]
data = {
"openRegistrations": not request.app.config.whitelist_enabled,
"protocols": ["activitypub"],
"services": {"inbound": [], "outbound": []},
"software": {"name": "activityrelay", "version": __version__},
"usage": {"localPosts": 0, "users": {"total": 1}},
"metadata": {"peers": request.app.database.hostnames},
"version": niversion,
}
if niversion == "2.1":
data["software"]["repository"] = "https://git.pleroma.social/pleroma/relay"
return Response.new(data, ctype="json")
@register_route("GET", "/.well-known/nodeinfo")
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",
)
return Response.new(data, ctype="json")
@register_route("GET", "/stats")
async def stats(*args, **kwargs):
return Response.new(STATS, ctype="json")