Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_comp_ap_gateway/http_server.py @ 4270:0d7bb4df2343
Reformatted code base using black.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 19 Jun 2024 18:44:57 +0200 |
parents | 49019947cc76 |
children |
comparison
equal
deleted
inserted
replaced
4269:64a85ce8be70 | 4270:0d7bb4df2343 |
---|---|
39 from libervia.backend.core.log import getLogger | 39 from libervia.backend.core.log import getLogger |
40 from libervia.backend.tools.common import date_utils, uri | 40 from libervia.backend.tools.common import date_utils, uri |
41 from libervia.backend.memory.sqla_mapping import SubscriptionState | 41 from libervia.backend.memory.sqla_mapping import SubscriptionState |
42 | 42 |
43 from .constants import ( | 43 from .constants import ( |
44 NS_AP, MEDIA_TYPE_AP, MEDIA_TYPE_AP_ALT, CONTENT_TYPE_WEBFINGER, CONTENT_TYPE_AP, | 44 NS_AP, |
45 TYPE_ACTOR, TYPE_INBOX, TYPE_SHARED_INBOX, TYPE_OUTBOX, TYPE_EVENT, AP_REQUEST_TYPES, | 45 MEDIA_TYPE_AP, |
46 PAGE_SIZE, ACTIVITY_TYPES_LOWER, ACTIVIY_NO_ACCOUNT_ALLOWED, SIGN_HEADERS, HS2019, | 46 MEDIA_TYPE_AP_ALT, |
47 SIGN_EXP, TYPE_FOLLOWERS, TYPE_FOLLOWING, TYPE_ITEM, TYPE_LIKE, TYPE_REACTION, | 47 CONTENT_TYPE_WEBFINGER, |
48 ST_AP_CACHE | 48 CONTENT_TYPE_AP, |
49 TYPE_ACTOR, | |
50 TYPE_INBOX, | |
51 TYPE_SHARED_INBOX, | |
52 TYPE_OUTBOX, | |
53 TYPE_EVENT, | |
54 AP_REQUEST_TYPES, | |
55 PAGE_SIZE, | |
56 ACTIVITY_TYPES_LOWER, | |
57 ACTIVIY_NO_ACCOUNT_ALLOWED, | |
58 SIGN_HEADERS, | |
59 HS2019, | |
60 SIGN_EXP, | |
61 TYPE_FOLLOWERS, | |
62 TYPE_FOLLOWING, | |
63 TYPE_ITEM, | |
64 TYPE_LIKE, | |
65 TYPE_REACTION, | |
66 ST_AP_CACHE, | |
49 ) | 67 ) |
50 from .regex import RE_SIG_PARAM | 68 from .regex import RE_SIG_PARAM |
51 | 69 |
52 | 70 |
53 log = getLogger(__name__) | 71 log = getLogger(__name__) |
54 | 72 |
55 VERSION = unicodedata.normalize( | 73 VERSION = unicodedata.normalize( |
56 'NFKD', | 74 "NFKD", f"{C.APP_NAME} ActivityPub Gateway {C.APP_VERSION}" |
57 f"{C.APP_NAME} ActivityPub Gateway {C.APP_VERSION}" | |
58 ) | 75 ) |
59 | 76 |
60 | 77 |
61 class HTTPAPGServer(web_resource.Resource): | 78 class HTTPAPGServer(web_resource.Resource): |
62 """HTTP Server handling ActivityPub S2S protocol""" | 79 """HTTP Server handling ActivityPub S2S protocol""" |
80 | |
63 isLeaf = True | 81 isLeaf = True |
64 | 82 |
65 def __init__(self, ap_gateway): | 83 def __init__(self, ap_gateway): |
66 self.apg = ap_gateway | 84 self.apg = ap_gateway |
67 self._seen_digest = deque(maxlen=50) | 85 self._seen_digest = deque(maxlen=50) |
68 super().__init__() | 86 super().__init__() |
69 | 87 |
70 def response_code( | 88 def response_code( |
71 self, | 89 self, request: "HTTPRequest", http_code: int, msg: Optional[str] = None |
72 request: "HTTPRequest", | |
73 http_code: int, | |
74 msg: Optional[str] = None | |
75 ) -> None: | 90 ) -> None: |
76 """Log and set HTTP return code and associated message""" | 91 """Log and set HTTP return code and associated message""" |
77 if msg is not None: | 92 if msg is not None: |
78 log.warning(msg) | 93 log.warning(msg) |
79 request.setResponseCode(http_code, None if msg is None else msg.encode()) | 94 request.setResponseCode(http_code, None if msg is None else msg.encode()) |
80 | 95 |
81 def _on_request_error(self, failure_: failure.Failure, request: "HTTPRequest") -> None: | 96 def _on_request_error( |
97 self, failure_: failure.Failure, request: "HTTPRequest" | |
98 ) -> None: | |
82 exc = failure_.value | 99 exc = failure_.value |
83 if isinstance(exc, exceptions.NotFound): | 100 if isinstance(exc, exceptions.NotFound): |
84 self.response_code( | 101 self.response_code(request, http.NOT_FOUND, str(exc)) |
85 request, | |
86 http.NOT_FOUND, | |
87 str(exc) | |
88 ) | |
89 else: | 102 else: |
90 log.exception(f"Internal error: {failure_.value}") | 103 log.exception(f"Internal error: {failure_.value}") |
91 self.response_code( | 104 self.response_code( |
92 request, | 105 request, http.INTERNAL_SERVER_ERROR, f"internal error: {failure_.value}" |
93 http.INTERNAL_SERVER_ERROR, | |
94 f"internal error: {failure_.value}" | |
95 ) | 106 ) |
96 request.finish() | 107 request.finish() |
97 raise failure_ | 108 raise failure_ |
98 | 109 |
99 request.finish() | 110 request.finish() |
103 query = parse.parse_qs(url_parsed.query) | 114 query = parse.parse_qs(url_parsed.query) |
104 resource = query.get("resource", [""])[0] | 115 resource = query.get("resource", [""])[0] |
105 account = resource[5:].strip() | 116 account = resource[5:].strip() |
106 if not resource.startswith("acct:") or not account: | 117 if not resource.startswith("acct:") or not account: |
107 return web_resource.ErrorPage( | 118 return web_resource.ErrorPage( |
108 http.BAD_REQUEST, "Bad Request" , "Invalid webfinger resource" | 119 http.BAD_REQUEST, "Bad Request", "Invalid webfinger resource" |
109 ).render(request) | 120 ).render(request) |
110 | 121 |
111 actor_url = self.apg.build_apurl(TYPE_ACTOR, account) | 122 actor_url = self.apg.build_apurl(TYPE_ACTOR, account) |
112 | 123 |
113 resp = { | 124 resp = { |
114 "aliases": [actor_url], | 125 "aliases": [actor_url], |
115 "subject": resource, | 126 "subject": resource, |
116 "links": [ | 127 "links": [ |
117 { | 128 {"rel": "self", "type": "application/activity+json", "href": actor_url} |
118 "rel": "self", | 129 ], |
119 "type": "application/activity+json", | |
120 "href": actor_url | |
121 } | |
122 ] | |
123 } | 130 } |
124 request.setHeader("content-type", CONTENT_TYPE_WEBFINGER) | 131 request.setHeader("content-type", CONTENT_TYPE_WEBFINGER) |
125 request.write(json.dumps(resp).encode()) | 132 request.write(json.dumps(resp).encode()) |
126 request.finish() | 133 request.finish() |
127 | 134 |
132 data: dict, | 139 data: dict, |
133 account_jid: jid.JID, | 140 account_jid: jid.JID, |
134 node: Optional[str], | 141 node: Optional[str], |
135 ap_account: str, | 142 ap_account: str, |
136 ap_url: str, | 143 ap_url: str, |
137 signing_actor: str | 144 signing_actor: str, |
138 ) -> None: | 145 ) -> None: |
139 if node is None: | 146 if node is None: |
140 node = self.apg._m.namespace | 147 node = self.apg._m.namespace |
141 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) | 148 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) |
142 object_ = data.get("object") | 149 object_ = data.get("object") |
179 # needed | 186 # needed |
180 await self.apg.new_ap_delete_item(client, None, node, obj) | 187 await self.apg.new_ap_delete_item(client, None, node, obj) |
181 elif type_ == TYPE_LIKE: | 188 elif type_ == TYPE_LIKE: |
182 await self.handle_attachment_item(client, obj, {"noticed": False}) | 189 await self.handle_attachment_item(client, obj, {"noticed": False}) |
183 elif type_ == TYPE_REACTION: | 190 elif type_ == TYPE_REACTION: |
184 await self.handle_attachment_item(client, obj, { | 191 await self.handle_attachment_item( |
185 "reactions": {"operation": "update", "remove": [obj["content"]]} | 192 client, |
186 }) | 193 obj, |
194 {"reactions": {"operation": "update", "remove": [obj["content"]]}}, | |
195 ) | |
187 else: | 196 else: |
188 log.warning(f"Unmanaged undo type: {type_!r}") | 197 log.warning(f"Unmanaged undo type: {type_!r}") |
189 | 198 |
190 async def handle_follow_activity( | 199 async def handle_follow_activity( |
191 self, | 200 self, |
194 data: dict, | 203 data: dict, |
195 account_jid: jid.JID, | 204 account_jid: jid.JID, |
196 node: Optional[str], | 205 node: Optional[str], |
197 ap_account: str, | 206 ap_account: str, |
198 ap_url: str, | 207 ap_url: str, |
199 signing_actor: str | 208 signing_actor: str, |
200 ) -> None: | 209 ) -> None: |
201 if node is None: | 210 if node is None: |
202 node = self.apg._m.namespace | 211 node = self.apg._m.namespace |
203 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) | 212 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) |
204 try: | 213 try: |
205 subscription = await self.apg._p.subscribe( | 214 subscription = await self.apg._p.subscribe( |
206 client, | 215 client, |
207 account_jid, | 216 account_jid, |
208 node, | 217 node, |
209 # subscriptions from AP are always public | 218 # subscriptions from AP are always public |
210 options=self.apg._pps.set_public_opt() | 219 options=self.apg._pps.set_public_opt(), |
211 ) | 220 ) |
212 except pubsub.SubscriptionPending: | 221 except pubsub.SubscriptionPending: |
213 log.info(f"subscription to node {node!r} of {account_jid} is pending") | 222 log.info(f"subscription to node {node!r} of {account_jid} is pending") |
214 # TODO: manage SubscriptionUnconfigured | 223 # TODO: manage SubscriptionUnconfigured |
215 else: | 224 else: |
216 if subscription.state != "subscribed": | 225 if subscription.state != "subscribed": |
217 # other states should raise an Exception | 226 # other states should raise an Exception |
218 raise exceptions.InternalError('"subscribed" state was expected') | 227 raise exceptions.InternalError('"subscribed" state was expected') |
219 inbox = await self.apg.get_ap_inbox_from_id(signing_actor, use_shared=False) | 228 inbox = await self.apg.get_ap_inbox_from_id(signing_actor, use_shared=False) |
220 actor_id = self.apg.build_apurl(TYPE_ACTOR, ap_account) | 229 actor_id = self.apg.build_apurl(TYPE_ACTOR, ap_account) |
221 accept_data = self.apg.create_activity( | 230 accept_data = self.apg.create_activity("Accept", actor_id, object_=data) |
222 "Accept", actor_id, object_=data | |
223 ) | |
224 await self.apg.sign_and_post(inbox, actor_id, accept_data) | 231 await self.apg.sign_and_post(inbox, actor_id, accept_data) |
225 await self.apg._c.synchronise(client, account_jid, node, resync=False) | 232 await self.apg._c.synchronise(client, account_jid, node, resync=False) |
226 | 233 |
227 async def handle_accept_activity( | 234 async def handle_accept_activity( |
228 self, | 235 self, |
231 data: dict, | 238 data: dict, |
232 account_jid: jid.JID, | 239 account_jid: jid.JID, |
233 node: Optional[str], | 240 node: Optional[str], |
234 ap_account: str, | 241 ap_account: str, |
235 ap_url: str, | 242 ap_url: str, |
236 signing_actor: str | 243 signing_actor: str, |
237 ) -> None: | 244 ) -> None: |
238 if node is None: | 245 if node is None: |
239 node = self.apg._m.namespace | 246 node = self.apg._m.namespace |
240 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) | 247 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) |
241 objects = await self.apg.ap_get_list(requestor_actor_id, data, "object") | 248 objects = await self.apg.ap_get_list(requestor_actor_id, data, "object") |
251 f"{client.jid}. Ignoring it" | 258 f"{client.jid}. Ignoring it" |
252 ) | 259 ) |
253 continue | 260 continue |
254 try: | 261 try: |
255 sub = next( | 262 sub = next( |
256 s for s in follow_node.subscriptions if s.subscriber==account_jid | 263 s |
264 for s in follow_node.subscriptions | |
265 if s.subscriber == account_jid | |
257 ) | 266 ) |
258 except StopIteration: | 267 except StopIteration: |
259 log.warning( | 268 log.warning( |
260 "Received a follow accept on a node without subscription: " | 269 "Received a follow accept on a node without subscription: " |
261 f"{node!r} at {client.jid}. Ignoring it" | 270 f"{node!r} at {client.jid}. Ignoring it" |
281 data: dict, | 290 data: dict, |
282 account_jid: Optional[jid.JID], | 291 account_jid: Optional[jid.JID], |
283 node: Optional[str], | 292 node: Optional[str], |
284 ap_account: Optional[str], | 293 ap_account: Optional[str], |
285 ap_url: str, | 294 ap_url: str, |
286 signing_actor: str | 295 signing_actor: str, |
287 ): | 296 ): |
288 if node is None: | 297 if node is None: |
289 node = self.apg._m.namespace | 298 node = self.apg._m.namespace |
290 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) | 299 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) |
291 objects = await self.apg.ap_get_list(requestor_actor_id, data, "object") | 300 objects = await self.apg.ap_get_list(requestor_actor_id, data, "object") |
323 node = self.apg._m.namespace | 332 node = self.apg._m.namespace |
324 sender = await self.apg.ap_get_sender_actor(requestor_actor_id, obj) | 333 sender = await self.apg.ap_get_sender_actor(requestor_actor_id, obj) |
325 if repeated: | 334 if repeated: |
326 # we don't check sender when item is repeated, as it should be different | 335 # we don't check sender when item is repeated, as it should be different |
327 # from post author in this case | 336 # from post author in this case |
328 sender_jid = await self.apg.get_jid_from_id( | 337 sender_jid = await self.apg.get_jid_from_id(requestor_actor_id, sender) |
329 requestor_actor_id, | |
330 sender | |
331 ) | |
332 repeater_jid = await self.apg.get_jid_from_id( | 338 repeater_jid = await self.apg.get_jid_from_id( |
333 requestor_actor_id, | 339 requestor_actor_id, signing_actor |
334 signing_actor | |
335 ) | 340 ) |
336 repeated_item_id = obj["id"] | 341 repeated_item_id = obj["id"] |
337 if self.apg.is_local_url(repeated_item_id): | 342 if self.apg.is_local_url(repeated_item_id): |
338 # the repeated object is from XMPP, we need to parse the URL to find | 343 # the repeated object is from XMPP, we need to parse the URL to find |
339 # the right ID | 344 # the right ID |
345 try: | 350 try: |
346 url_account, url_item_id = url_args | 351 url_account, url_item_id = url_args |
347 if not url_account or not url_item_id: | 352 if not url_account or not url_item_id: |
348 raise ValueError | 353 raise ValueError |
349 except (RuntimeError, ValueError): | 354 except (RuntimeError, ValueError): |
350 raise exceptions.DataError( | 355 raise exceptions.DataError("local URI is invalid: {repeated_id}") |
351 "local URI is invalid: {repeated_id}" | |
352 ) | |
353 else: | 356 else: |
354 url_jid, url_node = await self.apg.get_jid_and_node(url_account) | 357 url_jid, url_node = await self.apg.get_jid_and_node(url_account) |
355 if ((url_jid != sender_jid | 358 if ( |
356 or url_node and url_node != self.apg._m.namespace)): | 359 url_jid != sender_jid |
360 or url_node | |
361 and url_node != self.apg._m.namespace | |
362 ): | |
357 raise exceptions.DataError( | 363 raise exceptions.DataError( |
358 "announced ID doesn't match sender ({sender}): " | 364 "announced ID doesn't match sender ({sender}): " |
359 f"[repeated_item_id]" | 365 f"[repeated_item_id]" |
360 ) | 366 ) |
361 | 367 |
366 "at": data.get("published"), | 372 "at": data.get("published"), |
367 "uri": uri.build_xmpp_uri( | 373 "uri": uri.build_xmpp_uri( |
368 "pubsub", | 374 "pubsub", |
369 path=sender_jid.full(), | 375 path=sender_jid.full(), |
370 node=self.apg._m.namespace, | 376 node=self.apg._m.namespace, |
371 item=repeated_item_id | 377 item=repeated_item_id, |
372 ) | 378 ), |
373 } | 379 } |
374 # we must use activity's id and targets, not the original item ones | 380 # we must use activity's id and targets, not the original item ones |
375 for field in ("id", "to", "bto", "cc", "bcc"): | 381 for field in ("id", "to", "bto", "cc", "bcc"): |
376 obj[field] = data.get(field) | 382 obj[field] = data.get(field) |
377 else: | 383 else: |
378 if sender != signing_actor: | 384 if sender != signing_actor: |
379 log.warning( | 385 log.warning("Ignoring object not attributed to signing actor: {obj}") |
380 "Ignoring object not attributed to signing actor: {obj}" | |
381 ) | |
382 continue | 386 continue |
383 | 387 |
384 await self.apg.new_ap_item(client, account_jid, node, obj) | 388 await self.apg.new_ap_item(client, account_jid, node, obj) |
385 | 389 |
386 async def handle_create_activity( | 390 async def handle_create_activity( |
390 data: dict, | 394 data: dict, |
391 account_jid: Optional[jid.JID], | 395 account_jid: Optional[jid.JID], |
392 node: Optional[str], | 396 node: Optional[str], |
393 ap_account: Optional[str], | 397 ap_account: Optional[str], |
394 ap_url: str, | 398 ap_url: str, |
395 signing_actor: str | 399 signing_actor: str, |
396 ): | 400 ): |
397 await self.handle_new_ap_items( | 401 await self.handle_new_ap_items( |
398 requestor_actor_id, request, data, account_jid, node, signing_actor | 402 requestor_actor_id, request, data, account_jid, node, signing_actor |
399 ) | 403 ) |
400 | 404 |
405 data: dict, | 409 data: dict, |
406 account_jid: Optional[jid.JID], | 410 account_jid: Optional[jid.JID], |
407 node: Optional[str], | 411 node: Optional[str], |
408 ap_account: Optional[str], | 412 ap_account: Optional[str], |
409 ap_url: str, | 413 ap_url: str, |
410 signing_actor: str | 414 signing_actor: str, |
411 ): | 415 ): |
412 # Update is the same as create: the item ID stays the same, thus the item will be | 416 # Update is the same as create: the item ID stays the same, thus the item will be |
413 # overwritten | 417 # overwritten |
414 await self.handle_new_ap_items( | 418 await self.handle_new_ap_items( |
415 requestor_actor_id, request, data, account_jid, node, signing_actor | 419 requestor_actor_id, request, data, account_jid, node, signing_actor |
422 data: dict, | 426 data: dict, |
423 account_jid: Optional[jid.JID], | 427 account_jid: Optional[jid.JID], |
424 node: Optional[str], | 428 node: Optional[str], |
425 ap_account: Optional[str], | 429 ap_account: Optional[str], |
426 ap_url: str, | 430 ap_url: str, |
427 signing_actor: str | 431 signing_actor: str, |
428 ): | 432 ): |
429 # we create a new item | 433 # we create a new item |
430 await self.handle_new_ap_items( | 434 await self.handle_new_ap_items( |
431 requestor_actor_id, | 435 requestor_actor_id, |
432 request, | 436 request, |
433 data, | 437 data, |
434 account_jid, | 438 account_jid, |
435 node, | 439 node, |
436 signing_actor, | 440 signing_actor, |
437 repeated=True | 441 repeated=True, |
438 ) | 442 ) |
439 | 443 |
440 async def handle_attachment_item( | 444 async def handle_attachment_item( |
441 self, | 445 self, client: SatXMPPEntity, data: dict, attachment_data: dict |
442 client: SatXMPPEntity, | |
443 data: dict, | |
444 attachment_data: dict | |
445 ) -> None: | 446 ) -> None: |
446 target_ids = data.get("object") | 447 target_ids = data.get("object") |
447 if not target_ids: | 448 if not target_ids: |
448 raise exceptions.DataError("object should be set") | 449 raise exceptions.DataError("object should be set") |
449 elif isinstance(target_ids, list): | 450 elif isinstance(target_ids, list): |
483 item_node = self.apg._m.namespace | 484 item_node = self.apg._m.namespace |
484 attachment_node = self.apg._pa.get_attachment_node_name( | 485 attachment_node = self.apg._pa.get_attachment_node_name( |
485 author_jid, item_node, item_id | 486 author_jid, item_node, item_id |
486 ) | 487 ) |
487 cached_node = await self.apg.host.memory.storage.get_pubsub_node( | 488 cached_node = await self.apg.host.memory.storage.get_pubsub_node( |
488 client, | 489 client, author_jid, attachment_node, with_subscriptions=True, create=True |
489 author_jid, | |
490 attachment_node, | |
491 with_subscriptions=True, | |
492 create=True | |
493 ) | 490 ) |
494 found_items, __ = await self.apg.host.memory.storage.get_items( | 491 found_items, __ = await self.apg.host.memory.storage.get_items( |
495 cached_node, item_ids=[client.jid.userhost()] | 492 cached_node, item_ids=[client.jid.userhost()] |
496 ) | 493 ) |
497 if not found_items: | 494 if not found_items: |
499 else: | 496 else: |
500 found_item = found_items[0] | 497 found_item = found_items[0] |
501 old_item_elt = found_item.data | 498 old_item_elt = found_item.data |
502 | 499 |
503 item_elt = await self.apg._pa.apply_set_handler( | 500 item_elt = await self.apg._pa.apply_set_handler( |
504 client, | 501 client, {"extra": attachment_data}, old_item_elt, None |
505 {"extra": attachment_data}, | |
506 old_item_elt, | |
507 None | |
508 ) | 502 ) |
509 # we reparse the element, as there can be other attachments | 503 # we reparse the element, as there can be other attachments |
510 attachments_data = self.apg._pa.items_2_attachment_data(client, [item_elt]) | 504 attachments_data = self.apg._pa.items_2_attachment_data(client, [item_elt]) |
511 # and we update the cache | 505 # and we update the cache |
512 await self.apg.host.memory.storage.cache_pubsub_items( | 506 await self.apg.host.memory.storage.cache_pubsub_items( |
513 client, | 507 client, cached_node, [item_elt], attachments_data or [{}] |
514 cached_node, | |
515 [item_elt], | |
516 attachments_data or [{}] | |
517 ) | 508 ) |
518 | 509 |
519 if self.apg.is_virtual_jid(author_jid): | 510 if self.apg.is_virtual_jid(author_jid): |
520 # the attachment is on t a virtual pubsub service (linking to an AP item), | 511 # the attachment is on t a virtual pubsub service (linking to an AP item), |
521 # we notify all subscribers | 512 # we notify all subscribers |
523 if subscription.state != SubscriptionState.SUBSCRIBED: | 514 if subscription.state != SubscriptionState.SUBSCRIBED: |
524 continue | 515 continue |
525 self.apg.pubsub_service.notifyPublish( | 516 self.apg.pubsub_service.notifyPublish( |
526 author_jid, | 517 author_jid, |
527 attachment_node, | 518 attachment_node, |
528 [(subscription.subscriber, None, [item_elt])] | 519 [(subscription.subscriber, None, [item_elt])], |
529 ) | 520 ) |
530 else: | 521 else: |
531 # the attachment is on an XMPP item, we publish it to the attachment node | 522 # the attachment is on an XMPP item, we publish it to the attachment node |
532 await self.apg._p.send_items( | 523 await self.apg._p.send_items( |
533 client, author_jid, attachment_node, [item_elt] | 524 client, author_jid, attachment_node, [item_elt] |
540 data: dict, | 531 data: dict, |
541 account_jid: Optional[jid.JID], | 532 account_jid: Optional[jid.JID], |
542 node: Optional[str], | 533 node: Optional[str], |
543 ap_account: Optional[str], | 534 ap_account: Optional[str], |
544 ap_url: str, | 535 ap_url: str, |
545 signing_actor: str | 536 signing_actor: str, |
546 ) -> None: | 537 ) -> None: |
547 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) | 538 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) |
548 await self.handle_attachment_item(client, data, {"noticed": True}) | 539 await self.handle_attachment_item(client, data, {"noticed": True}) |
549 | 540 |
550 async def handle_emojireact_activity( | 541 async def handle_emojireact_activity( |
554 data: dict, | 545 data: dict, |
555 account_jid: Optional[jid.JID], | 546 account_jid: Optional[jid.JID], |
556 node: Optional[str], | 547 node: Optional[str], |
557 ap_account: Optional[str], | 548 ap_account: Optional[str], |
558 ap_url: str, | 549 ap_url: str, |
559 signing_actor: str | 550 signing_actor: str, |
560 ) -> None: | 551 ) -> None: |
561 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) | 552 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) |
562 await self.handle_attachment_item(client, data, { | 553 await self.handle_attachment_item( |
563 "reactions": {"operation": "update", "add": [data["content"]]} | 554 client, data, {"reactions": {"operation": "update", "add": [data["content"]]}} |
564 }) | 555 ) |
565 | 556 |
566 async def handle_join_activity( | 557 async def handle_join_activity( |
567 self, | 558 self, |
568 requestor_actor_id: str, | 559 requestor_actor_id: str, |
569 request: "HTTPRequest", | 560 request: "HTTPRequest", |
570 data: dict, | 561 data: dict, |
571 account_jid: Optional[jid.JID], | 562 account_jid: Optional[jid.JID], |
572 node: Optional[str], | 563 node: Optional[str], |
573 ap_account: Optional[str], | 564 ap_account: Optional[str], |
574 ap_url: str, | 565 ap_url: str, |
575 signing_actor: str | 566 signing_actor: str, |
576 ) -> None: | 567 ) -> None: |
577 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) | 568 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) |
578 await self.handle_attachment_item(client, data, {"rsvp": {"attending": "yes"}}) | 569 await self.handle_attachment_item(client, data, {"rsvp": {"attending": "yes"}}) |
579 | 570 |
580 async def handle_leave_activity( | 571 async def handle_leave_activity( |
584 data: dict, | 575 data: dict, |
585 account_jid: Optional[jid.JID], | 576 account_jid: Optional[jid.JID], |
586 node: Optional[str], | 577 node: Optional[str], |
587 ap_account: Optional[str], | 578 ap_account: Optional[str], |
588 ap_url: str, | 579 ap_url: str, |
589 signing_actor: str | 580 signing_actor: str, |
590 ) -> None: | 581 ) -> None: |
591 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) | 582 client = await self.apg.get_virtual_client(requestor_actor_id, signing_actor) |
592 await self.handle_attachment_item(client, data, {"rsvp": {"attending": "no"}}) | 583 await self.handle_attachment_item(client, data, {"rsvp": {"attending": "no"}}) |
593 | 584 |
594 async def ap_actor_request( | 585 async def ap_actor_request( |
597 data: Optional[dict], | 588 data: Optional[dict], |
598 account_jid: jid.JID, | 589 account_jid: jid.JID, |
599 node: Optional[str], | 590 node: Optional[str], |
600 ap_account: str, | 591 ap_account: str, |
601 ap_url: str, | 592 ap_url: str, |
602 signing_actor: Optional[str] | 593 signing_actor: Optional[str], |
603 ) -> dict: | 594 ) -> dict: |
604 inbox = self.apg.build_apurl(TYPE_INBOX, ap_account) | 595 inbox = self.apg.build_apurl(TYPE_INBOX, ap_account) |
605 shared_inbox = self.apg.build_apurl(TYPE_SHARED_INBOX) | 596 shared_inbox = self.apg.build_apurl(TYPE_SHARED_INBOX) |
606 outbox = self.apg.build_apurl(TYPE_OUTBOX, ap_account) | 597 outbox = self.apg.build_apurl(TYPE_OUTBOX, ap_account) |
607 followers = self.apg.build_apurl(TYPE_FOLLOWERS, ap_account) | 598 followers = self.apg.build_apurl(TYPE_FOLLOWERS, ap_account) |
621 events = self.apg.build_apurl(TYPE_OUTBOX, events_account) | 612 events = self.apg.build_apurl(TYPE_OUTBOX, events_account) |
622 | 613 |
623 actor_data = { | 614 actor_data = { |
624 "@context": [ | 615 "@context": [ |
625 "https://www.w3.org/ns/activitystreams", | 616 "https://www.w3.org/ns/activitystreams", |
626 "https://w3id.org/security/v1" | 617 "https://w3id.org/security/v1", |
627 ], | 618 ], |
628 | |
629 # XXX: Mastodon doesn't like percent-encode arobas, so we have to unescape it | 619 # XXX: Mastodon doesn't like percent-encode arobas, so we have to unescape it |
630 # if it is escaped | 620 # if it is escaped |
631 "id": ap_url.replace("%40", "@"), | 621 "id": ap_url.replace("%40", "@"), |
632 "type": "Person", | 622 "type": "Person", |
633 "preferredUsername": preferred_username, | 623 "preferredUsername": preferred_username, |
637 "followers": followers, | 627 "followers": followers, |
638 "following": following, | 628 "following": following, |
639 "publicKey": { | 629 "publicKey": { |
640 "id": f"{ap_url}#main-key", | 630 "id": f"{ap_url}#main-key", |
641 "owner": ap_url, | 631 "owner": ap_url, |
642 "publicKeyPem": self.apg.public_key_pem | 632 "publicKeyPem": self.apg.public_key_pem, |
643 }, | 633 }, |
644 "endpoints": { | 634 "endpoints": { |
645 "sharedInbox": shared_inbox, | 635 "sharedInbox": shared_inbox, |
646 "events": events, | 636 "events": events, |
647 }, | 637 }, |
662 else: | 652 else: |
663 avatar_url = self.apg.build_apurl("avatar", filename) | 653 avatar_url = self.apg.build_apurl("avatar", filename) |
664 actor_data["icon"] = { | 654 actor_data["icon"] = { |
665 "type": "Image", | 655 "type": "Image", |
666 "url": avatar_url, | 656 "url": avatar_url, |
667 "mediaType": media_type | 657 "mediaType": media_type, |
668 } | 658 } |
669 | 659 |
670 return actor_data | 660 return actor_data |
671 | 661 |
672 def get_canonical_url(self, request: "HTTPRequest") -> str: | 662 def get_canonical_url(self, request: "HTTPRequest") -> str: |
673 return parse.urljoin( | 663 return parse.urljoin( |
674 f"https://{self.apg.public_url}", | 664 f"https://{self.apg.public_url}", |
675 request.path.decode().rstrip("/") | 665 request.path.decode().rstrip("/"), |
676 # we unescape "@" for the same reason as in [ap_actor_request] | 666 # we unescape "@" for the same reason as in [ap_actor_request] |
677 ).replace("%40", "@") | 667 ).replace("%40", "@") |
678 | 668 |
679 def query_data_2_rsm_request( | 669 def query_data_2_rsm_request( |
680 self, | 670 self, query_data: Dict[str, List[str]] |
681 query_data: Dict[str, List[str]] | |
682 ) -> rsm.RSMRequest: | 671 ) -> rsm.RSMRequest: |
683 """Get RSM kwargs to use with RSMRequest from query data""" | 672 """Get RSM kwargs to use with RSMRequest from query data""" |
684 page = query_data.get("page") | 673 page = query_data.get("page") |
685 | 674 |
686 if page == ["first"]: | 675 if page == ["first"]: |
688 elif page == ["last"]: | 677 elif page == ["last"]: |
689 return rsm.RSMRequest(max_=PAGE_SIZE) | 678 return rsm.RSMRequest(max_=PAGE_SIZE) |
690 else: | 679 else: |
691 for query_key in ("index", "before", "after"): | 680 for query_key in ("index", "before", "after"): |
692 try: | 681 try: |
693 kwargs={query_key: query_data[query_key][0], "max_": PAGE_SIZE} | 682 kwargs = {query_key: query_data[query_key][0], "max_": PAGE_SIZE} |
694 except (KeyError, IndexError, ValueError): | 683 except (KeyError, IndexError, ValueError): |
695 pass | 684 pass |
696 else: | 685 else: |
697 return rsm.RSMRequest(**kwargs) | 686 return rsm.RSMRequest(**kwargs) |
698 raise ValueError(f"Invalid query data: {query_data!r}") | 687 raise ValueError(f"Invalid query data: {query_data!r}") |
703 data: Optional[dict], | 692 data: Optional[dict], |
704 account_jid: jid.JID, | 693 account_jid: jid.JID, |
705 node: Optional[str], | 694 node: Optional[str], |
706 ap_account: str, | 695 ap_account: str, |
707 ap_url: str, | 696 ap_url: str, |
708 query_data: Dict[str, List[str]] | 697 query_data: Dict[str, List[str]], |
709 ) -> dict: | 698 ) -> dict: |
710 if node is None: | 699 if node is None: |
711 node = self.apg._m.namespace | 700 node = self.apg._m.namespace |
712 # we only keep useful keys, and sort to have consistent URL which can | 701 # we only keep useful keys, and sort to have consistent URL which can |
713 # be used as ID | 702 # be used as ID |
717 items, metadata = await self.apg._p.get_items( | 706 items, metadata = await self.apg._p.get_items( |
718 client=self.apg.client, | 707 client=self.apg.client, |
719 service=account_jid, | 708 service=account_jid, |
720 node=node, | 709 node=node, |
721 rsm_request=self.query_data_2_rsm_request(query_data), | 710 rsm_request=self.query_data_2_rsm_request(query_data), |
722 extra = {C.KEY_USE_CACHE: False} | 711 extra={C.KEY_USE_CACHE: False}, |
723 ) | 712 ) |
724 except error.StanzaError as e: | 713 except error.StanzaError as e: |
725 log.warning(f"Can't get data from pubsub node {node} at {account_jid}: {e}") | 714 log.warning(f"Can't get data from pubsub node {node} at {account_jid}: {e}") |
726 return {} | 715 return {} |
727 | 716 |
728 base_url = self.get_canonical_url(request) | 717 base_url = self.get_canonical_url(request) |
729 url = f"{base_url}?{parse.urlencode(query_data, True)}" | 718 url = f"{base_url}?{parse.urlencode(query_data, True)}" |
730 if node and node.startswith(self.apg._events.namespace): | 719 if node and node.startswith(self.apg._events.namespace): |
731 ordered_items = [ | 720 ordered_items = [ |
732 await self.apg.ap_events.event_data_2_ap_item( | 721 await self.apg.ap_events.event_data_2_ap_item( |
733 self.apg._events.event_elt_2_event_data(item), | 722 self.apg._events.event_elt_2_event_data(item), account_jid |
734 account_jid | |
735 ) | 723 ) |
736 for item in reversed(items) | 724 for item in reversed(items) |
737 ] | 725 ] |
738 else: | 726 else: |
739 ordered_items = [ | 727 ordered_items = [ |
740 await self.apg.mb_data_2_ap_item( | 728 await self.apg.mb_data_2_ap_item( |
741 self.apg.client, | 729 self.apg.client, |
742 await self.apg._m.item_2_mb_data( | 730 await self.apg._m.item_2_mb_data( |
743 self.apg.client, | 731 self.apg.client, item, account_jid, node |
744 item, | 732 ), |
745 account_jid, | |
746 node | |
747 ) | |
748 ) | 733 ) |
749 for item in reversed(items) | 734 for item in reversed(items) |
750 ] | 735 ] |
751 ret_data = { | 736 ret_data = { |
752 "@context": ["https://www.w3.org/ns/activitystreams"], | 737 "@context": ["https://www.w3.org/ns/activitystreams"], |
753 "id": url, | 738 "id": url, |
754 "type": "OrderedCollectionPage", | 739 "type": "OrderedCollectionPage", |
755 "partOf": base_url, | 740 "partOf": base_url, |
756 "orderedItems": ordered_items | 741 "orderedItems": ordered_items, |
757 } | 742 } |
758 | 743 |
759 if "rsm" not in metadata: | 744 if "rsm" not in metadata: |
760 # no RSM available, we return what we have | 745 # no RSM available, we return what we have |
761 return ret_data | 746 return ret_data |
762 | 747 |
763 # AP OrderedCollection must be in reversed chronological order, thus the opposite | 748 # AP OrderedCollection must be in reversed chronological order, thus the opposite |
764 # of what we get with RSM (at least with Libervia Pubsub) | 749 # of what we get with RSM (at least with Libervia Pubsub) |
765 if not metadata["complete"]: | 750 if not metadata["complete"]: |
766 try: | 751 try: |
767 last= metadata["rsm"]["last"] | 752 last = metadata["rsm"]["last"] |
768 except KeyError: | 753 except KeyError: |
769 last = None | 754 last = None |
770 ret_data["prev"] = f"{base_url}?{parse.urlencode({'after': last})}" | 755 ret_data["prev"] = f"{base_url}?{parse.urlencode({'after': last})}" |
771 if metadata["rsm"]["index"] != 0: | 756 if metadata["rsm"]["index"] != 0: |
772 try: | 757 try: |
773 first= metadata["rsm"]["first"] | 758 first = metadata["rsm"]["first"] |
774 except KeyError: | 759 except KeyError: |
775 first = None | 760 first = None |
776 ret_data["next"] = f"{base_url}?{parse.urlencode({'before': first})}" | 761 ret_data["next"] = f"{base_url}?{parse.urlencode({'before': first})}" |
777 | 762 |
778 return ret_data | 763 return ret_data |
783 data: Optional[dict], | 768 data: Optional[dict], |
784 account_jid: jid.JID, | 769 account_jid: jid.JID, |
785 node: Optional[str], | 770 node: Optional[str], |
786 ap_account: str, | 771 ap_account: str, |
787 ap_url: str, | 772 ap_url: str, |
788 signing_actor: Optional[str] | 773 signing_actor: Optional[str], |
789 ) -> dict: | 774 ) -> dict: |
790 if node is None: | 775 if node is None: |
791 node = self.apg._m.namespace | 776 node = self.apg._m.namespace |
792 | 777 |
793 parsed_url = parse.urlparse(request.uri.decode()) | 778 parsed_url = parse.urlparse(request.uri.decode()) |
807 client=self.apg.client, | 792 client=self.apg.client, |
808 service=account_jid, | 793 service=account_jid, |
809 node=node, | 794 node=node, |
810 max_items=0, | 795 max_items=0, |
811 rsm_request=rsm.RSMRequest(max_=0), | 796 rsm_request=rsm.RSMRequest(max_=0), |
812 extra = {C.KEY_USE_CACHE: False} | 797 extra={C.KEY_USE_CACHE: False}, |
813 ) | 798 ) |
814 except error.StanzaError as e: | 799 except error.StanzaError as e: |
815 log.warning(f"Can't get data from pubsub node {node} at {account_jid}: {e}") | 800 log.warning(f"Can't get data from pubsub node {node} at {account_jid}: {e}") |
816 return {} | 801 return {} |
817 try: | 802 try: |
842 data: Optional[dict], | 827 data: Optional[dict], |
843 account_jid: Optional[jid.JID], | 828 account_jid: Optional[jid.JID], |
844 node: Optional[str], | 829 node: Optional[str], |
845 ap_account: Optional[str], | 830 ap_account: Optional[str], |
846 ap_url: str, | 831 ap_url: str, |
847 signing_actor: Optional[str] | 832 signing_actor: Optional[str], |
848 ) -> None: | 833 ) -> None: |
849 assert data is not None | 834 assert data is not None |
850 if signing_actor is None: | 835 if signing_actor is None: |
851 raise exceptions.InternalError("signing_actor must be set for inbox requests") | 836 raise exceptions.InternalError("signing_actor must be set for inbox requests") |
852 await self.check_signing_actor(requestor_actor_id, data, signing_actor) | 837 await self.check_signing_actor(requestor_actor_id, data, signing_actor) |
853 activity_type = (data.get("type") or "").lower() | 838 activity_type = (data.get("type") or "").lower() |
854 if not activity_type in ACTIVITY_TYPES_LOWER: | 839 if not activity_type in ACTIVITY_TYPES_LOWER: |
855 return self.response_code( | 840 return self.response_code( |
856 request, | 841 request, |
857 http.UNSUPPORTED_MEDIA_TYPE, | 842 http.UNSUPPORTED_MEDIA_TYPE, |
858 f"request is not an activity, ignoring" | 843 f"request is not an activity, ignoring", |
859 ) | 844 ) |
860 | 845 |
861 if account_jid is None and activity_type not in ACTIVIY_NO_ACCOUNT_ALLOWED: | 846 if account_jid is None and activity_type not in ACTIVIY_NO_ACCOUNT_ALLOWED: |
862 return self.response_code( | 847 return self.response_code( |
863 request, | 848 request, |
864 http.UNSUPPORTED_MEDIA_TYPE, | 849 http.UNSUPPORTED_MEDIA_TYPE, |
865 f"{activity_type.title()!r} activity must target an account" | 850 f"{activity_type.title()!r} activity must target an account", |
866 ) | 851 ) |
867 | 852 |
868 try: | 853 try: |
869 method = getattr(self, f"handle_{activity_type}_activity") | 854 method = getattr(self, f"handle_{activity_type}_activity") |
870 except AttributeError: | 855 except AttributeError: |
871 return self.response_code( | 856 return self.response_code( |
872 request, | 857 request, |
873 http.UNSUPPORTED_MEDIA_TYPE, | 858 http.UNSUPPORTED_MEDIA_TYPE, |
874 f"{activity_type.title()} activity is not yet supported" | 859 f"{activity_type.title()} activity is not yet supported", |
875 ) | 860 ) |
876 else: | 861 else: |
877 await method( | 862 await method( |
878 requestor_actor_id, request, data, account_jid, node, ap_account, ap_url, | 863 requestor_actor_id, |
879 signing_actor | 864 request, |
865 data, | |
866 account_jid, | |
867 node, | |
868 ap_account, | |
869 ap_url, | |
870 signing_actor, | |
880 ) | 871 ) |
881 | 872 |
882 async def ap_followers_request( | 873 async def ap_followers_request( |
883 self, | 874 self, |
884 request: "HTTPRequest", | 875 request: "HTTPRequest", |
885 data: Optional[dict], | 876 data: Optional[dict], |
886 account_jid: jid.JID, | 877 account_jid: jid.JID, |
887 node: Optional[str], | 878 node: Optional[str], |
888 ap_account: Optional[str], | 879 ap_account: Optional[str], |
889 ap_url: str, | 880 ap_url: str, |
890 signing_actor: Optional[str] | 881 signing_actor: Optional[str], |
891 ) -> dict: | 882 ) -> dict: |
892 if node is None: | 883 if node is None: |
893 node = self.apg._m.namespace | 884 node = self.apg._m.namespace |
894 client = self.apg.client | 885 client = self.apg.client |
895 subscribers = await self.apg._pps.get_public_node_subscriptions( | 886 subscribers = await self.apg._pps.get_public_node_subscriptions( |
900 if self.apg.is_virtual_jid(subscriber): | 891 if self.apg.is_virtual_jid(subscriber): |
901 # the subscriber is an AP user subscribed with this gateway | 892 # the subscriber is an AP user subscribed with this gateway |
902 ap_account = self.apg._e.unescape(subscriber.user) | 893 ap_account = self.apg._e.unescape(subscriber.user) |
903 else: | 894 else: |
904 # regular XMPP user | 895 # regular XMPP user |
905 ap_account = await self.apg.get_ap_account_from_jid_and_node(subscriber, node) | 896 ap_account = await self.apg.get_ap_account_from_jid_and_node( |
897 subscriber, node | |
898 ) | |
906 followers.append(ap_account) | 899 followers.append(ap_account) |
907 | 900 |
908 url = self.get_canonical_url(request) | 901 url = self.get_canonical_url(request) |
909 return { | 902 return { |
910 "@context": ["https://www.w3.org/ns/activitystreams"], | 903 "@context": ["https://www.w3.org/ns/activitystreams"], |
911 "type": "OrderedCollection", | 904 "type": "OrderedCollection", |
912 "id": url, | |
913 "totalItems": len(subscribers), | |
914 "first": { | |
915 "type": "OrderedCollectionPage", | |
916 "id": url, | 905 "id": url, |
917 "orderedItems": followers | 906 "totalItems": len(subscribers), |
918 } | 907 "first": { |
908 "type": "OrderedCollectionPage", | |
909 "id": url, | |
910 "orderedItems": followers, | |
911 }, | |
919 } | 912 } |
920 | 913 |
921 async def ap_following_request( | 914 async def ap_following_request( |
922 self, | 915 self, |
923 request: "HTTPRequest", | 916 request: "HTTPRequest", |
924 data: Optional[dict], | 917 data: Optional[dict], |
925 account_jid: jid.JID, | 918 account_jid: jid.JID, |
926 node: Optional[str], | 919 node: Optional[str], |
927 ap_account: Optional[str], | 920 ap_account: Optional[str], |
928 ap_url: str, | 921 ap_url: str, |
929 signing_actor: Optional[str] | 922 signing_actor: Optional[str], |
930 ) -> dict[str, Any]: | 923 ) -> dict[str, Any]: |
931 client = self.apg.client | 924 client = self.apg.client |
932 subscriptions = await self.apg._pps.subscriptions( | 925 subscriptions = await self.apg._pps.subscriptions(client, account_jid, node) |
933 client, account_jid, node | |
934 ) | |
935 following = [] | 926 following = [] |
936 for sub_dict in subscriptions: | 927 for sub_dict in subscriptions: |
937 service = jid.JID(sub_dict["service"]) | 928 service = jid.JID(sub_dict["service"]) |
938 if self.apg.is_virtual_jid(service): | 929 if self.apg.is_virtual_jid(service): |
939 # the subscription is to an AP actor with this gateway | 930 # the subscription is to an AP actor with this gateway |
945 ) | 936 ) |
946 following.append(ap_account) | 937 following.append(ap_account) |
947 | 938 |
948 url = self.get_canonical_url(request) | 939 url = self.get_canonical_url(request) |
949 return { | 940 return { |
950 "@context": ["https://www.w3.org/ns/activitystreams"], | 941 "@context": ["https://www.w3.org/ns/activitystreams"], |
951 "type": "OrderedCollection", | 942 "type": "OrderedCollection", |
952 "id": url, | |
953 "totalItems": len(subscriptions), | |
954 "first": { | |
955 "type": "OrderedCollectionPage", | |
956 "id": url, | 943 "id": url, |
957 "orderedItems": following | 944 "totalItems": len(subscriptions), |
958 } | 945 "first": { |
946 "type": "OrderedCollectionPage", | |
947 "id": url, | |
948 "orderedItems": following, | |
949 }, | |
959 } | 950 } |
960 | 951 |
961 def _get_to_log( | 952 def _get_to_log( |
962 self, | 953 self, |
963 request: "HTTPRequest", | 954 request: "HTTPRequest", |
964 data: Optional[dict] = None, | 955 data: Optional[dict] = None, |
965 ) -> List[str]: | 956 ) -> List[str]: |
966 """Get base data to logs in verbose mode""" | 957 """Get base data to logs in verbose mode""" |
967 from pprint import pformat | 958 from pprint import pformat |
959 | |
968 to_log = [ | 960 to_log = [ |
969 "", | 961 "", |
970 f"<<< got {request.method.decode()} request - {request.uri.decode()}" | 962 f"<<< got {request.method.decode()} request - {request.uri.decode()}", |
971 ] | 963 ] |
972 if data is not None: | 964 if data is not None: |
973 to_log.append(pformat(data)) | 965 to_log.append(pformat(data)) |
974 if self.apg.verbose>=3: | 966 if self.apg.verbose >= 3: |
975 headers = "\n".join( | 967 headers = "\n".join( |
976 f" {k.decode()}: {v.decode()}" | 968 f" {k.decode()}: {v.decode()}" |
977 for k,v in request.getAllHeaders().items() | 969 for k, v in request.getAllHeaders().items() |
978 ) | 970 ) |
979 to_log.append(f" headers:\n{headers}") | 971 to_log.append(f" headers:\n{headers}") |
980 return to_log | 972 return to_log |
981 | 973 |
982 def get_requestor_actor_id( | 974 def get_requestor_actor_id( |
983 self, | 975 self, data: dict | None = None, uri_extra_args: list[str] | None = None |
984 data: dict|None = None, | |
985 uri_extra_args: list[str]|None = None | |
986 ) -> str: | 976 ) -> str: |
987 """Find the actor ID of the requestor. | 977 """Find the actor ID of the requestor. |
988 | 978 |
989 The requestor here is actually the local actor which will do the requests to | 979 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 | 980 achieve the task (e.g. retrieve external actor data), not the requestor of the |
1032 ): | 1022 ): |
1033 return self.apg.build_apurl(TYPE_ACTOR, ap_account) | 1023 return self.apg.build_apurl(TYPE_ACTOR, ap_account) |
1034 | 1024 |
1035 # Still nothing, we'll have to use a generic actor. | 1025 # Still nothing, we'll have to use a generic actor. |
1036 log.warning( | 1026 log.warning( |
1037 "Can't find destinee in \"to\" field, using generic requestor for signature." | 1027 'Can\'t find destinee in "to" field, using generic requestor for signature.' |
1038 ) | 1028 ) |
1039 return self.apg.build_apurl( | 1029 return self.apg.build_apurl(TYPE_ACTOR, f"libervia@{self.apg.public_url}") |
1040 TYPE_ACTOR, f"libervia@{self.apg.public_url}" | |
1041 ) | |
1042 | 1030 |
1043 async def ap_request( | 1031 async def ap_request( |
1044 self, | 1032 self, |
1045 request: "HTTPRequest", | 1033 request: "HTTPRequest", |
1046 data: dict|None = None, | 1034 data: dict | None = None, |
1047 signing_actor: str|None = None, | 1035 signing_actor: str | None = None, |
1048 requestor_actor_id: str|None = None, | 1036 requestor_actor_id: str | None = None, |
1049 ) -> None: | 1037 ) -> None: |
1050 if self.apg.verbose: | 1038 if self.apg.verbose: |
1051 to_log = self._get_to_log(request, data) | 1039 to_log = self._get_to_log(request, data) |
1052 | 1040 |
1053 path = request.path.decode() | 1041 path = request.path.decode() |
1054 ap_url = parse.urljoin( | 1042 ap_url = parse.urljoin(f"https://{self.apg.public_url}", path) |
1055 f"https://{self.apg.public_url}", | |
1056 path | |
1057 ) | |
1058 request_type, extra_args = self.apg.parse_apurl(ap_url) | 1043 request_type, extra_args = self.apg.parse_apurl(ap_url) |
1059 | 1044 |
1060 header_accept = request.getHeader("accept") or "" | 1045 header_accept = request.getHeader("accept") or "" |
1061 if ((MEDIA_TYPE_AP not in header_accept | 1046 if ( |
1062 and MEDIA_TYPE_AP_ALT not in header_accept | 1047 MEDIA_TYPE_AP not in header_accept |
1063 and request_type in self.apg.html_redirect)): | 1048 and MEDIA_TYPE_AP_ALT not in header_accept |
1049 and request_type in self.apg.html_redirect | |
1050 ): | |
1064 # this is not a AP request, and we have a redirections for it | 1051 # this is not a AP request, and we have a redirections for it |
1065 kw = {} | 1052 kw = {} |
1066 if extra_args: | 1053 if extra_args: |
1067 kw["jid"], kw["node"] = await self.apg.get_jid_and_node(extra_args[0]) | 1054 kw["jid"], kw["node"] = await self.apg.get_jid_and_node(extra_args[0]) |
1068 kw["jid_user"] = kw["jid"].user | 1055 kw["jid_user"] = kw["jid"].user |
1079 for redirection in redirections: | 1066 for redirection in redirections: |
1080 filters = redirection["filters"] | 1067 filters = redirection["filters"] |
1081 if not filters: | 1068 if not filters: |
1082 break | 1069 break |
1083 # if we have filter, they must all match | 1070 # if we have filter, they must all match |
1084 elif all(v in kw[k] for k,v in filters.items()): | 1071 elif all(v in kw[k] for k, v in filters.items()): |
1085 break | 1072 break |
1086 else: | 1073 else: |
1087 # no redirection is matching | 1074 # no redirection is matching |
1088 redirection = None | 1075 redirection = None |
1089 | 1076 |
1090 if redirection is not None: | 1077 if redirection is not None: |
1091 kw = {k: parse.quote(str(v), safe="") for k,v in kw.items()} | 1078 kw = {k: parse.quote(str(v), safe="") for k, v in kw.items()} |
1092 target_url = redirection["url"].format(**kw) | 1079 target_url = redirection["url"].format(**kw) |
1093 content = web_util.redirectTo(target_url.encode(), request) | 1080 content = web_util.redirectTo(target_url.encode(), request) |
1094 request.write(content) | 1081 request.write(content) |
1095 request.finish() | 1082 request.finish() |
1096 return | 1083 return |
1097 | 1084 |
1098 if requestor_actor_id is None: | 1085 if requestor_actor_id is None: |
1099 requestor_actor_id = self.get_requestor_actor_id( | 1086 requestor_actor_id = self.get_requestor_actor_id(data, extra_args) |
1100 data, extra_args | |
1101 ) | |
1102 if len(extra_args) == 0: | 1087 if len(extra_args) == 0: |
1103 if request_type != "shared_inbox": | 1088 if request_type != "shared_inbox": |
1104 raise exceptions.DataError(f"Invalid request type: {request_type!r}") | 1089 raise exceptions.DataError(f"Invalid request type: {request_type!r}") |
1105 ret_data = await self.ap_inbox_request( | 1090 ret_data = await self.ap_inbox_request( |
1106 requestor_actor_id, request, data, None, None, None, ap_url, signing_actor | 1091 requestor_actor_id, request, data, None, None, None, ap_url, signing_actor |
1119 if len(extra_args) > 1: | 1104 if len(extra_args) > 1: |
1120 log.warning(f"unexpected extra arguments: {extra_args!r}") | 1105 log.warning(f"unexpected extra arguments: {extra_args!r}") |
1121 ap_account = extra_args[0] | 1106 ap_account = extra_args[0] |
1122 account_jid, node = await self.apg.get_jid_and_node(ap_account) | 1107 account_jid, node = await self.apg.get_jid_and_node(ap_account) |
1123 if request_type not in AP_REQUEST_TYPES.get( | 1108 if request_type not in AP_REQUEST_TYPES.get( |
1124 request.method.decode().upper(), [] | 1109 request.method.decode().upper(), [] |
1125 ): | 1110 ): |
1126 raise exceptions.DataError(f"Invalid request type: {request_type!r}") | 1111 raise exceptions.DataError(f"Invalid request type: {request_type!r}") |
1127 method = getattr(self, f"ap_{request_type}_request") | 1112 method = getattr(self, f"ap_{request_type}_request") |
1128 ret_data = await method( | 1113 ret_data = await method( |
1129 requestor_actor_id, request, data, account_jid, node, ap_account, ap_url, signing_actor | 1114 requestor_actor_id, |
1115 request, | |
1116 data, | |
1117 account_jid, | |
1118 node, | |
1119 ap_account, | |
1120 ap_url, | |
1121 signing_actor, | |
1130 ) | 1122 ) |
1131 if ret_data is not None: | 1123 if ret_data is not None: |
1132 request.setHeader("content-type", CONTENT_TYPE_AP) | 1124 request.setHeader("content-type", CONTENT_TYPE_AP) |
1133 request.write(json.dumps(ret_data).encode()) | 1125 request.write(json.dumps(ret_data).encode()) |
1134 if self.apg.verbose: | 1126 if self.apg.verbose: |
1135 to_log.append(f"--- RET (code: {request.code})---") | 1127 to_log.append(f"--- RET (code: {request.code})---") |
1136 if self.apg.verbose>=2: | 1128 if self.apg.verbose >= 2: |
1137 if ret_data is not None: | 1129 if ret_data is not None: |
1138 from pprint import pformat | 1130 from pprint import pformat |
1131 | |
1139 to_log.append(f"{pformat(ret_data)}") | 1132 to_log.append(f"{pformat(ret_data)}") |
1140 to_log.append("---") | 1133 to_log.append("---") |
1141 log.info("\n".join(to_log)) | 1134 log.info("\n".join(to_log)) |
1142 request.finish() | 1135 request.finish() |
1143 | 1136 |
1147 if not isinstance(data, dict): | 1140 if not isinstance(data, dict): |
1148 log.warning(f"JSON data should be an object (uri={request.uri.decode()})") | 1141 log.warning(f"JSON data should be an object (uri={request.uri.decode()})") |
1149 self.response_code( | 1142 self.response_code( |
1150 request, | 1143 request, |
1151 http.BAD_REQUEST, | 1144 http.BAD_REQUEST, |
1152 f"invalid body, was expecting a JSON object" | 1145 f"invalid body, was expecting a JSON object", |
1153 ) | 1146 ) |
1154 request.finish() | 1147 request.finish() |
1155 return | 1148 return |
1156 except (json.JSONDecodeError, ValueError) as e: | 1149 except (json.JSONDecodeError, ValueError) as e: |
1157 self.response_code( | 1150 self.response_code( |
1158 request, | 1151 request, http.BAD_REQUEST, f"invalid json in inbox request: {e}" |
1159 http.BAD_REQUEST, | |
1160 f"invalid json in inbox request: {e}" | |
1161 ) | 1152 ) |
1162 request.finish() | 1153 request.finish() |
1163 return | 1154 return |
1164 else: | 1155 else: |
1165 request.content.seek(0) | 1156 request.content.seek(0) |
1183 if self.apg.verbose: | 1174 if self.apg.verbose: |
1184 to_log = self._get_to_log(request) | 1175 to_log = self._get_to_log(request) |
1185 to_log.append(f" body: {request.content.read()!r}") | 1176 to_log.append(f" body: {request.content.read()!r}") |
1186 request.content.seek(0) | 1177 request.content.seek(0) |
1187 log.info("\n".join(to_log)) | 1178 log.info("\n".join(to_log)) |
1188 self.response_code( | 1179 self.response_code(request, http.FORBIDDEN, f"invalid signature: {e}") |
1189 request, | |
1190 http.FORBIDDEN, | |
1191 f"invalid signature: {e}" | |
1192 ) | |
1193 request.finish() | 1180 request.finish() |
1194 return | 1181 return |
1195 except Exception as e: | 1182 except Exception as e: |
1196 self.response_code( | 1183 self.response_code( |
1197 request, | 1184 request, http.INTERNAL_SERVER_ERROR, f"Can't check signature: {e}" |
1198 http.INTERNAL_SERVER_ERROR, | |
1199 f"Can't check signature: {e}" | |
1200 ) | 1185 ) |
1201 request.finish() | 1186 request.finish() |
1202 return | 1187 return |
1203 | 1188 |
1204 request.setResponseCode(http.ACCEPTED) | 1189 request.setResponseCode(http.ACCEPTED) |
1217 ) | 1202 ) |
1218 except Exception as e: | 1203 except Exception as e: |
1219 self._on_request_error(failure.Failure(e), request) | 1204 self._on_request_error(failure.Failure(e), request) |
1220 | 1205 |
1221 async def check_signing_actor( | 1206 async def check_signing_actor( |
1222 self, | 1207 self, requestor_actor_id: str, data: dict, signing_actor: str |
1223 requestor_actor_id: str, | |
1224 data: dict, | |
1225 signing_actor: str | |
1226 ) -> None: | 1208 ) -> None: |
1227 """That that signing actor correspond to actor declared in data | 1209 """That that signing actor correspond to actor declared in data |
1228 | 1210 |
1229 @param requestor_actor_id: ID of the actor doing the request. | 1211 @param requestor_actor_id: ID of the actor doing the request. |
1230 @param data: request payload | 1212 @param data: request payload |
1239 raise exceptions.EncryptionError( | 1221 raise exceptions.EncryptionError( |
1240 f"signing actor ({signing_actor}) doesn't match actor in data ({actor})" | 1222 f"signing actor ({signing_actor}) doesn't match actor in data ({actor})" |
1241 ) | 1223 ) |
1242 | 1224 |
1243 async def check_signature( | 1225 async def check_signature( |
1244 self, | 1226 self, requestor_actor_id: str, request: "HTTPRequest" |
1245 requestor_actor_id: str, | |
1246 request: "HTTPRequest" | |
1247 ) -> str: | 1227 ) -> str: |
1248 """Check and validate HTTP signature | 1228 """Check and validate HTTP signature |
1249 | 1229 |
1250 @param requestor_actor_id: ID of the actor doing the request. | 1230 @param requestor_actor_id: ID of the actor doing the request. |
1251 @return: id of the signing actor | 1231 @return: id of the signing actor |
1262 try: | 1242 try: |
1263 key_id = sign_data["keyId"] | 1243 key_id = sign_data["keyId"] |
1264 except KeyError: | 1244 except KeyError: |
1265 raise exceptions.EncryptionError('"keyId" is missing from signature') | 1245 raise exceptions.EncryptionError('"keyId" is missing from signature') |
1266 algorithm = sign_data.get("algorithm", HS2019) | 1246 algorithm = sign_data.get("algorithm", HS2019) |
1267 signed_headers = sign_data.get( | 1247 signed_headers = ( |
1268 "headers", | 1248 sign_data.get("headers", "(created)" if algorithm == HS2019 else "date") |
1269 "(created)" if algorithm==HS2019 else "date" | 1249 .lower() |
1270 ).lower().split() | 1250 .split() |
1251 ) | |
1271 try: | 1252 try: |
1272 headers_to_check = SIGN_HEADERS[None] + SIGN_HEADERS[request.method] | 1253 headers_to_check = SIGN_HEADERS[None] + SIGN_HEADERS[request.method] |
1273 except KeyError: | 1254 except KeyError: |
1274 raise exceptions.InternalError( | 1255 raise exceptions.InternalError( |
1275 f"there should be a list of headers for {request.method} method" | 1256 f"there should be a list of headers for {request.method} method" |
1282 if len(set(header).intersection(signed_headers)) == 0: | 1263 if len(set(header).intersection(signed_headers)) == 0: |
1283 raise exceptions.EncryptionError( | 1264 raise exceptions.EncryptionError( |
1284 f"at least one of following header must be signed: {header}" | 1265 f"at least one of following header must be signed: {header}" |
1285 ) | 1266 ) |
1286 elif header not in signed_headers: | 1267 elif header not in signed_headers: |
1287 raise exceptions.EncryptionError( | 1268 raise exceptions.EncryptionError(f"the {header!r} header must be signed") |
1288 f"the {header!r} header must be signed" | |
1289 ) | |
1290 | 1269 |
1291 body = request.content.read() | 1270 body = request.content.read() |
1292 request.content.seek(0) | 1271 request.content.seek(0) |
1293 headers = {} | 1272 headers = {} |
1294 for to_sign in signed_headers: | 1273 for to_sign in signed_headers: |
1327 # as we need original host if a proxy has modified the header | 1306 # as we need original host if a proxy has modified the header |
1328 forwarded = request.getHeader("forwarded") | 1307 forwarded = request.getHeader("forwarded") |
1329 if forwarded is not None: | 1308 if forwarded is not None: |
1330 try: | 1309 try: |
1331 host = [ | 1310 host = [ |
1332 f[5:] for f in forwarded.split(";") | 1311 f[5:] |
1312 for f in forwarded.split(";") | |
1333 if f.startswith("host=") | 1313 if f.startswith("host=") |
1334 ][0] or None | 1314 ][0] or None |
1335 except IndexError: | 1315 except IndexError: |
1336 host = None | 1316 host = None |
1337 else: | 1317 else: |
1340 host = request.getHeader("x-forwarded-host") | 1320 host = request.getHeader("x-forwarded-host") |
1341 if host: | 1321 if host: |
1342 value = host | 1322 value = host |
1343 elif to_sign == "digest": | 1323 elif to_sign == "digest": |
1344 hashes = { | 1324 hashes = { |
1345 algo.lower(): hash_ for algo, hash_ in ( | 1325 algo.lower(): hash_ |
1326 for algo, hash_ in ( | |
1346 digest.split("=", 1) for digest in value.split(",") | 1327 digest.split("=", 1) for digest in value.split(",") |
1347 ) | 1328 ) |
1348 } | 1329 } |
1349 try: | 1330 try: |
1350 given_digest = hashes["sha-256"] | 1331 given_digest = hashes["sha-256"] |
1365 if "(created)" in headers: | 1346 if "(created)" in headers: |
1366 created = float(headers["created"]) | 1347 created = float(headers["created"]) |
1367 else: | 1348 else: |
1368 created = date_utils.date_parse(headers["date"]) | 1349 created = date_utils.date_parse(headers["date"]) |
1369 | 1350 |
1370 | |
1371 try: | 1351 try: |
1372 expires = float(headers["expires"]) | 1352 expires = float(headers["expires"]) |
1373 except KeyError: | 1353 except KeyError: |
1374 pass | 1354 pass |
1375 else: | 1355 else: |
1384 if created > limit_ts: | 1364 if created > limit_ts: |
1385 raise exceptions.EncryptionError("Signature has expired") | 1365 raise exceptions.EncryptionError("Signature has expired") |
1386 | 1366 |
1387 try: | 1367 try: |
1388 return await self.apg.check_signature( | 1368 return await self.apg.check_signature( |
1389 requestor_actor_id, | 1369 requestor_actor_id, sign_data["signature"], key_id, headers |
1390 sign_data["signature"], | |
1391 key_id, | |
1392 headers | |
1393 ) | 1370 ) |
1394 except exceptions.EncryptionError: | 1371 except exceptions.EncryptionError: |
1395 method, url = headers["(request-target)"].rsplit(' ', 1) | 1372 method, url = headers["(request-target)"].rsplit(" ", 1) |
1396 headers["(request-target)"] = f"{method} {parse.unquote(url)}" | 1373 headers["(request-target)"] = f"{method} {parse.unquote(url)}" |
1397 log.debug( | 1374 log.debug( |
1398 "Using workaround for (request-target) encoding bug in signature, " | 1375 "Using workaround for (request-target) encoding bug in signature, " |
1399 "see https://github.com/mastodon/mastodon/issues/18871" | 1376 "see https://github.com/mastodon/mastodon/issues/18871" |
1400 ) | 1377 ) |
1401 return await self.apg.check_signature( | 1378 return await self.apg.check_signature( |
1402 requestor_actor_id, | 1379 requestor_actor_id, sign_data["signature"], key_id, headers |
1403 sign_data["signature"], | |
1404 key_id, | |
1405 headers | |
1406 ) | 1380 ) |
1407 | 1381 |
1408 def render(self, request): | 1382 def render(self, request): |
1409 request.setHeader("server", VERSION) | 1383 request.setHeader("server", VERSION) |
1410 return super().render(request) | 1384 return super().render(request) |