server.py 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014-2016 OpenMarket Ltd
  3. # Copyright 2018 New Vector Ltd
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. import functools
  17. import logging
  18. import re
  19. from twisted.internet import defer
  20. import synapse
  21. from synapse.api.errors import Codes, FederationDeniedError, SynapseError
  22. from synapse.api.urls import FEDERATION_PREFIX as PREFIX
  23. from synapse.http.endpoint import parse_and_validate_server_name
  24. from synapse.http.server import JsonResource
  25. from synapse.http.servlet import (
  26. parse_boolean_from_args,
  27. parse_integer_from_args,
  28. parse_json_object_from_request,
  29. parse_string_from_args,
  30. )
  31. from synapse.types import ThirdPartyInstanceID, get_domain_from_id
  32. from synapse.util.logcontext import run_in_background
  33. from synapse.util.ratelimitutils import FederationRateLimiter
  34. from synapse.util.versionstring import get_version_string
  35. logger = logging.getLogger(__name__)
  36. class TransportLayerServer(JsonResource):
  37. """Handles incoming federation HTTP requests"""
  38. def __init__(self, hs):
  39. self.hs = hs
  40. self.clock = hs.get_clock()
  41. super(TransportLayerServer, self).__init__(hs, canonical_json=False)
  42. self.authenticator = Authenticator(hs)
  43. self.ratelimiter = FederationRateLimiter(
  44. self.clock,
  45. window_size=hs.config.federation_rc_window_size,
  46. sleep_limit=hs.config.federation_rc_sleep_limit,
  47. sleep_msec=hs.config.federation_rc_sleep_delay,
  48. reject_limit=hs.config.federation_rc_reject_limit,
  49. concurrent_requests=hs.config.federation_rc_concurrent,
  50. )
  51. self.register_servlets()
  52. def register_servlets(self):
  53. register_servlets(
  54. self.hs,
  55. resource=self,
  56. ratelimiter=self.ratelimiter,
  57. authenticator=self.authenticator,
  58. )
  59. class AuthenticationError(SynapseError):
  60. """There was a problem authenticating the request"""
  61. pass
  62. class NoAuthenticationError(AuthenticationError):
  63. """The request had no authentication information"""
  64. pass
  65. class Authenticator(object):
  66. def __init__(self, hs):
  67. self.keyring = hs.get_keyring()
  68. self.server_name = hs.hostname
  69. self.store = hs.get_datastore()
  70. self.federation_domain_whitelist = hs.config.federation_domain_whitelist
  71. # A method just so we can pass 'self' as the authenticator to the Servlets
  72. @defer.inlineCallbacks
  73. def authenticate_request(self, request, content):
  74. json_request = {
  75. "method": request.method,
  76. "uri": request.uri,
  77. "destination": self.server_name,
  78. "signatures": {},
  79. }
  80. if content is not None:
  81. json_request["content"] = content
  82. origin = None
  83. auth_headers = request.requestHeaders.getRawHeaders(b"Authorization")
  84. if not auth_headers:
  85. raise NoAuthenticationError(
  86. 401, "Missing Authorization headers", Codes.UNAUTHORIZED,
  87. )
  88. for auth in auth_headers:
  89. if auth.startswith(b"X-Matrix"):
  90. (origin, key, sig) = _parse_auth_header(auth)
  91. json_request["origin"] = origin
  92. json_request["signatures"].setdefault(origin, {})[key] = sig
  93. if (
  94. self.federation_domain_whitelist is not None and
  95. origin not in self.federation_domain_whitelist
  96. ):
  97. raise FederationDeniedError(origin)
  98. if not json_request["signatures"]:
  99. raise NoAuthenticationError(
  100. 401, "Missing Authorization headers", Codes.UNAUTHORIZED,
  101. )
  102. yield self.keyring.verify_json_for_server(origin, json_request)
  103. logger.info("Request from %s", origin)
  104. request.authenticated_entity = origin
  105. # If we get a valid signed request from the other side, its probably
  106. # alive
  107. retry_timings = yield self.store.get_destination_retry_timings(origin)
  108. if retry_timings and retry_timings["retry_last_ts"]:
  109. run_in_background(self._reset_retry_timings, origin)
  110. defer.returnValue(origin)
  111. @defer.inlineCallbacks
  112. def _reset_retry_timings(self, origin):
  113. try:
  114. logger.info("Marking origin %r as up", origin)
  115. yield self.store.set_destination_retry_timings(origin, 0, 0)
  116. except Exception:
  117. logger.exception("Error resetting retry timings on %s", origin)
  118. def _parse_auth_header(header_bytes):
  119. """Parse an X-Matrix auth header
  120. Args:
  121. header_bytes (bytes): header value
  122. Returns:
  123. Tuple[str, str, str]: origin, key id, signature.
  124. Raises:
  125. AuthenticationError if the header could not be parsed
  126. """
  127. try:
  128. header_str = header_bytes.decode('utf-8')
  129. params = header_str.split(" ")[1].split(",")
  130. param_dict = dict(kv.split("=") for kv in params)
  131. def strip_quotes(value):
  132. if value.startswith("\""):
  133. return value[1:-1]
  134. else:
  135. return value
  136. origin = strip_quotes(param_dict["origin"])
  137. # ensure that the origin is a valid server name
  138. parse_and_validate_server_name(origin)
  139. key = strip_quotes(param_dict["key"])
  140. sig = strip_quotes(param_dict["sig"])
  141. return origin, key, sig
  142. except Exception as e:
  143. logger.warn(
  144. "Error parsing auth header '%s': %s",
  145. header_bytes.decode('ascii', 'replace'),
  146. e,
  147. )
  148. raise AuthenticationError(
  149. 400, "Malformed Authorization header", Codes.UNAUTHORIZED,
  150. )
  151. class BaseFederationServlet(object):
  152. """Abstract base class for federation servlet classes.
  153. The servlet object should have a PATH attribute which takes the form of a regexp to
  154. match against the request path (excluding the /federation/v1 prefix).
  155. The servlet should also implement one or more of on_GET, on_POST, on_PUT, to match
  156. the appropriate HTTP method. These methods have the signature:
  157. on_<METHOD>(self, origin, content, query, **kwargs)
  158. With arguments:
  159. origin (unicode|None): The authenticated server_name of the calling server,
  160. unless REQUIRE_AUTH is set to False and authentication failed.
  161. content (unicode|None): decoded json body of the request. None if the
  162. request was a GET.
  163. query (dict[bytes, list[bytes]]): Query params from the request. url-decoded
  164. (ie, '+' and '%xx' are decoded) but note that it is *not* utf8-decoded
  165. yet.
  166. **kwargs (dict[unicode, unicode]): the dict mapping keys to path
  167. components as specified in the path match regexp.
  168. Returns:
  169. Deferred[(int, object)|None]: either (response code, response object) to
  170. return a JSON response, or None if the request has already been handled.
  171. Raises:
  172. SynapseError: to return an error code
  173. Exception: other exceptions will be caught, logged, and a 500 will be
  174. returned.
  175. """
  176. REQUIRE_AUTH = True
  177. def __init__(self, handler, authenticator, ratelimiter, server_name):
  178. self.handler = handler
  179. self.authenticator = authenticator
  180. self.ratelimiter = ratelimiter
  181. def _wrap(self, func):
  182. authenticator = self.authenticator
  183. ratelimiter = self.ratelimiter
  184. @defer.inlineCallbacks
  185. @functools.wraps(func)
  186. def new_func(request, *args, **kwargs):
  187. """ A callback which can be passed to HttpServer.RegisterPaths
  188. Args:
  189. request (twisted.web.http.Request):
  190. *args: unused?
  191. **kwargs (dict[unicode, unicode]): the dict mapping keys to path
  192. components as specified in the path match regexp.
  193. Returns:
  194. Deferred[(int, object)|None]: (response code, response object) as returned
  195. by the callback method. None if the request has already been handled.
  196. """
  197. content = None
  198. if request.method in ["PUT", "POST"]:
  199. # TODO: Handle other method types? other content types?
  200. content = parse_json_object_from_request(request)
  201. try:
  202. origin = yield authenticator.authenticate_request(request, content)
  203. except NoAuthenticationError:
  204. origin = None
  205. if self.REQUIRE_AUTH:
  206. logger.warn("authenticate_request failed: missing authentication")
  207. raise
  208. except Exception as e:
  209. logger.warn("authenticate_request failed: %s", e)
  210. raise
  211. if origin:
  212. with ratelimiter.ratelimit(origin) as d:
  213. yield d
  214. response = yield func(
  215. origin, content, request.args, *args, **kwargs
  216. )
  217. else:
  218. response = yield func(
  219. origin, content, request.args, *args, **kwargs
  220. )
  221. defer.returnValue(response)
  222. # Extra logic that functools.wraps() doesn't finish
  223. new_func.__self__ = func.__self__
  224. return new_func
  225. def register(self, server):
  226. pattern = re.compile("^" + PREFIX + self.PATH + "$")
  227. for method in ("GET", "PUT", "POST"):
  228. code = getattr(self, "on_%s" % (method), None)
  229. if code is None:
  230. continue
  231. server.register_paths(method, (pattern,), self._wrap(code))
  232. class FederationSendServlet(BaseFederationServlet):
  233. PATH = "/send/(?P<transaction_id>[^/]*)/"
  234. def __init__(self, handler, server_name, **kwargs):
  235. super(FederationSendServlet, self).__init__(
  236. handler, server_name=server_name, **kwargs
  237. )
  238. self.server_name = server_name
  239. # This is when someone is trying to send us a bunch of data.
  240. @defer.inlineCallbacks
  241. def on_PUT(self, origin, content, query, transaction_id):
  242. """ Called on PUT /send/<transaction_id>/
  243. Args:
  244. request (twisted.web.http.Request): The HTTP request.
  245. transaction_id (str): The transaction_id associated with this
  246. request. This is *not* None.
  247. Returns:
  248. Deferred: Results in a tuple of `(code, response)`, where
  249. `response` is a python dict to be converted into JSON that is
  250. used as the response body.
  251. """
  252. # Parse the request
  253. try:
  254. transaction_data = content
  255. logger.debug(
  256. "Decoded %s: %s",
  257. transaction_id, str(transaction_data)
  258. )
  259. logger.info(
  260. "Received txn %s from %s. (PDUs: %d, EDUs: %d)",
  261. transaction_id, origin,
  262. len(transaction_data.get("pdus", [])),
  263. len(transaction_data.get("edus", [])),
  264. )
  265. # We should ideally be getting this from the security layer.
  266. # origin = body["origin"]
  267. # Add some extra data to the transaction dict that isn't included
  268. # in the request body.
  269. transaction_data.update(
  270. transaction_id=transaction_id,
  271. destination=self.server_name
  272. )
  273. except Exception as e:
  274. logger.exception(e)
  275. defer.returnValue((400, {"error": "Invalid transaction"}))
  276. return
  277. try:
  278. code, response = yield self.handler.on_incoming_transaction(
  279. origin, transaction_data,
  280. )
  281. except Exception:
  282. logger.exception("on_incoming_transaction failed")
  283. raise
  284. defer.returnValue((code, response))
  285. class FederationPullServlet(BaseFederationServlet):
  286. PATH = "/pull/"
  287. # This is for when someone asks us for everything since version X
  288. def on_GET(self, origin, content, query):
  289. return self.handler.on_pull_request(query["origin"][0], query["v"])
  290. class FederationEventServlet(BaseFederationServlet):
  291. PATH = "/event/(?P<event_id>[^/]*)/"
  292. # This is when someone asks for a data item for a given server data_id pair.
  293. def on_GET(self, origin, content, query, event_id):
  294. return self.handler.on_pdu_request(origin, event_id)
  295. class FederationStateServlet(BaseFederationServlet):
  296. PATH = "/state/(?P<context>[^/]*)/"
  297. # This is when someone asks for all data for a given context.
  298. def on_GET(self, origin, content, query, context):
  299. return self.handler.on_context_state_request(
  300. origin,
  301. context,
  302. query.get("event_id", [None])[0],
  303. )
  304. class FederationStateIdsServlet(BaseFederationServlet):
  305. PATH = "/state_ids/(?P<room_id>[^/]*)/"
  306. def on_GET(self, origin, content, query, room_id):
  307. return self.handler.on_state_ids_request(
  308. origin,
  309. room_id,
  310. query.get("event_id", [None])[0],
  311. )
  312. class FederationBackfillServlet(BaseFederationServlet):
  313. PATH = "/backfill/(?P<context>[^/]*)/"
  314. def on_GET(self, origin, content, query, context):
  315. versions = query["v"]
  316. limits = query["limit"]
  317. if not limits:
  318. return defer.succeed((400, {"error": "Did not include limit param"}))
  319. limit = int(limits[-1])
  320. return self.handler.on_backfill_request(origin, context, versions, limit)
  321. class FederationQueryServlet(BaseFederationServlet):
  322. PATH = "/query/(?P<query_type>[^/]*)"
  323. # This is when we receive a server-server Query
  324. def on_GET(self, origin, content, query, query_type):
  325. return self.handler.on_query_request(
  326. query_type,
  327. {k: v[0].decode("utf-8") for k, v in query.items()}
  328. )
  329. class FederationMakeJoinServlet(BaseFederationServlet):
  330. PATH = "/make_join/(?P<context>[^/]*)/(?P<user_id>[^/]*)"
  331. @defer.inlineCallbacks
  332. def on_GET(self, origin, _content, query, context, user_id):
  333. """
  334. Args:
  335. origin (unicode): The authenticated server_name of the calling server
  336. _content (None): (GETs don't have bodies)
  337. query (dict[bytes, list[bytes]]): Query params from the request.
  338. **kwargs (dict[unicode, unicode]): the dict mapping keys to path
  339. components as specified in the path match regexp.
  340. Returns:
  341. Deferred[(int, object)|None]: either (response code, response object) to
  342. return a JSON response, or None if the request has already been handled.
  343. """
  344. versions = query.get(b'ver')
  345. if versions is not None:
  346. supported_versions = [v.decode("utf-8") for v in versions]
  347. else:
  348. supported_versions = ["1"]
  349. content = yield self.handler.on_make_join_request(
  350. origin, context, user_id,
  351. supported_versions=supported_versions,
  352. )
  353. defer.returnValue((200, content))
  354. class FederationMakeLeaveServlet(BaseFederationServlet):
  355. PATH = "/make_leave/(?P<context>[^/]*)/(?P<user_id>[^/]*)"
  356. @defer.inlineCallbacks
  357. def on_GET(self, origin, content, query, context, user_id):
  358. content = yield self.handler.on_make_leave_request(
  359. origin, context, user_id,
  360. )
  361. defer.returnValue((200, content))
  362. class FederationSendLeaveServlet(BaseFederationServlet):
  363. PATH = "/send_leave/(?P<room_id>[^/]*)/(?P<event_id>[^/]*)"
  364. @defer.inlineCallbacks
  365. def on_PUT(self, origin, content, query, room_id, event_id):
  366. content = yield self.handler.on_send_leave_request(origin, content)
  367. defer.returnValue((200, content))
  368. class FederationEventAuthServlet(BaseFederationServlet):
  369. PATH = "/event_auth/(?P<context>[^/]*)/(?P<event_id>[^/]*)"
  370. def on_GET(self, origin, content, query, context, event_id):
  371. return self.handler.on_event_auth(origin, context, event_id)
  372. class FederationSendJoinServlet(BaseFederationServlet):
  373. PATH = "/send_join/(?P<context>[^/]*)/(?P<event_id>[^/]*)"
  374. @defer.inlineCallbacks
  375. def on_PUT(self, origin, content, query, context, event_id):
  376. # TODO(paul): assert that context/event_id parsed from path actually
  377. # match those given in content
  378. content = yield self.handler.on_send_join_request(origin, content)
  379. defer.returnValue((200, content))
  380. class FederationInviteServlet(BaseFederationServlet):
  381. PATH = "/invite/(?P<context>[^/]*)/(?P<event_id>[^/]*)"
  382. @defer.inlineCallbacks
  383. def on_PUT(self, origin, content, query, context, event_id):
  384. # TODO(paul): assert that context/event_id parsed from path actually
  385. # match those given in content
  386. content = yield self.handler.on_invite_request(origin, content)
  387. defer.returnValue((200, content))
  388. class FederationThirdPartyInviteExchangeServlet(BaseFederationServlet):
  389. PATH = "/exchange_third_party_invite/(?P<room_id>[^/]*)"
  390. @defer.inlineCallbacks
  391. def on_PUT(self, origin, content, query, room_id):
  392. content = yield self.handler.on_exchange_third_party_invite_request(
  393. origin, room_id, content
  394. )
  395. defer.returnValue((200, content))
  396. class FederationClientKeysQueryServlet(BaseFederationServlet):
  397. PATH = "/user/keys/query"
  398. def on_POST(self, origin, content, query):
  399. return self.handler.on_query_client_keys(origin, content)
  400. class FederationUserDevicesQueryServlet(BaseFederationServlet):
  401. PATH = "/user/devices/(?P<user_id>[^/]*)"
  402. def on_GET(self, origin, content, query, user_id):
  403. return self.handler.on_query_user_devices(origin, user_id)
  404. class FederationClientKeysClaimServlet(BaseFederationServlet):
  405. PATH = "/user/keys/claim"
  406. @defer.inlineCallbacks
  407. def on_POST(self, origin, content, query):
  408. response = yield self.handler.on_claim_client_keys(origin, content)
  409. defer.returnValue((200, response))
  410. class FederationQueryAuthServlet(BaseFederationServlet):
  411. PATH = "/query_auth/(?P<context>[^/]*)/(?P<event_id>[^/]*)"
  412. @defer.inlineCallbacks
  413. def on_POST(self, origin, content, query, context, event_id):
  414. new_content = yield self.handler.on_query_auth_request(
  415. origin, content, context, event_id
  416. )
  417. defer.returnValue((200, new_content))
  418. class FederationGetMissingEventsServlet(BaseFederationServlet):
  419. # TODO(paul): Why does this path alone end with "/?" optional?
  420. PATH = "/get_missing_events/(?P<room_id>[^/]*)/?"
  421. @defer.inlineCallbacks
  422. def on_POST(self, origin, content, query, room_id):
  423. limit = int(content.get("limit", 10))
  424. min_depth = int(content.get("min_depth", 0))
  425. earliest_events = content.get("earliest_events", [])
  426. latest_events = content.get("latest_events", [])
  427. content = yield self.handler.on_get_missing_events(
  428. origin,
  429. room_id=room_id,
  430. earliest_events=earliest_events,
  431. latest_events=latest_events,
  432. min_depth=min_depth,
  433. limit=limit,
  434. )
  435. defer.returnValue((200, content))
  436. class On3pidBindServlet(BaseFederationServlet):
  437. PATH = "/3pid/onbind"
  438. REQUIRE_AUTH = False
  439. @defer.inlineCallbacks
  440. def on_POST(self, origin, content, query):
  441. if "invites" in content:
  442. last_exception = None
  443. for invite in content["invites"]:
  444. try:
  445. if "signed" not in invite or "token" not in invite["signed"]:
  446. message = ("Rejecting received notification of third-"
  447. "party invite without signed: %s" % (invite,))
  448. logger.info(message)
  449. raise SynapseError(400, message)
  450. yield self.handler.exchange_third_party_invite(
  451. invite["sender"],
  452. invite["mxid"],
  453. invite["room_id"],
  454. invite["signed"],
  455. )
  456. except Exception as e:
  457. last_exception = e
  458. if last_exception:
  459. raise last_exception
  460. defer.returnValue((200, {}))
  461. class OpenIdUserInfo(BaseFederationServlet):
  462. """
  463. Exchange a bearer token for information about a user.
  464. The response format should be compatible with:
  465. http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
  466. GET /openid/userinfo?access_token=ABDEFGH HTTP/1.1
  467. HTTP/1.1 200 OK
  468. Content-Type: application/json
  469. {
  470. "sub": "@userpart:example.org",
  471. }
  472. """
  473. PATH = "/openid/userinfo"
  474. REQUIRE_AUTH = False
  475. @defer.inlineCallbacks
  476. def on_GET(self, origin, content, query):
  477. token = query.get("access_token", [None])[0]
  478. if token is None:
  479. defer.returnValue((401, {
  480. "errcode": "M_MISSING_TOKEN", "error": "Access Token required"
  481. }))
  482. return
  483. user_id = yield self.handler.on_openid_userinfo(token)
  484. if user_id is None:
  485. defer.returnValue((401, {
  486. "errcode": "M_UNKNOWN_TOKEN",
  487. "error": "Access Token unknown or expired"
  488. }))
  489. defer.returnValue((200, {"sub": user_id}))
  490. class PublicRoomList(BaseFederationServlet):
  491. """
  492. Fetch the public room list for this server.
  493. This API returns information in the same format as /publicRooms on the
  494. client API, but will only ever include local public rooms and hence is
  495. intended for consumption by other home servers.
  496. GET /publicRooms HTTP/1.1
  497. HTTP/1.1 200 OK
  498. Content-Type: application/json
  499. {
  500. "chunk": [
  501. {
  502. "aliases": [
  503. "#test:localhost"
  504. ],
  505. "guest_can_join": false,
  506. "name": "test room",
  507. "num_joined_members": 3,
  508. "room_id": "!whkydVegtvatLfXmPN:localhost",
  509. "world_readable": false
  510. }
  511. ],
  512. "end": "END",
  513. "start": "START"
  514. }
  515. """
  516. PATH = "/publicRooms"
  517. @defer.inlineCallbacks
  518. def on_GET(self, origin, content, query):
  519. limit = parse_integer_from_args(query, "limit", 0)
  520. since_token = parse_string_from_args(query, "since", None)
  521. include_all_networks = parse_boolean_from_args(
  522. query, "include_all_networks", False
  523. )
  524. third_party_instance_id = parse_string_from_args(
  525. query, "third_party_instance_id", None
  526. )
  527. if include_all_networks:
  528. network_tuple = None
  529. elif third_party_instance_id:
  530. network_tuple = ThirdPartyInstanceID.from_string(third_party_instance_id)
  531. else:
  532. network_tuple = ThirdPartyInstanceID(None, None)
  533. data = yield self.handler.get_local_public_room_list(
  534. limit, since_token,
  535. network_tuple=network_tuple
  536. )
  537. defer.returnValue((200, data))
  538. class FederationVersionServlet(BaseFederationServlet):
  539. PATH = "/version"
  540. REQUIRE_AUTH = False
  541. def on_GET(self, origin, content, query):
  542. return defer.succeed((200, {
  543. "server": {
  544. "name": "Synapse",
  545. "version": get_version_string(synapse)
  546. },
  547. }))
  548. class FederationGroupsProfileServlet(BaseFederationServlet):
  549. """Get/set the basic profile of a group on behalf of a user
  550. """
  551. PATH = "/groups/(?P<group_id>[^/]*)/profile$"
  552. @defer.inlineCallbacks
  553. def on_GET(self, origin, content, query, group_id):
  554. requester_user_id = parse_string_from_args(query, "requester_user_id")
  555. if get_domain_from_id(requester_user_id) != origin:
  556. raise SynapseError(403, "requester_user_id doesn't match origin")
  557. new_content = yield self.handler.get_group_profile(
  558. group_id, requester_user_id
  559. )
  560. defer.returnValue((200, new_content))
  561. @defer.inlineCallbacks
  562. def on_POST(self, origin, content, query, group_id):
  563. requester_user_id = parse_string_from_args(query, "requester_user_id")
  564. if get_domain_from_id(requester_user_id) != origin:
  565. raise SynapseError(403, "requester_user_id doesn't match origin")
  566. new_content = yield self.handler.update_group_profile(
  567. group_id, requester_user_id, content
  568. )
  569. defer.returnValue((200, new_content))
  570. class FederationGroupsSummaryServlet(BaseFederationServlet):
  571. PATH = "/groups/(?P<group_id>[^/]*)/summary$"
  572. @defer.inlineCallbacks
  573. def on_GET(self, origin, content, query, group_id):
  574. requester_user_id = parse_string_from_args(query, "requester_user_id")
  575. if get_domain_from_id(requester_user_id) != origin:
  576. raise SynapseError(403, "requester_user_id doesn't match origin")
  577. new_content = yield self.handler.get_group_summary(
  578. group_id, requester_user_id
  579. )
  580. defer.returnValue((200, new_content))
  581. class FederationGroupsRoomsServlet(BaseFederationServlet):
  582. """Get the rooms in a group on behalf of a user
  583. """
  584. PATH = "/groups/(?P<group_id>[^/]*)/rooms$"
  585. @defer.inlineCallbacks
  586. def on_GET(self, origin, content, query, group_id):
  587. requester_user_id = parse_string_from_args(query, "requester_user_id")
  588. if get_domain_from_id(requester_user_id) != origin:
  589. raise SynapseError(403, "requester_user_id doesn't match origin")
  590. new_content = yield self.handler.get_rooms_in_group(
  591. group_id, requester_user_id
  592. )
  593. defer.returnValue((200, new_content))
  594. class FederationGroupsAddRoomsServlet(BaseFederationServlet):
  595. """Add/remove room from group
  596. """
  597. PATH = "/groups/(?P<group_id>[^/]*)/room/(?P<room_id>[^/]*)$"
  598. @defer.inlineCallbacks
  599. def on_POST(self, origin, content, query, group_id, room_id):
  600. requester_user_id = parse_string_from_args(query, "requester_user_id")
  601. if get_domain_from_id(requester_user_id) != origin:
  602. raise SynapseError(403, "requester_user_id doesn't match origin")
  603. new_content = yield self.handler.add_room_to_group(
  604. group_id, requester_user_id, room_id, content
  605. )
  606. defer.returnValue((200, new_content))
  607. @defer.inlineCallbacks
  608. def on_DELETE(self, origin, content, query, group_id, room_id):
  609. requester_user_id = parse_string_from_args(query, "requester_user_id")
  610. if get_domain_from_id(requester_user_id) != origin:
  611. raise SynapseError(403, "requester_user_id doesn't match origin")
  612. new_content = yield self.handler.remove_room_from_group(
  613. group_id, requester_user_id, room_id,
  614. )
  615. defer.returnValue((200, new_content))
  616. class FederationGroupsAddRoomsConfigServlet(BaseFederationServlet):
  617. """Update room config in group
  618. """
  619. PATH = (
  620. "/groups/(?P<group_id>[^/]*)/room/(?P<room_id>[^/]*)"
  621. "/config/(?P<config_key>[^/]*)$"
  622. )
  623. @defer.inlineCallbacks
  624. def on_POST(self, origin, content, query, group_id, room_id, config_key):
  625. requester_user_id = parse_string_from_args(query, "requester_user_id")
  626. if get_domain_from_id(requester_user_id) != origin:
  627. raise SynapseError(403, "requester_user_id doesn't match origin")
  628. result = yield self.groups_handler.update_room_in_group(
  629. group_id, requester_user_id, room_id, config_key, content,
  630. )
  631. defer.returnValue((200, result))
  632. class FederationGroupsUsersServlet(BaseFederationServlet):
  633. """Get the users in a group on behalf of a user
  634. """
  635. PATH = "/groups/(?P<group_id>[^/]*)/users$"
  636. @defer.inlineCallbacks
  637. def on_GET(self, origin, content, query, group_id):
  638. requester_user_id = parse_string_from_args(query, "requester_user_id")
  639. if get_domain_from_id(requester_user_id) != origin:
  640. raise SynapseError(403, "requester_user_id doesn't match origin")
  641. new_content = yield self.handler.get_users_in_group(
  642. group_id, requester_user_id
  643. )
  644. defer.returnValue((200, new_content))
  645. class FederationGroupsInvitedUsersServlet(BaseFederationServlet):
  646. """Get the users that have been invited to a group
  647. """
  648. PATH = "/groups/(?P<group_id>[^/]*)/invited_users$"
  649. @defer.inlineCallbacks
  650. def on_GET(self, origin, content, query, group_id):
  651. requester_user_id = parse_string_from_args(query, "requester_user_id")
  652. if get_domain_from_id(requester_user_id) != origin:
  653. raise SynapseError(403, "requester_user_id doesn't match origin")
  654. new_content = yield self.handler.get_invited_users_in_group(
  655. group_id, requester_user_id
  656. )
  657. defer.returnValue((200, new_content))
  658. class FederationGroupsInviteServlet(BaseFederationServlet):
  659. """Ask a group server to invite someone to the group
  660. """
  661. PATH = "/groups/(?P<group_id>[^/]*)/users/(?P<user_id>[^/]*)/invite$"
  662. @defer.inlineCallbacks
  663. def on_POST(self, origin, content, query, group_id, user_id):
  664. requester_user_id = parse_string_from_args(query, "requester_user_id")
  665. if get_domain_from_id(requester_user_id) != origin:
  666. raise SynapseError(403, "requester_user_id doesn't match origin")
  667. new_content = yield self.handler.invite_to_group(
  668. group_id, user_id, requester_user_id, content,
  669. )
  670. defer.returnValue((200, new_content))
  671. class FederationGroupsAcceptInviteServlet(BaseFederationServlet):
  672. """Accept an invitation from the group server
  673. """
  674. PATH = "/groups/(?P<group_id>[^/]*)/users/(?P<user_id>[^/]*)/accept_invite$"
  675. @defer.inlineCallbacks
  676. def on_POST(self, origin, content, query, group_id, user_id):
  677. if get_domain_from_id(user_id) != origin:
  678. raise SynapseError(403, "user_id doesn't match origin")
  679. new_content = yield self.handler.accept_invite(
  680. group_id, user_id, content,
  681. )
  682. defer.returnValue((200, new_content))
  683. class FederationGroupsJoinServlet(BaseFederationServlet):
  684. """Attempt to join a group
  685. """
  686. PATH = "/groups/(?P<group_id>[^/]*)/users/(?P<user_id>[^/]*)/join$"
  687. @defer.inlineCallbacks
  688. def on_POST(self, origin, content, query, group_id, user_id):
  689. if get_domain_from_id(user_id) != origin:
  690. raise SynapseError(403, "user_id doesn't match origin")
  691. new_content = yield self.handler.join_group(
  692. group_id, user_id, content,
  693. )
  694. defer.returnValue((200, new_content))
  695. class FederationGroupsRemoveUserServlet(BaseFederationServlet):
  696. """Leave or kick a user from the group
  697. """
  698. PATH = "/groups/(?P<group_id>[^/]*)/users/(?P<user_id>[^/]*)/remove$"
  699. @defer.inlineCallbacks
  700. def on_POST(self, origin, content, query, group_id, user_id):
  701. requester_user_id = parse_string_from_args(query, "requester_user_id")
  702. if get_domain_from_id(requester_user_id) != origin:
  703. raise SynapseError(403, "requester_user_id doesn't match origin")
  704. new_content = yield self.handler.remove_user_from_group(
  705. group_id, user_id, requester_user_id, content,
  706. )
  707. defer.returnValue((200, new_content))
  708. class FederationGroupsLocalInviteServlet(BaseFederationServlet):
  709. """A group server has invited a local user
  710. """
  711. PATH = "/groups/local/(?P<group_id>[^/]*)/users/(?P<user_id>[^/]*)/invite$"
  712. @defer.inlineCallbacks
  713. def on_POST(self, origin, content, query, group_id, user_id):
  714. if get_domain_from_id(group_id) != origin:
  715. raise SynapseError(403, "group_id doesn't match origin")
  716. new_content = yield self.handler.on_invite(
  717. group_id, user_id, content,
  718. )
  719. defer.returnValue((200, new_content))
  720. class FederationGroupsRemoveLocalUserServlet(BaseFederationServlet):
  721. """A group server has removed a local user
  722. """
  723. PATH = "/groups/local/(?P<group_id>[^/]*)/users/(?P<user_id>[^/]*)/remove$"
  724. @defer.inlineCallbacks
  725. def on_POST(self, origin, content, query, group_id, user_id):
  726. if get_domain_from_id(group_id) != origin:
  727. raise SynapseError(403, "user_id doesn't match origin")
  728. new_content = yield self.handler.user_removed_from_group(
  729. group_id, user_id, content,
  730. )
  731. defer.returnValue((200, new_content))
  732. class FederationGroupsRenewAttestaionServlet(BaseFederationServlet):
  733. """A group or user's server renews their attestation
  734. """
  735. PATH = "/groups/(?P<group_id>[^/]*)/renew_attestation/(?P<user_id>[^/]*)$"
  736. @defer.inlineCallbacks
  737. def on_POST(self, origin, content, query, group_id, user_id):
  738. # We don't need to check auth here as we check the attestation signatures
  739. new_content = yield self.handler.on_renew_attestation(
  740. group_id, user_id, content
  741. )
  742. defer.returnValue((200, new_content))
  743. class FederationGroupsSummaryRoomsServlet(BaseFederationServlet):
  744. """Add/remove a room from the group summary, with optional category.
  745. Matches both:
  746. - /groups/:group/summary/rooms/:room_id
  747. - /groups/:group/summary/categories/:category/rooms/:room_id
  748. """
  749. PATH = (
  750. "/groups/(?P<group_id>[^/]*)/summary"
  751. "(/categories/(?P<category_id>[^/]+))?"
  752. "/rooms/(?P<room_id>[^/]*)$"
  753. )
  754. @defer.inlineCallbacks
  755. def on_POST(self, origin, content, query, group_id, category_id, room_id):
  756. requester_user_id = parse_string_from_args(query, "requester_user_id")
  757. if get_domain_from_id(requester_user_id) != origin:
  758. raise SynapseError(403, "requester_user_id doesn't match origin")
  759. if category_id == "":
  760. raise SynapseError(400, "category_id cannot be empty string")
  761. resp = yield self.handler.update_group_summary_room(
  762. group_id, requester_user_id,
  763. room_id=room_id,
  764. category_id=category_id,
  765. content=content,
  766. )
  767. defer.returnValue((200, resp))
  768. @defer.inlineCallbacks
  769. def on_DELETE(self, origin, content, query, group_id, category_id, room_id):
  770. requester_user_id = parse_string_from_args(query, "requester_user_id")
  771. if get_domain_from_id(requester_user_id) != origin:
  772. raise SynapseError(403, "requester_user_id doesn't match origin")
  773. if category_id == "":
  774. raise SynapseError(400, "category_id cannot be empty string")
  775. resp = yield self.handler.delete_group_summary_room(
  776. group_id, requester_user_id,
  777. room_id=room_id,
  778. category_id=category_id,
  779. )
  780. defer.returnValue((200, resp))
  781. class FederationGroupsCategoriesServlet(BaseFederationServlet):
  782. """Get all categories for a group
  783. """
  784. PATH = (
  785. "/groups/(?P<group_id>[^/]*)/categories/$"
  786. )
  787. @defer.inlineCallbacks
  788. def on_GET(self, origin, content, query, group_id):
  789. requester_user_id = parse_string_from_args(query, "requester_user_id")
  790. if get_domain_from_id(requester_user_id) != origin:
  791. raise SynapseError(403, "requester_user_id doesn't match origin")
  792. resp = yield self.handler.get_group_categories(
  793. group_id, requester_user_id,
  794. )
  795. defer.returnValue((200, resp))
  796. class FederationGroupsCategoryServlet(BaseFederationServlet):
  797. """Add/remove/get a category in a group
  798. """
  799. PATH = (
  800. "/groups/(?P<group_id>[^/]*)/categories/(?P<category_id>[^/]+)$"
  801. )
  802. @defer.inlineCallbacks
  803. def on_GET(self, origin, content, query, group_id, category_id):
  804. requester_user_id = parse_string_from_args(query, "requester_user_id")
  805. if get_domain_from_id(requester_user_id) != origin:
  806. raise SynapseError(403, "requester_user_id doesn't match origin")
  807. resp = yield self.handler.get_group_category(
  808. group_id, requester_user_id, category_id
  809. )
  810. defer.returnValue((200, resp))
  811. @defer.inlineCallbacks
  812. def on_POST(self, origin, content, query, group_id, category_id):
  813. requester_user_id = parse_string_from_args(query, "requester_user_id")
  814. if get_domain_from_id(requester_user_id) != origin:
  815. raise SynapseError(403, "requester_user_id doesn't match origin")
  816. if category_id == "":
  817. raise SynapseError(400, "category_id cannot be empty string")
  818. resp = yield self.handler.upsert_group_category(
  819. group_id, requester_user_id, category_id, content,
  820. )
  821. defer.returnValue((200, resp))
  822. @defer.inlineCallbacks
  823. def on_DELETE(self, origin, content, query, group_id, category_id):
  824. requester_user_id = parse_string_from_args(query, "requester_user_id")
  825. if get_domain_from_id(requester_user_id) != origin:
  826. raise SynapseError(403, "requester_user_id doesn't match origin")
  827. if category_id == "":
  828. raise SynapseError(400, "category_id cannot be empty string")
  829. resp = yield self.handler.delete_group_category(
  830. group_id, requester_user_id, category_id,
  831. )
  832. defer.returnValue((200, resp))
  833. class FederationGroupsRolesServlet(BaseFederationServlet):
  834. """Get roles in a group
  835. """
  836. PATH = (
  837. "/groups/(?P<group_id>[^/]*)/roles/$"
  838. )
  839. @defer.inlineCallbacks
  840. def on_GET(self, origin, content, query, group_id):
  841. requester_user_id = parse_string_from_args(query, "requester_user_id")
  842. if get_domain_from_id(requester_user_id) != origin:
  843. raise SynapseError(403, "requester_user_id doesn't match origin")
  844. resp = yield self.handler.get_group_roles(
  845. group_id, requester_user_id,
  846. )
  847. defer.returnValue((200, resp))
  848. class FederationGroupsRoleServlet(BaseFederationServlet):
  849. """Add/remove/get a role in a group
  850. """
  851. PATH = (
  852. "/groups/(?P<group_id>[^/]*)/roles/(?P<role_id>[^/]+)$"
  853. )
  854. @defer.inlineCallbacks
  855. def on_GET(self, origin, content, query, group_id, role_id):
  856. requester_user_id = parse_string_from_args(query, "requester_user_id")
  857. if get_domain_from_id(requester_user_id) != origin:
  858. raise SynapseError(403, "requester_user_id doesn't match origin")
  859. resp = yield self.handler.get_group_role(
  860. group_id, requester_user_id, role_id
  861. )
  862. defer.returnValue((200, resp))
  863. @defer.inlineCallbacks
  864. def on_POST(self, origin, content, query, group_id, role_id):
  865. requester_user_id = parse_string_from_args(query, "requester_user_id")
  866. if get_domain_from_id(requester_user_id) != origin:
  867. raise SynapseError(403, "requester_user_id doesn't match origin")
  868. if role_id == "":
  869. raise SynapseError(400, "role_id cannot be empty string")
  870. resp = yield self.handler.update_group_role(
  871. group_id, requester_user_id, role_id, content,
  872. )
  873. defer.returnValue((200, resp))
  874. @defer.inlineCallbacks
  875. def on_DELETE(self, origin, content, query, group_id, role_id):
  876. requester_user_id = parse_string_from_args(query, "requester_user_id")
  877. if get_domain_from_id(requester_user_id) != origin:
  878. raise SynapseError(403, "requester_user_id doesn't match origin")
  879. if role_id == "":
  880. raise SynapseError(400, "role_id cannot be empty string")
  881. resp = yield self.handler.delete_group_role(
  882. group_id, requester_user_id, role_id,
  883. )
  884. defer.returnValue((200, resp))
  885. class FederationGroupsSummaryUsersServlet(BaseFederationServlet):
  886. """Add/remove a user from the group summary, with optional role.
  887. Matches both:
  888. - /groups/:group/summary/users/:user_id
  889. - /groups/:group/summary/roles/:role/users/:user_id
  890. """
  891. PATH = (
  892. "/groups/(?P<group_id>[^/]*)/summary"
  893. "(/roles/(?P<role_id>[^/]+))?"
  894. "/users/(?P<user_id>[^/]*)$"
  895. )
  896. @defer.inlineCallbacks
  897. def on_POST(self, origin, content, query, group_id, role_id, user_id):
  898. requester_user_id = parse_string_from_args(query, "requester_user_id")
  899. if get_domain_from_id(requester_user_id) != origin:
  900. raise SynapseError(403, "requester_user_id doesn't match origin")
  901. if role_id == "":
  902. raise SynapseError(400, "role_id cannot be empty string")
  903. resp = yield self.handler.update_group_summary_user(
  904. group_id, requester_user_id,
  905. user_id=user_id,
  906. role_id=role_id,
  907. content=content,
  908. )
  909. defer.returnValue((200, resp))
  910. @defer.inlineCallbacks
  911. def on_DELETE(self, origin, content, query, group_id, role_id, user_id):
  912. requester_user_id = parse_string_from_args(query, "requester_user_id")
  913. if get_domain_from_id(requester_user_id) != origin:
  914. raise SynapseError(403, "requester_user_id doesn't match origin")
  915. if role_id == "":
  916. raise SynapseError(400, "role_id cannot be empty string")
  917. resp = yield self.handler.delete_group_summary_user(
  918. group_id, requester_user_id,
  919. user_id=user_id,
  920. role_id=role_id,
  921. )
  922. defer.returnValue((200, resp))
  923. class FederationGroupsBulkPublicisedServlet(BaseFederationServlet):
  924. """Get roles in a group
  925. """
  926. PATH = (
  927. "/get_groups_publicised$"
  928. )
  929. @defer.inlineCallbacks
  930. def on_POST(self, origin, content, query):
  931. resp = yield self.handler.bulk_get_publicised_groups(
  932. content["user_ids"], proxy=False,
  933. )
  934. defer.returnValue((200, resp))
  935. class FederationGroupsSettingJoinPolicyServlet(BaseFederationServlet):
  936. """Sets whether a group is joinable without an invite or knock
  937. """
  938. PATH = "/groups/(?P<group_id>[^/]*)/settings/m.join_policy$"
  939. @defer.inlineCallbacks
  940. def on_PUT(self, origin, content, query, group_id):
  941. requester_user_id = parse_string_from_args(query, "requester_user_id")
  942. if get_domain_from_id(requester_user_id) != origin:
  943. raise SynapseError(403, "requester_user_id doesn't match origin")
  944. new_content = yield self.handler.set_group_join_policy(
  945. group_id, requester_user_id, content
  946. )
  947. defer.returnValue((200, new_content))
  948. FEDERATION_SERVLET_CLASSES = (
  949. FederationSendServlet,
  950. FederationPullServlet,
  951. FederationEventServlet,
  952. FederationStateServlet,
  953. FederationStateIdsServlet,
  954. FederationBackfillServlet,
  955. FederationQueryServlet,
  956. FederationMakeJoinServlet,
  957. FederationMakeLeaveServlet,
  958. FederationEventServlet,
  959. FederationSendJoinServlet,
  960. FederationSendLeaveServlet,
  961. FederationInviteServlet,
  962. FederationQueryAuthServlet,
  963. FederationGetMissingEventsServlet,
  964. FederationEventAuthServlet,
  965. FederationClientKeysQueryServlet,
  966. FederationUserDevicesQueryServlet,
  967. FederationClientKeysClaimServlet,
  968. FederationThirdPartyInviteExchangeServlet,
  969. On3pidBindServlet,
  970. OpenIdUserInfo,
  971. FederationVersionServlet,
  972. )
  973. ROOM_LIST_CLASSES = (
  974. PublicRoomList,
  975. )
  976. GROUP_SERVER_SERVLET_CLASSES = (
  977. FederationGroupsProfileServlet,
  978. FederationGroupsSummaryServlet,
  979. FederationGroupsRoomsServlet,
  980. FederationGroupsUsersServlet,
  981. FederationGroupsInvitedUsersServlet,
  982. FederationGroupsInviteServlet,
  983. FederationGroupsAcceptInviteServlet,
  984. FederationGroupsJoinServlet,
  985. FederationGroupsRemoveUserServlet,
  986. FederationGroupsSummaryRoomsServlet,
  987. FederationGroupsCategoriesServlet,
  988. FederationGroupsCategoryServlet,
  989. FederationGroupsRolesServlet,
  990. FederationGroupsRoleServlet,
  991. FederationGroupsSummaryUsersServlet,
  992. FederationGroupsAddRoomsServlet,
  993. FederationGroupsAddRoomsConfigServlet,
  994. FederationGroupsSettingJoinPolicyServlet,
  995. )
  996. GROUP_LOCAL_SERVLET_CLASSES = (
  997. FederationGroupsLocalInviteServlet,
  998. FederationGroupsRemoveLocalUserServlet,
  999. FederationGroupsBulkPublicisedServlet,
  1000. )
  1001. GROUP_ATTESTATION_SERVLET_CLASSES = (
  1002. FederationGroupsRenewAttestaionServlet,
  1003. )
  1004. def register_servlets(hs, resource, authenticator, ratelimiter):
  1005. for servletclass in FEDERATION_SERVLET_CLASSES:
  1006. servletclass(
  1007. handler=hs.get_federation_server(),
  1008. authenticator=authenticator,
  1009. ratelimiter=ratelimiter,
  1010. server_name=hs.hostname,
  1011. ).register(resource)
  1012. for servletclass in ROOM_LIST_CLASSES:
  1013. servletclass(
  1014. handler=hs.get_room_list_handler(),
  1015. authenticator=authenticator,
  1016. ratelimiter=ratelimiter,
  1017. server_name=hs.hostname,
  1018. ).register(resource)
  1019. for servletclass in GROUP_SERVER_SERVLET_CLASSES:
  1020. servletclass(
  1021. handler=hs.get_groups_server_handler(),
  1022. authenticator=authenticator,
  1023. ratelimiter=ratelimiter,
  1024. server_name=hs.hostname,
  1025. ).register(resource)
  1026. for servletclass in GROUP_LOCAL_SERVLET_CLASSES:
  1027. servletclass(
  1028. handler=hs.get_groups_local_handler(),
  1029. authenticator=authenticator,
  1030. ratelimiter=ratelimiter,
  1031. server_name=hs.hostname,
  1032. ).register(resource)
  1033. for servletclass in GROUP_ATTESTATION_SERVLET_CLASSES:
  1034. servletclass(
  1035. handler=hs.get_groups_attestation_renewer(),
  1036. authenticator=authenticator,
  1037. ratelimiter=ratelimiter,
  1038. server_name=hs.hostname,
  1039. ).register(resource)