Mercurial > libervia-backend
diff libervia/backend/plugins/plugin_comp_ap_gateway/http_server.py @ 4259:49019947cc76
component AP Gateway: implement HTTP GET signature.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 05 Jun 2024 22:34:09 +0200 |
parents | 5f2d496c633f |
children | 0d7bb4df2343 |
line wrap: on
line diff
--- a/libervia/backend/plugins/plugin_comp_ap_gateway/http_server.py Wed Jun 05 22:33:37 2024 +0200 +++ b/libervia/backend/plugins/plugin_comp_ap_gateway/http_server.py Wed Jun 05 22:34:09 2024 +0200 @@ -127,6 +127,7 @@ async def handle_undo_activity( self, + requestor_actor_id: str, request: "HTTPRequest", data: dict, account_jid: jid.JID, @@ -137,7 +138,7 @@ ) -> None: if node is None: node = self.apg._m.namespace - client = await self.apg.get_virtual_client(signing_actor) + client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) object_ = data.get("object") if isinstance(object_, str): # we check first if it's not a cached object @@ -150,10 +151,10 @@ # because we'll undo the activity, we can remove it from cache await self.apg.client._ap_storage.remove(ap_cache_key) else: - objects = await self.apg.ap_get_list(data, "object") + objects = await self.apg.ap_get_list(requestor_actor_id, data, "object") for obj in objects: type_ = obj.get("type") - actor = await self.apg.ap_get_sender_actor(obj) + actor = await self.apg.ap_get_sender_actor(requestor_actor_id, obj) if actor != signing_actor: log.warning(f"ignoring object not attributed to signing actor: {data}") continue @@ -188,6 +189,7 @@ async def handle_follow_activity( self, + requestor_actor_id: str, request: "HTTPRequest", data: dict, account_jid: jid.JID, @@ -198,7 +200,7 @@ ) -> None: if node is None: node = self.apg._m.namespace - client = await self.apg.get_virtual_client(signing_actor) + client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) try: subscription = await self.apg._p.subscribe( client, @@ -224,6 +226,7 @@ async def handle_accept_activity( self, + requestor_actor_id: str, request: "HTTPRequest", data: dict, account_jid: jid.JID, @@ -234,8 +237,8 @@ ) -> None: if node is None: node = self.apg._m.namespace - client = await self.apg.get_virtual_client(signing_actor) - objects = await self.apg.ap_get_list(data, "object") + client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) + objects = await self.apg.ap_get_list(requestor_actor_id, data, "object") for obj in objects: type_ = obj.get("type") if type_ == "Follow": @@ -273,6 +276,7 @@ async def handle_delete_activity( self, + requestor_actor_id: str, request: "HTTPRequest", data: dict, account_jid: Optional[jid.JID], @@ -283,13 +287,14 @@ ): if node is None: node = self.apg._m.namespace - client = await self.apg.get_virtual_client(signing_actor) - objects = await self.apg.ap_get_list(data, "object") + client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) + objects = await self.apg.ap_get_list(requestor_actor_id, data, "object") for obj in objects: await self.apg.new_ap_delete_item(client, account_jid, node, obj) async def handle_new_ap_items( self, + requestor_actor_id: str, request: "HTTPRequest", data: dict, account_jid: Optional[jid.JID], @@ -308,20 +313,26 @@ f"happen. Ignoring object from {signing_actor}\n{data}" ) raise exceptions.DataError("unexpected field in item") - client = await self.apg.get_virtual_client(signing_actor) - objects = await self.apg.ap_get_list(data, "object") + client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) + objects = await self.apg.ap_get_list(requestor_actor_id, data, "object") for obj in objects: if node is None: if obj.get("type") == TYPE_EVENT: node = self.apg._events.namespace else: node = self.apg._m.namespace - sender = await self.apg.ap_get_sender_actor(obj) + sender = await self.apg.ap_get_sender_actor(requestor_actor_id, obj) if repeated: # we don't check sender when item is repeated, as it should be different # from post author in this case - sender_jid = await self.apg.get_jid_from_id(sender) - repeater_jid = await self.apg.get_jid_from_id(signing_actor) + sender_jid = await self.apg.get_jid_from_id( + requestor_actor_id, + sender + ) + repeater_jid = await self.apg.get_jid_from_id( + requestor_actor_id, + signing_actor + ) repeated_item_id = obj["id"] if self.apg.is_local_url(repeated_item_id): # the repeated object is from XMPP, we need to parse the URL to find @@ -374,6 +385,7 @@ async def handle_create_activity( self, + requestor_actor_id: str, request: "HTTPRequest", data: dict, account_jid: Optional[jid.JID], @@ -382,10 +394,13 @@ ap_url: str, signing_actor: str ): - await self.handle_new_ap_items(request, data, account_jid, node, signing_actor) + await self.handle_new_ap_items( + requestor_actor_id, request, data, account_jid, node, signing_actor + ) async def handle_update_activity( self, + requestor_actor_id: str, request: "HTTPRequest", data: dict, account_jid: Optional[jid.JID], @@ -396,10 +411,13 @@ ): # Update is the same as create: the item ID stays the same, thus the item will be # overwritten - await self.handle_new_ap_items(request, data, account_jid, node, signing_actor) + await self.handle_new_ap_items( + requestor_actor_id, request, data, account_jid, node, signing_actor + ) async def handle_announce_activity( self, + requestor_actor_id: str, request: "HTTPRequest", data: dict, account_jid: Optional[jid.JID], @@ -410,6 +428,7 @@ ): # we create a new item await self.handle_new_ap_items( + requestor_actor_id, request, data, account_jid, @@ -516,6 +535,7 @@ async def handle_like_activity( self, + requestor_actor_id: str, request: "HTTPRequest", data: dict, account_jid: Optional[jid.JID], @@ -524,11 +544,12 @@ ap_url: str, signing_actor: str ) -> None: - client = await self.apg.get_virtual_client(signing_actor) + client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) await self.handle_attachment_item(client, data, {"noticed": True}) async def handle_emojireact_activity( self, + requestor_actor_id: str, request: "HTTPRequest", data: dict, account_jid: Optional[jid.JID], @@ -537,13 +558,14 @@ ap_url: str, signing_actor: str ) -> None: - client = await self.apg.get_virtual_client(signing_actor) + client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) await self.handle_attachment_item(client, data, { "reactions": {"operation": "update", "add": [data["content"]]} }) async def handle_join_activity( self, + requestor_actor_id: str, request: "HTTPRequest", data: dict, account_jid: Optional[jid.JID], @@ -552,11 +574,12 @@ ap_url: str, signing_actor: str ) -> None: - client = await self.apg.get_virtual_client(signing_actor) + client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) await self.handle_attachment_item(client, data, {"rsvp": {"attending": "yes"}}) async def handle_leave_activity( self, + requestor_actor_id: str, request: "HTTPRequest", data: dict, account_jid: Optional[jid.JID], @@ -565,7 +588,7 @@ ap_url: str, signing_actor: str ) -> None: - client = await self.apg.get_virtual_client(signing_actor) + client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) await self.handle_attachment_item(client, data, {"rsvp": {"attending": "no"}}) async def ap_actor_request( @@ -814,6 +837,7 @@ async def ap_inbox_request( self, + requestor_actor_id: str, request: "HTTPRequest", data: Optional[dict], account_jid: Optional[jid.JID], @@ -825,7 +849,7 @@ assert data is not None if signing_actor is None: raise exceptions.InternalError("signing_actor must be set for inbox requests") - await self.check_signing_actor(data, signing_actor) + await self.check_signing_actor(requestor_actor_id, data, signing_actor) activity_type = (data.get("type") or "").lower() if not activity_type in ACTIVITY_TYPES_LOWER: return self.response_code( @@ -851,7 +875,8 @@ ) else: await method( - request, data, account_jid, node, ap_account, ap_url, signing_actor + requestor_actor_id, request, data, account_jid, node, ap_account, ap_url, + signing_actor ) async def ap_followers_request( @@ -954,11 +979,73 @@ to_log.append(f" headers:\n{headers}") return to_log + def get_requestor_actor_id( + self, + data: dict|None = None, + uri_extra_args: list[str]|None = None + ) -> str: + """Find the actor ID of the requestor. + + The requestor here is actually the local actor which will do the requests to + achieve the task (e.g. retrieve external actor data), not the requestor of the + received AP request. + + It will notably be used as requestor actor ID to sign HTTP requests. We need to + sign GET request too to access instance checking HTTP GET signature (e.g. Mastodon + instances set in "secure mode"). + + We look for the destinee of the request and check if it's a local actor, and + default to a generic one if we can't find it. + + Destinee is first checked in data if any, otherwise in request URI. + + @param data: parsed JSON data of original AP request, if any. + @param uri_extra_args: arguments of the AP request as returned by + [self.apg.parse_apurl]. It is most of time the destinee of the request. + @return: requestor_actor_id to use to sign HTTP request needed to answer the + original request. + """ + # We first check for destinee in data. + if data: + try: + for to_ in data["to"]: + if self.apg.is_local_url(to_): + url_type, url_args = self.apg.parse_apurl(to_) + if url_type != TYPE_ACTOR or not url_args: + continue + ap_account = url_args[0] + if ( + not ap_account.endswith(f"@{self.apg.public_url}") + or ap_account.count("@") != 1 + ): + continue + return to_ + except KeyError: + pass + + # If nothing relevant, we try URI arguments. + if uri_extra_args: + ap_account = uri_extra_args[0] + if ( + ap_account.endswith(f"@{self.apg.public_url}") + and ap_account.count("@") == 1 + ): + return self.apg.build_apurl(TYPE_ACTOR, ap_account) + + # Still nothing, we'll have to use a generic actor. + log.warning( + "Can't find destinee in \"to\" field, using generic requestor for signature." + ) + return self.apg.build_apurl( + TYPE_ACTOR, f"libervia@{self.apg.public_url}" + ) + async def ap_request( self, request: "HTTPRequest", - data: Optional[dict] = None, - signing_actor: Optional[str] = None + data: dict|None = None, + signing_actor: str|None = None, + requestor_actor_id: str|None = None, ) -> None: if self.apg.verbose: to_log = self._get_to_log(request, data) @@ -969,6 +1056,7 @@ path ) request_type, extra_args = self.apg.parse_apurl(ap_url) + header_accept = request.getHeader("accept") or "" if ((MEDIA_TYPE_AP not in header_accept and MEDIA_TYPE_AP_ALT not in header_accept @@ -1007,11 +1095,15 @@ request.finish() return + if requestor_actor_id is None: + requestor_actor_id = self.get_requestor_actor_id( + data, extra_args + ) if len(extra_args) == 0: if request_type != "shared_inbox": raise exceptions.DataError(f"Invalid request type: {request_type!r}") ret_data = await self.ap_inbox_request( - request, data, None, None, None, ap_url, signing_actor + requestor_actor_id, request, data, None, None, None, ap_url, signing_actor ) elif request_type == "avatar": if len(extra_args) != 1: @@ -1034,7 +1126,7 @@ raise exceptions.DataError(f"Invalid request type: {request_type!r}") method = getattr(self, f"ap_{request_type}_request") ret_data = await method( - request, data, account_jid, node, ap_account, ap_url, signing_actor + requestor_actor_id, request, data, account_jid, node, ap_account, ap_url, signing_actor ) if ret_data is not None: request.setHeader("content-type", CONTENT_TYPE_AP) @@ -1072,6 +1164,8 @@ else: request.content.seek(0) + requestor_actor_id = self.get_requestor_actor_id(data) + try: if data["type"] == "Delete" and data["actor"] == data["object"]: # we don't handle actor deletion @@ -1084,7 +1178,7 @@ pass try: - signing_actor = await self.check_signature(request) + signing_actor = await self.check_signature(requestor_actor_id, request) except exceptions.EncryptionError as e: if self.apg.verbose: to_log = self._get_to_log(request) @@ -1118,29 +1212,42 @@ # default response code, may be changed, e.g. in case of exception try: - return await self.ap_request(request, data, signing_actor) + return await self.ap_request( + request, data, signing_actor, requestor_actor_id=requestor_actor_id + ) except Exception as e: self._on_request_error(failure.Failure(e), request) - async def check_signing_actor(self, data: dict, signing_actor: str) -> None: + async def check_signing_actor( + self, + requestor_actor_id: str, + data: dict, + signing_actor: str + ) -> None: """That that signing actor correspond to actor declared in data + @param requestor_actor_id: ID of the actor doing the request. @param data: request payload @param signing_actor: actor ID of the signing entity, as returned by check_signature @raise exceptions.NotFound: no actor found in data @raise exceptions.EncryptionError: signing actor doesn't match actor in data """ - actor = await self.apg.ap_get_sender_actor(data) + actor = await self.apg.ap_get_sender_actor(requestor_actor_id, data) if signing_actor != actor: raise exceptions.EncryptionError( f"signing actor ({signing_actor}) doesn't match actor in data ({actor})" ) - async def check_signature(self, request: "HTTPRequest") -> str: + async def check_signature( + self, + requestor_actor_id: str, + request: "HTTPRequest" + ) -> str: """Check and validate HTTP signature + @param requestor_actor_id: ID of the actor doing the request. @return: id of the signing actor @raise exceptions.EncryptionError: signature is not present or doesn't match @@ -1279,6 +1386,7 @@ try: return await self.apg.check_signature( + requestor_actor_id, sign_data["signature"], key_id, headers @@ -1291,6 +1399,7 @@ "see https://github.com/mastodon/mastodon/issues/18871" ) return await self.apg.check_signature( + requestor_actor_id, sign_data["signature"], key_id, headers