Mercurial > libervia-backend
comparison 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 |
comparison
equal
deleted
inserted
replaced
4258:ba28ca268f4a | 4259:49019947cc76 |
---|---|
125 request.write(json.dumps(resp).encode()) | 125 request.write(json.dumps(resp).encode()) |
126 request.finish() | 126 request.finish() |
127 | 127 |
128 async def handle_undo_activity( | 128 async def handle_undo_activity( |
129 self, | 129 self, |
130 requestor_actor_id: str, | |
130 request: "HTTPRequest", | 131 request: "HTTPRequest", |
131 data: dict, | 132 data: dict, |
132 account_jid: jid.JID, | 133 account_jid: jid.JID, |
133 node: Optional[str], | 134 node: Optional[str], |
134 ap_account: str, | 135 ap_account: str, |
135 ap_url: str, | 136 ap_url: str, |
136 signing_actor: str | 137 signing_actor: str |
137 ) -> None: | 138 ) -> None: |
138 if node is None: | 139 if node is None: |
139 node = self.apg._m.namespace | 140 node = self.apg._m.namespace |
140 client = await self.apg.get_virtual_client(signing_actor) | 141 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) |
141 object_ = data.get("object") | 142 object_ = data.get("object") |
142 if isinstance(object_, str): | 143 if isinstance(object_, str): |
143 # we check first if it's not a cached object | 144 # we check first if it's not a cached object |
144 ap_cache_key = f"{ST_AP_CACHE}{object_}" | 145 ap_cache_key = f"{ST_AP_CACHE}{object_}" |
145 value = await self.apg.client._ap_storage.get(ap_cache_key) | 146 value = await self.apg.client._ap_storage.get(ap_cache_key) |
148 if value is not None: | 149 if value is not None: |
149 objects = [value] | 150 objects = [value] |
150 # because we'll undo the activity, we can remove it from cache | 151 # because we'll undo the activity, we can remove it from cache |
151 await self.apg.client._ap_storage.remove(ap_cache_key) | 152 await self.apg.client._ap_storage.remove(ap_cache_key) |
152 else: | 153 else: |
153 objects = await self.apg.ap_get_list(data, "object") | 154 objects = await self.apg.ap_get_list(requestor_actor_id, data, "object") |
154 for obj in objects: | 155 for obj in objects: |
155 type_ = obj.get("type") | 156 type_ = obj.get("type") |
156 actor = await self.apg.ap_get_sender_actor(obj) | 157 actor = await self.apg.ap_get_sender_actor(requestor_actor_id, obj) |
157 if actor != signing_actor: | 158 if actor != signing_actor: |
158 log.warning(f"ignoring object not attributed to signing actor: {data}") | 159 log.warning(f"ignoring object not attributed to signing actor: {data}") |
159 continue | 160 continue |
160 | 161 |
161 if type_ == "Follow": | 162 if type_ == "Follow": |
186 else: | 187 else: |
187 log.warning(f"Unmanaged undo type: {type_!r}") | 188 log.warning(f"Unmanaged undo type: {type_!r}") |
188 | 189 |
189 async def handle_follow_activity( | 190 async def handle_follow_activity( |
190 self, | 191 self, |
192 requestor_actor_id: str, | |
191 request: "HTTPRequest", | 193 request: "HTTPRequest", |
192 data: dict, | 194 data: dict, |
193 account_jid: jid.JID, | 195 account_jid: jid.JID, |
194 node: Optional[str], | 196 node: Optional[str], |
195 ap_account: str, | 197 ap_account: str, |
196 ap_url: str, | 198 ap_url: str, |
197 signing_actor: str | 199 signing_actor: str |
198 ) -> None: | 200 ) -> None: |
199 if node is None: | 201 if node is None: |
200 node = self.apg._m.namespace | 202 node = self.apg._m.namespace |
201 client = await self.apg.get_virtual_client(signing_actor) | 203 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) |
202 try: | 204 try: |
203 subscription = await self.apg._p.subscribe( | 205 subscription = await self.apg._p.subscribe( |
204 client, | 206 client, |
205 account_jid, | 207 account_jid, |
206 node, | 208 node, |
222 await self.apg.sign_and_post(inbox, actor_id, accept_data) | 224 await self.apg.sign_and_post(inbox, actor_id, accept_data) |
223 await self.apg._c.synchronise(client, account_jid, node, resync=False) | 225 await self.apg._c.synchronise(client, account_jid, node, resync=False) |
224 | 226 |
225 async def handle_accept_activity( | 227 async def handle_accept_activity( |
226 self, | 228 self, |
229 requestor_actor_id: str, | |
227 request: "HTTPRequest", | 230 request: "HTTPRequest", |
228 data: dict, | 231 data: dict, |
229 account_jid: jid.JID, | 232 account_jid: jid.JID, |
230 node: Optional[str], | 233 node: Optional[str], |
231 ap_account: str, | 234 ap_account: str, |
232 ap_url: str, | 235 ap_url: str, |
233 signing_actor: str | 236 signing_actor: str |
234 ) -> None: | 237 ) -> None: |
235 if node is None: | 238 if node is None: |
236 node = self.apg._m.namespace | 239 node = self.apg._m.namespace |
237 client = await self.apg.get_virtual_client(signing_actor) | 240 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) |
238 objects = await self.apg.ap_get_list(data, "object") | 241 objects = await self.apg.ap_get_list(requestor_actor_id, data, "object") |
239 for obj in objects: | 242 for obj in objects: |
240 type_ = obj.get("type") | 243 type_ = obj.get("type") |
241 if type_ == "Follow": | 244 if type_ == "Follow": |
242 follow_node = await self.apg.host.memory.storage.get_pubsub_node( | 245 follow_node = await self.apg.host.memory.storage.get_pubsub_node( |
243 client, client.jid, node, with_subscriptions=True | 246 client, client.jid, node, with_subscriptions=True |
271 else: | 274 else: |
272 log.warning(f"Unmanaged accept type: {type_!r}") | 275 log.warning(f"Unmanaged accept type: {type_!r}") |
273 | 276 |
274 async def handle_delete_activity( | 277 async def handle_delete_activity( |
275 self, | 278 self, |
279 requestor_actor_id: str, | |
276 request: "HTTPRequest", | 280 request: "HTTPRequest", |
277 data: dict, | 281 data: dict, |
278 account_jid: Optional[jid.JID], | 282 account_jid: Optional[jid.JID], |
279 node: Optional[str], | 283 node: Optional[str], |
280 ap_account: Optional[str], | 284 ap_account: Optional[str], |
281 ap_url: str, | 285 ap_url: str, |
282 signing_actor: str | 286 signing_actor: str |
283 ): | 287 ): |
284 if node is None: | 288 if node is None: |
285 node = self.apg._m.namespace | 289 node = self.apg._m.namespace |
286 client = await self.apg.get_virtual_client(signing_actor) | 290 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) |
287 objects = await self.apg.ap_get_list(data, "object") | 291 objects = await self.apg.ap_get_list(requestor_actor_id, data, "object") |
288 for obj in objects: | 292 for obj in objects: |
289 await self.apg.new_ap_delete_item(client, account_jid, node, obj) | 293 await self.apg.new_ap_delete_item(client, account_jid, node, obj) |
290 | 294 |
291 async def handle_new_ap_items( | 295 async def handle_new_ap_items( |
292 self, | 296 self, |
297 requestor_actor_id: str, | |
293 request: "HTTPRequest", | 298 request: "HTTPRequest", |
294 data: dict, | 299 data: dict, |
295 account_jid: Optional[jid.JID], | 300 account_jid: Optional[jid.JID], |
296 node: Optional[str], | 301 node: Optional[str], |
297 signing_actor: str, | 302 signing_actor: str, |
306 log.error( | 311 log.error( |
307 '"_repeated" field already present in given AP item, this should not ' | 312 '"_repeated" field already present in given AP item, this should not ' |
308 f"happen. Ignoring object from {signing_actor}\n{data}" | 313 f"happen. Ignoring object from {signing_actor}\n{data}" |
309 ) | 314 ) |
310 raise exceptions.DataError("unexpected field in item") | 315 raise exceptions.DataError("unexpected field in item") |
311 client = await self.apg.get_virtual_client(signing_actor) | 316 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) |
312 objects = await self.apg.ap_get_list(data, "object") | 317 objects = await self.apg.ap_get_list(requestor_actor_id, data, "object") |
313 for obj in objects: | 318 for obj in objects: |
314 if node is None: | 319 if node is None: |
315 if obj.get("type") == TYPE_EVENT: | 320 if obj.get("type") == TYPE_EVENT: |
316 node = self.apg._events.namespace | 321 node = self.apg._events.namespace |
317 else: | 322 else: |
318 node = self.apg._m.namespace | 323 node = self.apg._m.namespace |
319 sender = await self.apg.ap_get_sender_actor(obj) | 324 sender = await self.apg.ap_get_sender_actor(requestor_actor_id, obj) |
320 if repeated: | 325 if repeated: |
321 # we don't check sender when item is repeated, as it should be different | 326 # we don't check sender when item is repeated, as it should be different |
322 # from post author in this case | 327 # from post author in this case |
323 sender_jid = await self.apg.get_jid_from_id(sender) | 328 sender_jid = await self.apg.get_jid_from_id( |
324 repeater_jid = await self.apg.get_jid_from_id(signing_actor) | 329 requestor_actor_id, |
330 sender | |
331 ) | |
332 repeater_jid = await self.apg.get_jid_from_id( | |
333 requestor_actor_id, | |
334 signing_actor | |
335 ) | |
325 repeated_item_id = obj["id"] | 336 repeated_item_id = obj["id"] |
326 if self.apg.is_local_url(repeated_item_id): | 337 if self.apg.is_local_url(repeated_item_id): |
327 # the repeated object is from XMPP, we need to parse the URL to find | 338 # the repeated object is from XMPP, we need to parse the URL to find |
328 # the right ID | 339 # the right ID |
329 url_type, url_args = self.apg.parse_apurl(repeated_item_id) | 340 url_type, url_args = self.apg.parse_apurl(repeated_item_id) |
372 | 383 |
373 await self.apg.new_ap_item(client, account_jid, node, obj) | 384 await self.apg.new_ap_item(client, account_jid, node, obj) |
374 | 385 |
375 async def handle_create_activity( | 386 async def handle_create_activity( |
376 self, | 387 self, |
388 requestor_actor_id: str, | |
377 request: "HTTPRequest", | 389 request: "HTTPRequest", |
378 data: dict, | 390 data: dict, |
379 account_jid: Optional[jid.JID], | 391 account_jid: Optional[jid.JID], |
380 node: Optional[str], | 392 node: Optional[str], |
381 ap_account: Optional[str], | 393 ap_account: Optional[str], |
382 ap_url: str, | 394 ap_url: str, |
383 signing_actor: str | 395 signing_actor: str |
384 ): | 396 ): |
385 await self.handle_new_ap_items(request, data, account_jid, node, signing_actor) | 397 await self.handle_new_ap_items( |
398 requestor_actor_id, request, data, account_jid, node, signing_actor | |
399 ) | |
386 | 400 |
387 async def handle_update_activity( | 401 async def handle_update_activity( |
388 self, | 402 self, |
403 requestor_actor_id: str, | |
389 request: "HTTPRequest", | 404 request: "HTTPRequest", |
390 data: dict, | 405 data: dict, |
391 account_jid: Optional[jid.JID], | 406 account_jid: Optional[jid.JID], |
392 node: Optional[str], | 407 node: Optional[str], |
393 ap_account: Optional[str], | 408 ap_account: Optional[str], |
394 ap_url: str, | 409 ap_url: str, |
395 signing_actor: str | 410 signing_actor: str |
396 ): | 411 ): |
397 # Update is the same as create: the item ID stays the same, thus the item will be | 412 # Update is the same as create: the item ID stays the same, thus the item will be |
398 # overwritten | 413 # overwritten |
399 await self.handle_new_ap_items(request, data, account_jid, node, signing_actor) | 414 await self.handle_new_ap_items( |
415 requestor_actor_id, request, data, account_jid, node, signing_actor | |
416 ) | |
400 | 417 |
401 async def handle_announce_activity( | 418 async def handle_announce_activity( |
402 self, | 419 self, |
420 requestor_actor_id: str, | |
403 request: "HTTPRequest", | 421 request: "HTTPRequest", |
404 data: dict, | 422 data: dict, |
405 account_jid: Optional[jid.JID], | 423 account_jid: Optional[jid.JID], |
406 node: Optional[str], | 424 node: Optional[str], |
407 ap_account: Optional[str], | 425 ap_account: Optional[str], |
408 ap_url: str, | 426 ap_url: str, |
409 signing_actor: str | 427 signing_actor: str |
410 ): | 428 ): |
411 # we create a new item | 429 # we create a new item |
412 await self.handle_new_ap_items( | 430 await self.handle_new_ap_items( |
431 requestor_actor_id, | |
413 request, | 432 request, |
414 data, | 433 data, |
415 account_jid, | 434 account_jid, |
416 node, | 435 node, |
417 signing_actor, | 436 signing_actor, |
514 client, author_jid, attachment_node, [item_elt] | 533 client, author_jid, attachment_node, [item_elt] |
515 ) | 534 ) |
516 | 535 |
517 async def handle_like_activity( | 536 async def handle_like_activity( |
518 self, | 537 self, |
538 requestor_actor_id: str, | |
519 request: "HTTPRequest", | 539 request: "HTTPRequest", |
520 data: dict, | 540 data: dict, |
521 account_jid: Optional[jid.JID], | 541 account_jid: Optional[jid.JID], |
522 node: Optional[str], | 542 node: Optional[str], |
523 ap_account: Optional[str], | 543 ap_account: Optional[str], |
524 ap_url: str, | 544 ap_url: str, |
525 signing_actor: str | 545 signing_actor: str |
526 ) -> None: | 546 ) -> None: |
527 client = await self.apg.get_virtual_client(signing_actor) | 547 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) |
528 await self.handle_attachment_item(client, data, {"noticed": True}) | 548 await self.handle_attachment_item(client, data, {"noticed": True}) |
529 | 549 |
530 async def handle_emojireact_activity( | 550 async def handle_emojireact_activity( |
531 self, | 551 self, |
552 requestor_actor_id: str, | |
532 request: "HTTPRequest", | 553 request: "HTTPRequest", |
533 data: dict, | 554 data: dict, |
534 account_jid: Optional[jid.JID], | 555 account_jid: Optional[jid.JID], |
535 node: Optional[str], | 556 node: Optional[str], |
536 ap_account: Optional[str], | 557 ap_account: Optional[str], |
537 ap_url: str, | 558 ap_url: str, |
538 signing_actor: str | 559 signing_actor: str |
539 ) -> None: | 560 ) -> None: |
540 client = await self.apg.get_virtual_client(signing_actor) | 561 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) |
541 await self.handle_attachment_item(client, data, { | 562 await self.handle_attachment_item(client, data, { |
542 "reactions": {"operation": "update", "add": [data["content"]]} | 563 "reactions": {"operation": "update", "add": [data["content"]]} |
543 }) | 564 }) |
544 | 565 |
545 async def handle_join_activity( | 566 async def handle_join_activity( |
546 self, | 567 self, |
568 requestor_actor_id: str, | |
547 request: "HTTPRequest", | 569 request: "HTTPRequest", |
548 data: dict, | 570 data: dict, |
549 account_jid: Optional[jid.JID], | 571 account_jid: Optional[jid.JID], |
550 node: Optional[str], | 572 node: Optional[str], |
551 ap_account: Optional[str], | 573 ap_account: Optional[str], |
552 ap_url: str, | 574 ap_url: str, |
553 signing_actor: str | 575 signing_actor: str |
554 ) -> None: | 576 ) -> None: |
555 client = await self.apg.get_virtual_client(signing_actor) | 577 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) |
556 await self.handle_attachment_item(client, data, {"rsvp": {"attending": "yes"}}) | 578 await self.handle_attachment_item(client, data, {"rsvp": {"attending": "yes"}}) |
557 | 579 |
558 async def handle_leave_activity( | 580 async def handle_leave_activity( |
559 self, | 581 self, |
582 requestor_actor_id: str, | |
560 request: "HTTPRequest", | 583 request: "HTTPRequest", |
561 data: dict, | 584 data: dict, |
562 account_jid: Optional[jid.JID], | 585 account_jid: Optional[jid.JID], |
563 node: Optional[str], | 586 node: Optional[str], |
564 ap_account: Optional[str], | 587 ap_account: Optional[str], |
565 ap_url: str, | 588 ap_url: str, |
566 signing_actor: str | 589 signing_actor: str |
567 ) -> None: | 590 ) -> None: |
568 client = await self.apg.get_virtual_client(signing_actor) | 591 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) |
569 await self.handle_attachment_item(client, data, {"rsvp": {"attending": "no"}}) | 592 await self.handle_attachment_item(client, data, {"rsvp": {"attending": "no"}}) |
570 | 593 |
571 async def ap_actor_request( | 594 async def ap_actor_request( |
572 self, | 595 self, |
573 request: "HTTPRequest", | 596 request: "HTTPRequest", |
812 "last": url_last_page, | 835 "last": url_last_page, |
813 } | 836 } |
814 | 837 |
815 async def ap_inbox_request( | 838 async def ap_inbox_request( |
816 self, | 839 self, |
840 requestor_actor_id: str, | |
817 request: "HTTPRequest", | 841 request: "HTTPRequest", |
818 data: Optional[dict], | 842 data: Optional[dict], |
819 account_jid: Optional[jid.JID], | 843 account_jid: Optional[jid.JID], |
820 node: Optional[str], | 844 node: Optional[str], |
821 ap_account: Optional[str], | 845 ap_account: Optional[str], |
823 signing_actor: Optional[str] | 847 signing_actor: Optional[str] |
824 ) -> None: | 848 ) -> None: |
825 assert data is not None | 849 assert data is not None |
826 if signing_actor is None: | 850 if signing_actor is None: |
827 raise exceptions.InternalError("signing_actor must be set for inbox requests") | 851 raise exceptions.InternalError("signing_actor must be set for inbox requests") |
828 await self.check_signing_actor(data, signing_actor) | 852 await self.check_signing_actor(requestor_actor_id, data, signing_actor) |
829 activity_type = (data.get("type") or "").lower() | 853 activity_type = (data.get("type") or "").lower() |
830 if not activity_type in ACTIVITY_TYPES_LOWER: | 854 if not activity_type in ACTIVITY_TYPES_LOWER: |
831 return self.response_code( | 855 return self.response_code( |
832 request, | 856 request, |
833 http.UNSUPPORTED_MEDIA_TYPE, | 857 http.UNSUPPORTED_MEDIA_TYPE, |
849 http.UNSUPPORTED_MEDIA_TYPE, | 873 http.UNSUPPORTED_MEDIA_TYPE, |
850 f"{activity_type.title()} activity is not yet supported" | 874 f"{activity_type.title()} activity is not yet supported" |
851 ) | 875 ) |
852 else: | 876 else: |
853 await method( | 877 await method( |
854 request, data, account_jid, node, ap_account, ap_url, signing_actor | 878 requestor_actor_id, request, data, account_jid, node, ap_account, ap_url, |
879 signing_actor | |
855 ) | 880 ) |
856 | 881 |
857 async def ap_followers_request( | 882 async def ap_followers_request( |
858 self, | 883 self, |
859 request: "HTTPRequest", | 884 request: "HTTPRequest", |
952 for k,v in request.getAllHeaders().items() | 977 for k,v in request.getAllHeaders().items() |
953 ) | 978 ) |
954 to_log.append(f" headers:\n{headers}") | 979 to_log.append(f" headers:\n{headers}") |
955 return to_log | 980 return to_log |
956 | 981 |
982 def get_requestor_actor_id( | |
983 self, | |
984 data: dict|None = None, | |
985 uri_extra_args: list[str]|None = None | |
986 ) -> str: | |
987 """Find the actor ID of the requestor. | |
988 | |
989 The requestor here is actually the local actor which will do the requests to | |
990 achieve the task (e.g. retrieve external actor data), not the requestor of the | |
991 received AP request. | |
992 | |
993 It will notably be used as requestor actor ID to sign HTTP requests. We need to | |
994 sign GET request too to access instance checking HTTP GET signature (e.g. Mastodon | |
995 instances set in "secure mode"). | |
996 | |
997 We look for the destinee of the request and check if it's a local actor, and | |
998 default to a generic one if we can't find it. | |
999 | |
1000 Destinee is first checked in data if any, otherwise in request URI. | |
1001 | |
1002 @param data: parsed JSON data of original AP request, if any. | |
1003 @param uri_extra_args: arguments of the AP request as returned by | |
1004 [self.apg.parse_apurl]. It is most of time the destinee of the request. | |
1005 @return: requestor_actor_id to use to sign HTTP request needed to answer the | |
1006 original request. | |
1007 """ | |
1008 # We first check for destinee in data. | |
1009 if data: | |
1010 try: | |
1011 for to_ in data["to"]: | |
1012 if self.apg.is_local_url(to_): | |
1013 url_type, url_args = self.apg.parse_apurl(to_) | |
1014 if url_type != TYPE_ACTOR or not url_args: | |
1015 continue | |
1016 ap_account = url_args[0] | |
1017 if ( | |
1018 not ap_account.endswith(f"@{self.apg.public_url}") | |
1019 or ap_account.count("@") != 1 | |
1020 ): | |
1021 continue | |
1022 return to_ | |
1023 except KeyError: | |
1024 pass | |
1025 | |
1026 # If nothing relevant, we try URI arguments. | |
1027 if uri_extra_args: | |
1028 ap_account = uri_extra_args[0] | |
1029 if ( | |
1030 ap_account.endswith(f"@{self.apg.public_url}") | |
1031 and ap_account.count("@") == 1 | |
1032 ): | |
1033 return self.apg.build_apurl(TYPE_ACTOR, ap_account) | |
1034 | |
1035 # Still nothing, we'll have to use a generic actor. | |
1036 log.warning( | |
1037 "Can't find destinee in \"to\" field, using generic requestor for signature." | |
1038 ) | |
1039 return self.apg.build_apurl( | |
1040 TYPE_ACTOR, f"libervia@{self.apg.public_url}" | |
1041 ) | |
1042 | |
957 async def ap_request( | 1043 async def ap_request( |
958 self, | 1044 self, |
959 request: "HTTPRequest", | 1045 request: "HTTPRequest", |
960 data: Optional[dict] = None, | 1046 data: dict|None = None, |
961 signing_actor: Optional[str] = None | 1047 signing_actor: str|None = None, |
1048 requestor_actor_id: str|None = None, | |
962 ) -> None: | 1049 ) -> None: |
963 if self.apg.verbose: | 1050 if self.apg.verbose: |
964 to_log = self._get_to_log(request, data) | 1051 to_log = self._get_to_log(request, data) |
965 | 1052 |
966 path = request.path.decode() | 1053 path = request.path.decode() |
967 ap_url = parse.urljoin( | 1054 ap_url = parse.urljoin( |
968 f"https://{self.apg.public_url}", | 1055 f"https://{self.apg.public_url}", |
969 path | 1056 path |
970 ) | 1057 ) |
971 request_type, extra_args = self.apg.parse_apurl(ap_url) | 1058 request_type, extra_args = self.apg.parse_apurl(ap_url) |
1059 | |
972 header_accept = request.getHeader("accept") or "" | 1060 header_accept = request.getHeader("accept") or "" |
973 if ((MEDIA_TYPE_AP not in header_accept | 1061 if ((MEDIA_TYPE_AP not in header_accept |
974 and MEDIA_TYPE_AP_ALT not in header_accept | 1062 and MEDIA_TYPE_AP_ALT not in header_accept |
975 and request_type in self.apg.html_redirect)): | 1063 and request_type in self.apg.html_redirect)): |
976 # this is not a AP request, and we have a redirections for it | 1064 # this is not a AP request, and we have a redirections for it |
1005 content = web_util.redirectTo(target_url.encode(), request) | 1093 content = web_util.redirectTo(target_url.encode(), request) |
1006 request.write(content) | 1094 request.write(content) |
1007 request.finish() | 1095 request.finish() |
1008 return | 1096 return |
1009 | 1097 |
1098 if requestor_actor_id is None: | |
1099 requestor_actor_id = self.get_requestor_actor_id( | |
1100 data, extra_args | |
1101 ) | |
1010 if len(extra_args) == 0: | 1102 if len(extra_args) == 0: |
1011 if request_type != "shared_inbox": | 1103 if request_type != "shared_inbox": |
1012 raise exceptions.DataError(f"Invalid request type: {request_type!r}") | 1104 raise exceptions.DataError(f"Invalid request type: {request_type!r}") |
1013 ret_data = await self.ap_inbox_request( | 1105 ret_data = await self.ap_inbox_request( |
1014 request, data, None, None, None, ap_url, signing_actor | 1106 requestor_actor_id, request, data, None, None, None, ap_url, signing_actor |
1015 ) | 1107 ) |
1016 elif request_type == "avatar": | 1108 elif request_type == "avatar": |
1017 if len(extra_args) != 1: | 1109 if len(extra_args) != 1: |
1018 raise exceptions.DataError("avatar argument expected in URL") | 1110 raise exceptions.DataError("avatar argument expected in URL") |
1019 avatar_filename = extra_args[0] | 1111 avatar_filename = extra_args[0] |
1032 request.method.decode().upper(), [] | 1124 request.method.decode().upper(), [] |
1033 ): | 1125 ): |
1034 raise exceptions.DataError(f"Invalid request type: {request_type!r}") | 1126 raise exceptions.DataError(f"Invalid request type: {request_type!r}") |
1035 method = getattr(self, f"ap_{request_type}_request") | 1127 method = getattr(self, f"ap_{request_type}_request") |
1036 ret_data = await method( | 1128 ret_data = await method( |
1037 request, data, account_jid, node, ap_account, ap_url, signing_actor | 1129 requestor_actor_id, request, data, account_jid, node, ap_account, ap_url, signing_actor |
1038 ) | 1130 ) |
1039 if ret_data is not None: | 1131 if ret_data is not None: |
1040 request.setHeader("content-type", CONTENT_TYPE_AP) | 1132 request.setHeader("content-type", CONTENT_TYPE_AP) |
1041 request.write(json.dumps(ret_data).encode()) | 1133 request.write(json.dumps(ret_data).encode()) |
1042 if self.apg.verbose: | 1134 if self.apg.verbose: |
1070 request.finish() | 1162 request.finish() |
1071 return | 1163 return |
1072 else: | 1164 else: |
1073 request.content.seek(0) | 1165 request.content.seek(0) |
1074 | 1166 |
1167 requestor_actor_id = self.get_requestor_actor_id(data) | |
1168 | |
1075 try: | 1169 try: |
1076 if data["type"] == "Delete" and data["actor"] == data["object"]: | 1170 if data["type"] == "Delete" and data["actor"] == data["object"]: |
1077 # we don't handle actor deletion | 1171 # we don't handle actor deletion |
1078 request.setResponseCode(http.ACCEPTED) | 1172 request.setResponseCode(http.ACCEPTED) |
1079 log.debug(f"ignoring actor deletion ({data['actor']})") | 1173 log.debug(f"ignoring actor deletion ({data['actor']})") |
1082 return | 1176 return |
1083 except KeyError: | 1177 except KeyError: |
1084 pass | 1178 pass |
1085 | 1179 |
1086 try: | 1180 try: |
1087 signing_actor = await self.check_signature(request) | 1181 signing_actor = await self.check_signature(requestor_actor_id, request) |
1088 except exceptions.EncryptionError as e: | 1182 except exceptions.EncryptionError as e: |
1089 if self.apg.verbose: | 1183 if self.apg.verbose: |
1090 to_log = self._get_to_log(request) | 1184 to_log = self._get_to_log(request) |
1091 to_log.append(f" body: {request.content.read()!r}") | 1185 to_log.append(f" body: {request.content.read()!r}") |
1092 request.content.seek(0) | 1186 request.content.seek(0) |
1116 return | 1210 return |
1117 self._seen_digest.append(digest) | 1211 self._seen_digest.append(digest) |
1118 | 1212 |
1119 # default response code, may be changed, e.g. in case of exception | 1213 # default response code, may be changed, e.g. in case of exception |
1120 try: | 1214 try: |
1121 return await self.ap_request(request, data, signing_actor) | 1215 return await self.ap_request( |
1216 request, data, signing_actor, requestor_actor_id=requestor_actor_id | |
1217 ) | |
1122 except Exception as e: | 1218 except Exception as e: |
1123 self._on_request_error(failure.Failure(e), request) | 1219 self._on_request_error(failure.Failure(e), request) |
1124 | 1220 |
1125 async def check_signing_actor(self, data: dict, signing_actor: str) -> None: | 1221 async def check_signing_actor( |
1222 self, | |
1223 requestor_actor_id: str, | |
1224 data: dict, | |
1225 signing_actor: str | |
1226 ) -> None: | |
1126 """That that signing actor correspond to actor declared in data | 1227 """That that signing actor correspond to actor declared in data |
1127 | 1228 |
1229 @param requestor_actor_id: ID of the actor doing the request. | |
1128 @param data: request payload | 1230 @param data: request payload |
1129 @param signing_actor: actor ID of the signing entity, as returned by | 1231 @param signing_actor: actor ID of the signing entity, as returned by |
1130 check_signature | 1232 check_signature |
1131 @raise exceptions.NotFound: no actor found in data | 1233 @raise exceptions.NotFound: no actor found in data |
1132 @raise exceptions.EncryptionError: signing actor doesn't match actor in data | 1234 @raise exceptions.EncryptionError: signing actor doesn't match actor in data |
1133 """ | 1235 """ |
1134 actor = await self.apg.ap_get_sender_actor(data) | 1236 actor = await self.apg.ap_get_sender_actor(requestor_actor_id, data) |
1135 | 1237 |
1136 if signing_actor != actor: | 1238 if signing_actor != actor: |
1137 raise exceptions.EncryptionError( | 1239 raise exceptions.EncryptionError( |
1138 f"signing actor ({signing_actor}) doesn't match actor in data ({actor})" | 1240 f"signing actor ({signing_actor}) doesn't match actor in data ({actor})" |
1139 ) | 1241 ) |
1140 | 1242 |
1141 async def check_signature(self, request: "HTTPRequest") -> str: | 1243 async def check_signature( |
1244 self, | |
1245 requestor_actor_id: str, | |
1246 request: "HTTPRequest" | |
1247 ) -> str: | |
1142 """Check and validate HTTP signature | 1248 """Check and validate HTTP signature |
1143 | 1249 |
1250 @param requestor_actor_id: ID of the actor doing the request. | |
1144 @return: id of the signing actor | 1251 @return: id of the signing actor |
1145 | 1252 |
1146 @raise exceptions.EncryptionError: signature is not present or doesn't match | 1253 @raise exceptions.EncryptionError: signature is not present or doesn't match |
1147 """ | 1254 """ |
1148 signature = request.getHeader("Signature") | 1255 signature = request.getHeader("Signature") |
1277 if created > limit_ts: | 1384 if created > limit_ts: |
1278 raise exceptions.EncryptionError("Signature has expired") | 1385 raise exceptions.EncryptionError("Signature has expired") |
1279 | 1386 |
1280 try: | 1387 try: |
1281 return await self.apg.check_signature( | 1388 return await self.apg.check_signature( |
1389 requestor_actor_id, | |
1282 sign_data["signature"], | 1390 sign_data["signature"], |
1283 key_id, | 1391 key_id, |
1284 headers | 1392 headers |
1285 ) | 1393 ) |
1286 except exceptions.EncryptionError: | 1394 except exceptions.EncryptionError: |
1289 log.debug( | 1397 log.debug( |
1290 "Using workaround for (request-target) encoding bug in signature, " | 1398 "Using workaround for (request-target) encoding bug in signature, " |
1291 "see https://github.com/mastodon/mastodon/issues/18871" | 1399 "see https://github.com/mastodon/mastodon/issues/18871" |
1292 ) | 1400 ) |
1293 return await self.apg.check_signature( | 1401 return await self.apg.check_signature( |
1402 requestor_actor_id, | |
1294 sign_data["signature"], | 1403 sign_data["signature"], |
1295 key_id, | 1404 key_id, |
1296 headers | 1405 headers |
1297 ) | 1406 ) |
1298 | 1407 |