mirror of
https://git.pleroma.social/pleroma/relay.git
synced 2025-04-19 17:16:42 +00:00
* get actor url from incoming message * fall back to actor inbox if there is no shared inbox
217 lines
5.6 KiB
Python
217 lines
5.6 KiB
Python
from __future__ import annotations
|
|
|
|
import aputils
|
|
import traceback
|
|
|
|
from aiohttp import ClientConnectorError
|
|
from aiohttp.web import Request
|
|
from aputils import Signature, SignatureFailureError, Signer
|
|
from blib import HttpError, HttpMethod
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING
|
|
|
|
from .base import register_route
|
|
|
|
from .. import logger as logging
|
|
from ..database import schema
|
|
from ..misc import Message, Response
|
|
from ..processors import run_processor
|
|
|
|
if TYPE_CHECKING:
|
|
from ..application import Application
|
|
|
|
try:
|
|
from typing import Self
|
|
|
|
except ImportError:
|
|
from typing_extensions import Self
|
|
|
|
|
|
@dataclass(slots = True)
|
|
class InboxData:
|
|
signature: Signature
|
|
message: Message
|
|
actor: Message
|
|
signer: Signer
|
|
instance: schema.Instance | None
|
|
|
|
|
|
@classmethod
|
|
async def parse(cls: type[Self], app: Application, request: Request) -> Self:
|
|
signature: Signature | None = None
|
|
message: Message | None = None
|
|
actor: Message | None = None
|
|
signer: Signer | None = None
|
|
|
|
try:
|
|
signature = Signature.parse(request.headers["signature"])
|
|
|
|
except KeyError:
|
|
logging.verbose("Missing signature header")
|
|
raise HttpError(400, "missing signature header")
|
|
|
|
try:
|
|
message = await request.json(loads = Message.parse)
|
|
|
|
except Exception:
|
|
traceback.print_exc()
|
|
logging.verbose("Failed to parse message from actor: %s", signature.keyid)
|
|
raise HttpError(400, "failed to parse message")
|
|
|
|
if message is None:
|
|
logging.verbose("empty message")
|
|
raise HttpError(400, "missing message")
|
|
|
|
if "actor" not in message:
|
|
logging.verbose("actor not in message")
|
|
raise HttpError(400, "no actor in message")
|
|
|
|
actor_id: str
|
|
|
|
try:
|
|
actor_id = message.actor_id
|
|
|
|
except AttributeError:
|
|
actor_id = signature.keyid
|
|
|
|
try:
|
|
actor = await app.client.get(actor_id, True, Message)
|
|
|
|
except HttpError as e:
|
|
# ld signatures aren"t handled atm, so just ignore it
|
|
if message.type == "Delete":
|
|
logging.verbose("Instance sent a delete which cannot be handled")
|
|
raise HttpError(202, "")
|
|
|
|
logging.verbose("Failed to fetch actor: %s", signature.keyid)
|
|
logging.debug("HTTP Status %i: %s", e.status, e.message)
|
|
raise HttpError(400, "failed to fetch actor")
|
|
|
|
except ClientConnectorError as e:
|
|
logging.warning("Error when trying to fetch actor: %s, %s", signature.keyid, str(e))
|
|
raise HttpError(400, "failed to fetch actor")
|
|
|
|
except Exception:
|
|
traceback.print_exc()
|
|
raise HttpError(500, "unexpected error when fetching actor")
|
|
|
|
try:
|
|
signer = actor.signer
|
|
|
|
except KeyError:
|
|
logging.verbose("Actor missing public key: %s", signature.keyid)
|
|
raise HttpError(400, "actor missing public key")
|
|
|
|
try:
|
|
await signer.validate_request_async(request)
|
|
|
|
except SignatureFailureError as e:
|
|
logging.verbose("signature validation failed for \"%s\": %s", actor.id, e)
|
|
raise HttpError(401, str(e))
|
|
|
|
return cls(signature, message, actor, signer, None)
|
|
|
|
|
|
@property
|
|
def shared_inbox(self) -> str:
|
|
if self.actor is None:
|
|
raise AttributeError("Actor not set yet")
|
|
|
|
try:
|
|
return self.actor.shared_inbox
|
|
|
|
except KeyError:
|
|
return self.actor.inbox # type: ignore[no-any-return]
|
|
|
|
|
|
@register_route(HttpMethod.GET, "/actor", "/inbox")
|
|
async def handle_actor(app: Application, request: Request) -> Response:
|
|
with app.database.session(False) as conn:
|
|
config = conn.get_config_all()
|
|
|
|
data = Message.new_actor(
|
|
host = app.config.domain,
|
|
pubkey = app.signer.pubkey,
|
|
description = app.template.render_markdown(config.note),
|
|
approves = config.approval_required
|
|
)
|
|
|
|
return Response.new(data, ctype = "activity")
|
|
|
|
|
|
@register_route(HttpMethod.POST, "/actor", "/inbox")
|
|
async def handle_inbox(app: Application, request: Request) -> Response:
|
|
data = await InboxData.parse(app, request)
|
|
|
|
with app.database.session() as conn:
|
|
data.instance = conn.get_inbox(data.shared_inbox)
|
|
|
|
# reject if actor is banned
|
|
if conn.get_domain_ban(data.actor.domain):
|
|
logging.verbose("Ignored request from banned actor: %s", data.actor.id)
|
|
raise HttpError(403, "access denied")
|
|
|
|
# reject if activity type isn"t "Follow" and the actor isn"t following
|
|
if data.message.type != "Follow" and not data.instance:
|
|
logging.verbose(
|
|
"Rejected actor for trying to post while not following: %s",
|
|
data.actor.id
|
|
)
|
|
|
|
raise HttpError(401, "access denied")
|
|
|
|
logging.debug(">> payload %s", data.message.to_json(4))
|
|
|
|
await run_processor(data)
|
|
return Response.new(status = 202)
|
|
|
|
|
|
@register_route(HttpMethod.GET, "/outbox")
|
|
async def handle_outbox(app: Application, request: Request) -> Response:
|
|
msg = aputils.Message.new(
|
|
aputils.ObjectType.ORDERED_COLLECTION,
|
|
{
|
|
"id": f"https://{app.config.domain}/outbox",
|
|
"totalItems": 0,
|
|
"orderedItems": []
|
|
}
|
|
)
|
|
|
|
return Response.new(msg, ctype = "activity")
|
|
|
|
|
|
@register_route(HttpMethod.GET, "/following", "/followers")
|
|
async def handle_follow(app: Application, request: Request) -> Response:
|
|
with app.database.session(False) as s:
|
|
inboxes = [row["actor"] for row in s.get_inboxes()]
|
|
|
|
msg = aputils.Message.new(
|
|
aputils.ObjectType.COLLECTION,
|
|
{
|
|
"id": f"https://{app.config.domain}{request.path}",
|
|
"totalItems": len(inboxes),
|
|
"items": inboxes
|
|
}
|
|
)
|
|
|
|
return Response.new(msg, ctype = "activity")
|
|
|
|
|
|
@register_route(HttpMethod.GET, "/.well-known/webfinger")
|
|
async def get(app: Application, request: Request) -> Response:
|
|
try:
|
|
subject = request.query["resource"]
|
|
|
|
except KeyError:
|
|
raise HttpError(400, "missing \"resource\" query key")
|
|
|
|
if subject != f"acct:relay@{app.config.domain}":
|
|
raise HttpError(404, "user not found")
|
|
|
|
data = aputils.Webfinger.new(
|
|
handle = "relay",
|
|
domain = app.config.domain,
|
|
actor = app.config.actor
|
|
)
|
|
|
|
return Response.new(data, ctype = "json")
|