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)