1# Copyright 2021 The Matrix.org Foundation C.I.C. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14import logging 15from typing import Dict, List, Mapping, Optional, Sequence, Tuple, Type, Union 16 17from typing_extensions import Literal 18 19import synapse 20from synapse.api.errors import Codes, SynapseError 21from synapse.api.room_versions import RoomVersions 22from synapse.api.urls import FEDERATION_UNSTABLE_PREFIX, FEDERATION_V2_PREFIX 23from synapse.federation.transport.server._base import ( 24 Authenticator, 25 BaseFederationServlet, 26) 27from synapse.http.servlet import ( 28 parse_boolean_from_args, 29 parse_integer_from_args, 30 parse_string_from_args, 31 parse_strings_from_args, 32) 33from synapse.server import HomeServer 34from synapse.types import JsonDict 35from synapse.util.ratelimitutils import FederationRateLimiter 36from synapse.util.versionstring import get_version_string 37 38logger = logging.getLogger(__name__) 39 40 41class BaseFederationServerServlet(BaseFederationServlet): 42 """Abstract base class for federation servlet classes which provides a federation server handler. 43 44 See BaseFederationServlet for more information. 45 """ 46 47 def __init__( 48 self, 49 hs: HomeServer, 50 authenticator: Authenticator, 51 ratelimiter: FederationRateLimiter, 52 server_name: str, 53 ): 54 super().__init__(hs, authenticator, ratelimiter, server_name) 55 self.handler = hs.get_federation_server() 56 57 58class FederationSendServlet(BaseFederationServerServlet): 59 PATH = "/send/(?P<transaction_id>[^/]*)/?" 60 61 # We ratelimit manually in the handler as we queue up the requests and we 62 # don't want to fill up the ratelimiter with blocked requests. 63 RATELIMIT = False 64 65 # This is when someone is trying to send us a bunch of data. 66 async def on_PUT( 67 self, 68 origin: str, 69 content: JsonDict, 70 query: Dict[bytes, List[bytes]], 71 transaction_id: str, 72 ) -> Tuple[int, JsonDict]: 73 """Called on PUT /send/<transaction_id>/ 74 75 Args: 76 transaction_id: The transaction_id associated with this request. This 77 is *not* None. 78 79 Returns: 80 Tuple of `(code, response)`, where 81 `response` is a python dict to be converted into JSON that is 82 used as the response body. 83 """ 84 # Parse the request 85 try: 86 transaction_data = content 87 88 logger.debug("Decoded %s: %s", transaction_id, str(transaction_data)) 89 90 logger.info( 91 "Received txn %s from %s. (PDUs: %d, EDUs: %d)", 92 transaction_id, 93 origin, 94 len(transaction_data.get("pdus", [])), 95 len(transaction_data.get("edus", [])), 96 ) 97 98 except Exception as e: 99 logger.exception(e) 100 return 400, {"error": "Invalid transaction"} 101 102 code, response = await self.handler.on_incoming_transaction( 103 origin, transaction_id, self.server_name, transaction_data 104 ) 105 106 return code, response 107 108 109class FederationEventServlet(BaseFederationServerServlet): 110 PATH = "/event/(?P<event_id>[^/]*)/?" 111 112 # This is when someone asks for a data item for a given server data_id pair. 113 async def on_GET( 114 self, 115 origin: str, 116 content: Literal[None], 117 query: Dict[bytes, List[bytes]], 118 event_id: str, 119 ) -> Tuple[int, Union[JsonDict, str]]: 120 return await self.handler.on_pdu_request(origin, event_id) 121 122 123class FederationStateV1Servlet(BaseFederationServerServlet): 124 PATH = "/state/(?P<room_id>[^/]*)/?" 125 126 # This is when someone asks for all data for a given room. 127 async def on_GET( 128 self, 129 origin: str, 130 content: Literal[None], 131 query: Dict[bytes, List[bytes]], 132 room_id: str, 133 ) -> Tuple[int, JsonDict]: 134 return await self.handler.on_room_state_request( 135 origin, 136 room_id, 137 parse_string_from_args(query, "event_id", None, required=False), 138 ) 139 140 141class FederationStateIdsServlet(BaseFederationServerServlet): 142 PATH = "/state_ids/(?P<room_id>[^/]*)/?" 143 144 async def on_GET( 145 self, 146 origin: str, 147 content: Literal[None], 148 query: Dict[bytes, List[bytes]], 149 room_id: str, 150 ) -> Tuple[int, JsonDict]: 151 return await self.handler.on_state_ids_request( 152 origin, 153 room_id, 154 parse_string_from_args(query, "event_id", None, required=True), 155 ) 156 157 158class FederationBackfillServlet(BaseFederationServerServlet): 159 PATH = "/backfill/(?P<room_id>[^/]*)/?" 160 161 async def on_GET( 162 self, 163 origin: str, 164 content: Literal[None], 165 query: Dict[bytes, List[bytes]], 166 room_id: str, 167 ) -> Tuple[int, JsonDict]: 168 versions = [x.decode("ascii") for x in query[b"v"]] 169 limit = parse_integer_from_args(query, "limit", None) 170 171 if not limit: 172 return 400, {"error": "Did not include limit param"} 173 174 return await self.handler.on_backfill_request(origin, room_id, versions, limit) 175 176 177class FederationTimestampLookupServlet(BaseFederationServerServlet): 178 """ 179 API endpoint to fetch the `event_id` of the closest event to the given 180 timestamp (`ts` query parameter) in the given direction (`dir` query 181 parameter). 182 183 Useful for other homeservers when they're unable to find an event locally. 184 185 `ts` is a timestamp in milliseconds where we will find the closest event in 186 the given direction. 187 188 `dir` can be `f` or `b` to indicate forwards and backwards in time from the 189 given timestamp. 190 191 GET /_matrix/federation/unstable/org.matrix.msc3030/timestamp_to_event/<roomID>?ts=<timestamp>&dir=<direction> 192 { 193 "event_id": ... 194 } 195 """ 196 197 PATH = "/timestamp_to_event/(?P<room_id>[^/]*)/?" 198 PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc3030" 199 200 async def on_GET( 201 self, 202 origin: str, 203 content: Literal[None], 204 query: Dict[bytes, List[bytes]], 205 room_id: str, 206 ) -> Tuple[int, JsonDict]: 207 timestamp = parse_integer_from_args(query, "ts", required=True) 208 direction = parse_string_from_args( 209 query, "dir", default="f", allowed_values=["f", "b"], required=True 210 ) 211 212 return await self.handler.on_timestamp_to_event_request( 213 origin, room_id, timestamp, direction 214 ) 215 216 217class FederationQueryServlet(BaseFederationServerServlet): 218 PATH = "/query/(?P<query_type>[^/]*)" 219 220 # This is when we receive a server-server Query 221 async def on_GET( 222 self, 223 origin: str, 224 content: Literal[None], 225 query: Dict[bytes, List[bytes]], 226 query_type: str, 227 ) -> Tuple[int, JsonDict]: 228 args = {k.decode("utf8"): v[0].decode("utf-8") for k, v in query.items()} 229 args["origin"] = origin 230 return await self.handler.on_query_request(query_type, args) 231 232 233class FederationMakeJoinServlet(BaseFederationServerServlet): 234 PATH = "/make_join/(?P<room_id>[^/]*)/(?P<user_id>[^/]*)" 235 236 async def on_GET( 237 self, 238 origin: str, 239 content: Literal[None], 240 query: Dict[bytes, List[bytes]], 241 room_id: str, 242 user_id: str, 243 ) -> Tuple[int, JsonDict]: 244 """ 245 Args: 246 origin: The authenticated server_name of the calling server 247 248 content: (GETs don't have bodies) 249 250 query: Query params from the request. 251 252 **kwargs: the dict mapping keys to path components as specified in 253 the path match regexp. 254 255 Returns: 256 Tuple of (response code, response object) 257 """ 258 supported_versions = parse_strings_from_args(query, "ver", encoding="utf-8") 259 if supported_versions is None: 260 supported_versions = ["1"] 261 262 result = await self.handler.on_make_join_request( 263 origin, room_id, user_id, supported_versions=supported_versions 264 ) 265 return 200, result 266 267 268class FederationMakeLeaveServlet(BaseFederationServerServlet): 269 PATH = "/make_leave/(?P<room_id>[^/]*)/(?P<user_id>[^/]*)" 270 271 async def on_GET( 272 self, 273 origin: str, 274 content: Literal[None], 275 query: Dict[bytes, List[bytes]], 276 room_id: str, 277 user_id: str, 278 ) -> Tuple[int, JsonDict]: 279 result = await self.handler.on_make_leave_request(origin, room_id, user_id) 280 return 200, result 281 282 283class FederationV1SendLeaveServlet(BaseFederationServerServlet): 284 PATH = "/send_leave/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)" 285 286 async def on_PUT( 287 self, 288 origin: str, 289 content: JsonDict, 290 query: Dict[bytes, List[bytes]], 291 room_id: str, 292 event_id: str, 293 ) -> Tuple[int, Tuple[int, JsonDict]]: 294 result = await self.handler.on_send_leave_request(origin, content, room_id) 295 return 200, (200, result) 296 297 298class FederationV2SendLeaveServlet(BaseFederationServerServlet): 299 PATH = "/send_leave/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)" 300 301 PREFIX = FEDERATION_V2_PREFIX 302 303 async def on_PUT( 304 self, 305 origin: str, 306 content: JsonDict, 307 query: Dict[bytes, List[bytes]], 308 room_id: str, 309 event_id: str, 310 ) -> Tuple[int, JsonDict]: 311 result = await self.handler.on_send_leave_request(origin, content, room_id) 312 return 200, result 313 314 315class FederationMakeKnockServlet(BaseFederationServerServlet): 316 PATH = "/make_knock/(?P<room_id>[^/]*)/(?P<user_id>[^/]*)" 317 318 async def on_GET( 319 self, 320 origin: str, 321 content: Literal[None], 322 query: Dict[bytes, List[bytes]], 323 room_id: str, 324 user_id: str, 325 ) -> Tuple[int, JsonDict]: 326 # Retrieve the room versions the remote homeserver claims to support 327 supported_versions = parse_strings_from_args( 328 query, "ver", required=True, encoding="utf-8" 329 ) 330 331 result = await self.handler.on_make_knock_request( 332 origin, room_id, user_id, supported_versions=supported_versions 333 ) 334 return 200, result 335 336 337class FederationV1SendKnockServlet(BaseFederationServerServlet): 338 PATH = "/send_knock/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)" 339 340 async def on_PUT( 341 self, 342 origin: str, 343 content: JsonDict, 344 query: Dict[bytes, List[bytes]], 345 room_id: str, 346 event_id: str, 347 ) -> Tuple[int, JsonDict]: 348 result = await self.handler.on_send_knock_request(origin, content, room_id) 349 return 200, result 350 351 352class FederationEventAuthServlet(BaseFederationServerServlet): 353 PATH = "/event_auth/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)" 354 355 async def on_GET( 356 self, 357 origin: str, 358 content: Literal[None], 359 query: Dict[bytes, List[bytes]], 360 room_id: str, 361 event_id: str, 362 ) -> Tuple[int, JsonDict]: 363 return await self.handler.on_event_auth(origin, room_id, event_id) 364 365 366class FederationV1SendJoinServlet(BaseFederationServerServlet): 367 PATH = "/send_join/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)" 368 369 async def on_PUT( 370 self, 371 origin: str, 372 content: JsonDict, 373 query: Dict[bytes, List[bytes]], 374 room_id: str, 375 event_id: str, 376 ) -> Tuple[int, Tuple[int, JsonDict]]: 377 # TODO(paul): assert that event_id parsed from path actually 378 # match those given in content 379 result = await self.handler.on_send_join_request(origin, content, room_id) 380 return 200, (200, result) 381 382 383class FederationV2SendJoinServlet(BaseFederationServerServlet): 384 PATH = "/send_join/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)" 385 386 PREFIX = FEDERATION_V2_PREFIX 387 388 async def on_PUT( 389 self, 390 origin: str, 391 content: JsonDict, 392 query: Dict[bytes, List[bytes]], 393 room_id: str, 394 event_id: str, 395 ) -> Tuple[int, JsonDict]: 396 # TODO(paul): assert that event_id parsed from path actually 397 # match those given in content 398 result = await self.handler.on_send_join_request(origin, content, room_id) 399 return 200, result 400 401 402class FederationV1InviteServlet(BaseFederationServerServlet): 403 PATH = "/invite/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)" 404 405 async def on_PUT( 406 self, 407 origin: str, 408 content: JsonDict, 409 query: Dict[bytes, List[bytes]], 410 room_id: str, 411 event_id: str, 412 ) -> Tuple[int, Tuple[int, JsonDict]]: 413 # We don't get a room version, so we have to assume its EITHER v1 or 414 # v2. This is "fine" as the only difference between V1 and V2 is the 415 # state resolution algorithm, and we don't use that for processing 416 # invites 417 result = await self.handler.on_invite_request( 418 origin, content, room_version_id=RoomVersions.V1.identifier 419 ) 420 421 # V1 federation API is defined to return a content of `[200, {...}]` 422 # due to a historical bug. 423 return 200, (200, result) 424 425 426class FederationV2InviteServlet(BaseFederationServerServlet): 427 PATH = "/invite/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)" 428 429 PREFIX = FEDERATION_V2_PREFIX 430 431 async def on_PUT( 432 self, 433 origin: str, 434 content: JsonDict, 435 query: Dict[bytes, List[bytes]], 436 room_id: str, 437 event_id: str, 438 ) -> Tuple[int, JsonDict]: 439 # TODO(paul): assert that room_id/event_id parsed from path actually 440 # match those given in content 441 442 room_version = content["room_version"] 443 event = content["event"] 444 invite_room_state = content["invite_room_state"] 445 446 # Synapse expects invite_room_state to be in unsigned, as it is in v1 447 # API 448 449 event.setdefault("unsigned", {})["invite_room_state"] = invite_room_state 450 451 result = await self.handler.on_invite_request( 452 origin, event, room_version_id=room_version 453 ) 454 return 200, result 455 456 457class FederationThirdPartyInviteExchangeServlet(BaseFederationServerServlet): 458 PATH = "/exchange_third_party_invite/(?P<room_id>[^/]*)" 459 460 async def on_PUT( 461 self, 462 origin: str, 463 content: JsonDict, 464 query: Dict[bytes, List[bytes]], 465 room_id: str, 466 ) -> Tuple[int, JsonDict]: 467 await self.handler.on_exchange_third_party_invite_request(content) 468 return 200, {} 469 470 471class FederationClientKeysQueryServlet(BaseFederationServerServlet): 472 PATH = "/user/keys/query" 473 474 async def on_POST( 475 self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]] 476 ) -> Tuple[int, JsonDict]: 477 return await self.handler.on_query_client_keys(origin, content) 478 479 480class FederationUserDevicesQueryServlet(BaseFederationServerServlet): 481 PATH = "/user/devices/(?P<user_id>[^/]*)" 482 483 async def on_GET( 484 self, 485 origin: str, 486 content: Literal[None], 487 query: Dict[bytes, List[bytes]], 488 user_id: str, 489 ) -> Tuple[int, JsonDict]: 490 return await self.handler.on_query_user_devices(origin, user_id) 491 492 493class FederationClientKeysClaimServlet(BaseFederationServerServlet): 494 PATH = "/user/keys/claim" 495 496 async def on_POST( 497 self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]] 498 ) -> Tuple[int, JsonDict]: 499 response = await self.handler.on_claim_client_keys(origin, content) 500 return 200, response 501 502 503class FederationGetMissingEventsServlet(BaseFederationServerServlet): 504 # TODO(paul): Why does this path alone end with "/?" optional? 505 PATH = "/get_missing_events/(?P<room_id>[^/]*)/?" 506 507 async def on_POST( 508 self, 509 origin: str, 510 content: JsonDict, 511 query: Dict[bytes, List[bytes]], 512 room_id: str, 513 ) -> Tuple[int, JsonDict]: 514 limit = int(content.get("limit", 10)) 515 earliest_events = content.get("earliest_events", []) 516 latest_events = content.get("latest_events", []) 517 518 result = await self.handler.on_get_missing_events( 519 origin, 520 room_id=room_id, 521 earliest_events=earliest_events, 522 latest_events=latest_events, 523 limit=limit, 524 ) 525 526 return 200, result 527 528 529class On3pidBindServlet(BaseFederationServerServlet): 530 PATH = "/3pid/onbind" 531 532 REQUIRE_AUTH = False 533 534 async def on_POST( 535 self, origin: Optional[str], content: JsonDict, query: Dict[bytes, List[bytes]] 536 ) -> Tuple[int, JsonDict]: 537 if "invites" in content: 538 last_exception = None 539 for invite in content["invites"]: 540 try: 541 if "signed" not in invite or "token" not in invite["signed"]: 542 message = ( 543 "Rejecting received notification of third-" 544 "party invite without signed: %s" % (invite,) 545 ) 546 logger.info(message) 547 raise SynapseError(400, message) 548 await self.handler.exchange_third_party_invite( 549 invite["sender"], 550 invite["mxid"], 551 invite["room_id"], 552 invite["signed"], 553 ) 554 except Exception as e: 555 last_exception = e 556 if last_exception: 557 raise last_exception 558 return 200, {} 559 560 561class FederationVersionServlet(BaseFederationServlet): 562 PATH = "/version" 563 564 REQUIRE_AUTH = False 565 566 async def on_GET( 567 self, 568 origin: Optional[str], 569 content: Literal[None], 570 query: Dict[bytes, List[bytes]], 571 ) -> Tuple[int, JsonDict]: 572 return ( 573 200, 574 {"server": {"name": "Synapse", "version": get_version_string(synapse)}}, 575 ) 576 577 578class FederationSpaceSummaryServlet(BaseFederationServlet): 579 PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc2946" 580 PATH = "/spaces/(?P<room_id>[^/]*)" 581 582 def __init__( 583 self, 584 hs: HomeServer, 585 authenticator: Authenticator, 586 ratelimiter: FederationRateLimiter, 587 server_name: str, 588 ): 589 super().__init__(hs, authenticator, ratelimiter, server_name) 590 self.handler = hs.get_room_summary_handler() 591 592 async def on_GET( 593 self, 594 origin: str, 595 content: Literal[None], 596 query: Mapping[bytes, Sequence[bytes]], 597 room_id: str, 598 ) -> Tuple[int, JsonDict]: 599 suggested_only = parse_boolean_from_args(query, "suggested_only", default=False) 600 601 max_rooms_per_space = parse_integer_from_args(query, "max_rooms_per_space") 602 if max_rooms_per_space is not None and max_rooms_per_space < 0: 603 raise SynapseError( 604 400, 605 "Value for 'max_rooms_per_space' must be a non-negative integer", 606 Codes.BAD_JSON, 607 ) 608 609 exclude_rooms = parse_strings_from_args(query, "exclude_rooms", default=[]) 610 611 return 200, await self.handler.federation_space_summary( 612 origin, room_id, suggested_only, max_rooms_per_space, exclude_rooms 613 ) 614 615 # TODO When switching to the stable endpoint, remove the POST handler. 616 async def on_POST( 617 self, 618 origin: str, 619 content: JsonDict, 620 query: Mapping[bytes, Sequence[bytes]], 621 room_id: str, 622 ) -> Tuple[int, JsonDict]: 623 suggested_only = content.get("suggested_only", False) 624 if not isinstance(suggested_only, bool): 625 raise SynapseError( 626 400, "'suggested_only' must be a boolean", Codes.BAD_JSON 627 ) 628 629 exclude_rooms = content.get("exclude_rooms", []) 630 if not isinstance(exclude_rooms, list) or any( 631 not isinstance(x, str) for x in exclude_rooms 632 ): 633 raise SynapseError(400, "bad value for 'exclude_rooms'", Codes.BAD_JSON) 634 635 max_rooms_per_space = content.get("max_rooms_per_space") 636 if max_rooms_per_space is not None: 637 if not isinstance(max_rooms_per_space, int): 638 raise SynapseError( 639 400, "bad value for 'max_rooms_per_space'", Codes.BAD_JSON 640 ) 641 if max_rooms_per_space < 0: 642 raise SynapseError( 643 400, 644 "Value for 'max_rooms_per_space' must be a non-negative integer", 645 Codes.BAD_JSON, 646 ) 647 648 return 200, await self.handler.federation_space_summary( 649 origin, room_id, suggested_only, max_rooms_per_space, exclude_rooms 650 ) 651 652 653class FederationRoomHierarchyServlet(BaseFederationServlet): 654 PATH = "/hierarchy/(?P<room_id>[^/]*)" 655 656 def __init__( 657 self, 658 hs: HomeServer, 659 authenticator: Authenticator, 660 ratelimiter: FederationRateLimiter, 661 server_name: str, 662 ): 663 super().__init__(hs, authenticator, ratelimiter, server_name) 664 self.handler = hs.get_room_summary_handler() 665 666 async def on_GET( 667 self, 668 origin: str, 669 content: Literal[None], 670 query: Mapping[bytes, Sequence[bytes]], 671 room_id: str, 672 ) -> Tuple[int, JsonDict]: 673 suggested_only = parse_boolean_from_args(query, "suggested_only", default=False) 674 return 200, await self.handler.get_federation_hierarchy( 675 origin, room_id, suggested_only 676 ) 677 678 679class FederationRoomHierarchyUnstableServlet(FederationRoomHierarchyServlet): 680 PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc2946" 681 682 683class RoomComplexityServlet(BaseFederationServlet): 684 """ 685 Indicates to other servers how complex (and therefore likely 686 resource-intensive) a public room this server knows about is. 687 """ 688 689 PATH = "/rooms/(?P<room_id>[^/]*)/complexity" 690 PREFIX = FEDERATION_UNSTABLE_PREFIX 691 692 def __init__( 693 self, 694 hs: HomeServer, 695 authenticator: Authenticator, 696 ratelimiter: FederationRateLimiter, 697 server_name: str, 698 ): 699 super().__init__(hs, authenticator, ratelimiter, server_name) 700 self._store = self.hs.get_datastore() 701 702 async def on_GET( 703 self, 704 origin: str, 705 content: Literal[None], 706 query: Dict[bytes, List[bytes]], 707 room_id: str, 708 ) -> Tuple[int, JsonDict]: 709 is_public = await self._store.is_room_world_readable_or_publicly_joinable( 710 room_id 711 ) 712 713 if not is_public: 714 raise SynapseError(404, "Room not found", errcode=Codes.INVALID_PARAM) 715 716 complexity = await self._store.get_room_complexity(room_id) 717 return 200, complexity 718 719 720FEDERATION_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = ( 721 FederationSendServlet, 722 FederationEventServlet, 723 FederationStateV1Servlet, 724 FederationStateIdsServlet, 725 FederationBackfillServlet, 726 FederationTimestampLookupServlet, 727 FederationQueryServlet, 728 FederationMakeJoinServlet, 729 FederationMakeLeaveServlet, 730 FederationEventServlet, 731 FederationV1SendJoinServlet, 732 FederationV2SendJoinServlet, 733 FederationV1SendLeaveServlet, 734 FederationV2SendLeaveServlet, 735 FederationV1InviteServlet, 736 FederationV2InviteServlet, 737 FederationGetMissingEventsServlet, 738 FederationEventAuthServlet, 739 FederationClientKeysQueryServlet, 740 FederationUserDevicesQueryServlet, 741 FederationClientKeysClaimServlet, 742 FederationThirdPartyInviteExchangeServlet, 743 On3pidBindServlet, 744 FederationVersionServlet, 745 RoomComplexityServlet, 746 FederationSpaceSummaryServlet, 747 FederationRoomHierarchyServlet, 748 FederationRoomHierarchyUnstableServlet, 749 FederationV1SendKnockServlet, 750 FederationMakeKnockServlet, 751) 752