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