You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

389 lines
14 KiB

  1. # Copyright 2014-2016 OpenMarket Ltd
  2. # Copyright 2019 New Vector Ltd
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import logging
  16. import os
  17. import sys
  18. from typing import Dict, Iterable, List
  19. from twisted.internet.tcp import Port
  20. from twisted.web.resource import EncodingResourceWrapper, Resource
  21. from twisted.web.server import GzipEncoderFactory
  22. import synapse
  23. import synapse.config.logger
  24. from synapse import events
  25. from synapse.api.urls import (
  26. CLIENT_API_PREFIX,
  27. FEDERATION_PREFIX,
  28. LEGACY_MEDIA_PREFIX,
  29. MEDIA_R0_PREFIX,
  30. MEDIA_V3_PREFIX,
  31. SERVER_KEY_PREFIX,
  32. STATIC_PREFIX,
  33. )
  34. from synapse.app import _base
  35. from synapse.app._base import (
  36. handle_startup_exception,
  37. listen_http,
  38. max_request_body_size,
  39. redirect_stdio_to_logs,
  40. register_start,
  41. )
  42. from synapse.config._base import ConfigError, format_config_error
  43. from synapse.config.homeserver import HomeServerConfig
  44. from synapse.config.server import ListenerConfig
  45. from synapse.federation.transport.server import TransportLayerServer
  46. from synapse.http.additional_resource import AdditionalResource
  47. from synapse.http.server import (
  48. OptionsResource,
  49. RootOptionsRedirectResource,
  50. StaticResource,
  51. )
  52. from synapse.logging.context import LoggingContext
  53. from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy
  54. from synapse.replication.http import REPLICATION_PREFIX, ReplicationRestResource
  55. from synapse.rest import ClientRestResource
  56. from synapse.rest.admin import AdminRestResource
  57. from synapse.rest.health import HealthResource
  58. from synapse.rest.key.v2 import KeyResource
  59. from synapse.rest.synapse.client import build_synapse_client_resource_tree
  60. from synapse.rest.well_known import well_known_resource
  61. from synapse.server import HomeServer
  62. from synapse.storage import DataStore
  63. from synapse.util.check_dependencies import VERSION, check_requirements
  64. from synapse.util.httpresourcetree import create_resource_tree
  65. from synapse.util.module_loader import load_module
  66. logger = logging.getLogger("synapse.app.homeserver")
  67. def gz_wrap(r: Resource) -> Resource:
  68. return EncodingResourceWrapper(r, [GzipEncoderFactory()])
  69. class SynapseHomeServer(HomeServer):
  70. DATASTORE_CLASS = DataStore # type: ignore
  71. def _listener_http(
  72. self, config: HomeServerConfig, listener_config: ListenerConfig
  73. ) -> Iterable[Port]:
  74. port = listener_config.port
  75. # Must exist since this is an HTTP listener.
  76. assert listener_config.http_options is not None
  77. site_tag = listener_config.http_options.tag
  78. if site_tag is None:
  79. site_tag = str(port)
  80. # We always include a health resource.
  81. resources: Dict[str, Resource] = {"/health": HealthResource()}
  82. for res in listener_config.http_options.resources:
  83. for name in res.names:
  84. if name == "openid" and "federation" in res.names:
  85. # Skip loading openid resource if federation is defined
  86. # since federation resource will include openid
  87. continue
  88. if name == "health":
  89. # Skip loading, health resource is always included
  90. continue
  91. resources.update(self._configure_named_resource(name, res.compress))
  92. additional_resources = listener_config.http_options.additional_resources
  93. logger.debug("Configuring additional resources: %r", additional_resources)
  94. module_api = self.get_module_api()
  95. for path, resmodule in additional_resources.items():
  96. handler_cls, config = load_module(
  97. resmodule,
  98. ("listeners", site_tag, "additional_resources", "<%s>" % (path,)),
  99. )
  100. handler = handler_cls(config, module_api)
  101. if isinstance(handler, Resource):
  102. resource = handler
  103. elif hasattr(handler, "handle_request"):
  104. resource = AdditionalResource(self, handler.handle_request)
  105. else:
  106. raise ConfigError(
  107. "additional_resource %s does not implement a known interface"
  108. % (resmodule["module"],)
  109. )
  110. resources[path] = resource
  111. # Attach additional resources registered by modules.
  112. resources.update(self._module_web_resources)
  113. self._module_web_resources_consumed = True
  114. # Try to find something useful to serve at '/':
  115. #
  116. # 1. Redirect to the web client if it is an HTTP(S) URL.
  117. # 2. Redirect to the static "Synapse is running" page.
  118. # 3. Do not redirect and use a blank resource.
  119. if self.config.server.web_client_location:
  120. root_resource: Resource = RootOptionsRedirectResource(
  121. self.config.server.web_client_location
  122. )
  123. elif STATIC_PREFIX in resources:
  124. root_resource = RootOptionsRedirectResource(STATIC_PREFIX)
  125. else:
  126. root_resource = OptionsResource()
  127. ports = listen_http(
  128. listener_config,
  129. create_resource_tree(resources, root_resource),
  130. self.version_string,
  131. max_request_body_size(self.config),
  132. self.tls_server_context_factory,
  133. reactor=self.get_reactor(),
  134. )
  135. return ports
  136. def _configure_named_resource(
  137. self, name: str, compress: bool = False
  138. ) -> Dict[str, Resource]:
  139. """Build a resource map for a named resource
  140. Args:
  141. name: named resource: one of "client", "federation", etc
  142. compress: whether to enable gzip compression for this resource
  143. Returns:
  144. map from path to HTTP resource
  145. """
  146. resources: Dict[str, Resource] = {}
  147. if name == "client":
  148. client_resource: Resource = ClientRestResource(self)
  149. if compress:
  150. client_resource = gz_wrap(client_resource)
  151. resources.update(
  152. {
  153. CLIENT_API_PREFIX: client_resource,
  154. "/.well-known": well_known_resource(self),
  155. "/_synapse/admin": AdminRestResource(self),
  156. **build_synapse_client_resource_tree(self),
  157. }
  158. )
  159. if self.config.email.can_verify_email:
  160. from synapse.rest.synapse.client.password_reset import (
  161. PasswordResetSubmitTokenResource,
  162. )
  163. resources[
  164. "/_synapse/client/password_reset/email/submit_token"
  165. ] = PasswordResetSubmitTokenResource(self)
  166. if name == "consent":
  167. from synapse.rest.consent.consent_resource import ConsentResource
  168. consent_resource: Resource = ConsentResource(self)
  169. if compress:
  170. consent_resource = gz_wrap(consent_resource)
  171. resources["/_matrix/consent"] = consent_resource
  172. if name == "federation":
  173. federation_resource: Resource = TransportLayerServer(self)
  174. if compress:
  175. federation_resource = gz_wrap(federation_resource)
  176. resources[FEDERATION_PREFIX] = federation_resource
  177. if name == "openid":
  178. resources[FEDERATION_PREFIX] = TransportLayerServer(
  179. self, servlet_groups=["openid"]
  180. )
  181. if name in ["static", "client"]:
  182. resources[STATIC_PREFIX] = StaticResource(
  183. os.path.join(os.path.dirname(synapse.__file__), "static")
  184. )
  185. if name in ["media", "federation", "client"]:
  186. if self.config.server.enable_media_repo:
  187. media_repo = self.get_media_repository_resource()
  188. resources.update(
  189. {
  190. MEDIA_R0_PREFIX: media_repo,
  191. MEDIA_V3_PREFIX: media_repo,
  192. LEGACY_MEDIA_PREFIX: media_repo,
  193. }
  194. )
  195. elif name == "media":
  196. raise ConfigError(
  197. "'media' resource conflicts with enable_media_repo=False"
  198. )
  199. if name in ["keys", "federation"]:
  200. resources[SERVER_KEY_PREFIX] = KeyResource(self)
  201. if name == "metrics" and self.config.metrics.enable_metrics:
  202. metrics_resource: Resource = MetricsResource(RegistryProxy)
  203. if compress:
  204. metrics_resource = gz_wrap(metrics_resource)
  205. resources[METRICS_PREFIX] = metrics_resource
  206. if name == "replication":
  207. resources[REPLICATION_PREFIX] = ReplicationRestResource(self)
  208. return resources
  209. def start_listening(self) -> None:
  210. if self.config.redis.redis_enabled:
  211. # If redis is enabled we connect via the replication command handler
  212. # in the same way as the workers (since we're effectively a client
  213. # rather than a server).
  214. self.get_replication_command_handler().start_replication(self)
  215. for listener in self.config.server.listeners:
  216. if listener.type == "http":
  217. self._listening_services.extend(
  218. self._listener_http(self.config, listener)
  219. )
  220. elif listener.type == "manhole":
  221. _base.listen_manhole(
  222. listener.bind_addresses,
  223. listener.port,
  224. manhole_settings=self.config.server.manhole_settings,
  225. manhole_globals={"hs": self},
  226. )
  227. elif listener.type == "metrics":
  228. if not self.config.metrics.enable_metrics:
  229. logger.warning(
  230. "Metrics listener configured, but "
  231. "enable_metrics is not True!"
  232. )
  233. else:
  234. _base.listen_metrics(
  235. listener.bind_addresses,
  236. listener.port,
  237. )
  238. else:
  239. # this shouldn't happen, as the listener type should have been checked
  240. # during parsing
  241. logger.warning("Unrecognized listener type: %s", listener.type)
  242. def setup(config_options: List[str]) -> SynapseHomeServer:
  243. """
  244. Args:
  245. config_options_options: The options passed to Synapse. Usually `sys.argv[1:]`.
  246. Returns:
  247. A homeserver instance.
  248. """
  249. try:
  250. config = HomeServerConfig.load_or_generate_config(
  251. "Synapse Homeserver", config_options
  252. )
  253. except ConfigError as e:
  254. sys.stderr.write("\n")
  255. for f in format_config_error(e):
  256. sys.stderr.write(f)
  257. sys.stderr.write("\n")
  258. sys.exit(1)
  259. if not config:
  260. # If a config isn't returned, and an exception isn't raised, we're just
  261. # generating config files and shouldn't try to continue.
  262. sys.exit(0)
  263. if config.worker.worker_app:
  264. raise ConfigError(
  265. "You have specified `worker_app` in the config but are attempting to start a non-worker "
  266. "instance. Please use `python -m synapse.app.generic_worker` instead (or remove the option if this is the main process)."
  267. )
  268. sys.exit(1)
  269. events.USE_FROZEN_DICTS = config.server.use_frozen_dicts
  270. synapse.util.caches.TRACK_MEMORY_USAGE = config.caches.track_memory_usage
  271. if config.server.gc_seconds:
  272. synapse.metrics.MIN_TIME_BETWEEN_GCS = config.server.gc_seconds
  273. if (
  274. config.registration.enable_registration
  275. and not config.registration.enable_registration_without_verification
  276. ):
  277. if (
  278. not config.captcha.enable_registration_captcha
  279. and not config.registration.registrations_require_3pid
  280. and not config.registration.registration_requires_token
  281. ):
  282. raise ConfigError(
  283. "You have enabled open registration without any verification. This is a known vector for "
  284. "spam and abuse. If you would like to allow public registration, please consider adding email, "
  285. "captcha, or token-based verification. Otherwise this check can be removed by setting the "
  286. "`enable_registration_without_verification` config option to `true`."
  287. )
  288. hs = SynapseHomeServer(
  289. config.server.server_name,
  290. config=config,
  291. version_string=f"Synapse/{VERSION}",
  292. )
  293. synapse.config.logger.setup_logging(hs, config, use_worker_options=False)
  294. logger.info("Setting up server")
  295. try:
  296. hs.setup()
  297. except Exception as e:
  298. handle_startup_exception(e)
  299. async def start() -> None:
  300. # Load the OIDC provider metadatas, if OIDC is enabled.
  301. if hs.config.oidc.oidc_enabled:
  302. oidc = hs.get_oidc_handler()
  303. # Loading the provider metadata also ensures the provider config is valid.
  304. await oidc.load_metadata()
  305. await _base.start(hs)
  306. hs.get_datastores().main.db_pool.updates.start_doing_background_updates()
  307. register_start(start)
  308. return hs
  309. def run(hs: HomeServer) -> None:
  310. _base.start_reactor(
  311. "synapse-homeserver",
  312. soft_file_limit=hs.config.server.soft_file_limit,
  313. gc_thresholds=hs.config.server.gc_thresholds,
  314. pid_file=hs.config.server.pid_file,
  315. daemonize=hs.config.server.daemonize,
  316. print_pidfile=hs.config.server.print_pidfile,
  317. logger=logger,
  318. )
  319. def main() -> None:
  320. with LoggingContext("main"):
  321. # check base requirements
  322. check_requirements()
  323. hs = setup(sys.argv[1:])
  324. # redirect stdio to the logs, if configured.
  325. if not hs.config.logging.no_redirect_stdio:
  326. redirect_stdio_to_logs()
  327. run(hs)
  328. if __name__ == "__main__":
  329. main()