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.
 
 
 
 
 
 

400 line
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, TCPListenerConfig
  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,
  73. config: HomeServerConfig,
  74. listener_config: ListenerConfig,
  75. ) -> Iterable[Port]:
  76. # Must exist since this is an HTTP listener.
  77. assert listener_config.http_options is not None
  78. site_tag = listener_config.get_site_tag()
  79. # We always include a health resource.
  80. resources: Dict[str, Resource] = {"/health": HealthResource()}
  81. for res in listener_config.http_options.resources:
  82. for name in res.names:
  83. if name == "openid" and "federation" in res.names:
  84. # Skip loading openid resource if federation is defined
  85. # since federation resource will include openid
  86. continue
  87. if name == "health":
  88. # Skip loading, health resource is always included
  89. continue
  90. resources.update(self._configure_named_resource(name, res.compress))
  91. additional_resources = listener_config.http_options.additional_resources
  92. logger.debug("Configuring additional resources: %r", additional_resources)
  93. module_api = self.get_module_api()
  94. for path, resmodule in additional_resources.items():
  95. handler_cls, config = load_module(
  96. resmodule,
  97. ("listeners", site_tag, "additional_resources", "<%s>" % (path,)),
  98. )
  99. handler = handler_cls(config, module_api)
  100. if isinstance(handler, Resource):
  101. resource = handler
  102. elif hasattr(handler, "handle_request"):
  103. resource = AdditionalResource(self, handler.handle_request)
  104. else:
  105. raise ConfigError(
  106. "additional_resource %s does not implement a known interface"
  107. % (resmodule["module"],)
  108. )
  109. resources[path] = resource
  110. # Attach additional resources registered by modules.
  111. resources.update(self._module_web_resources)
  112. self._module_web_resources_consumed = True
  113. # Try to find something useful to serve at '/':
  114. #
  115. # 1. Redirect to the web client if it is an HTTP(S) URL.
  116. # 2. Redirect to the static "Synapse is running" page.
  117. # 3. Do not redirect and use a blank resource.
  118. if self.config.server.web_client_location:
  119. root_resource: Resource = RootOptionsRedirectResource(
  120. self.config.server.web_client_location
  121. )
  122. elif STATIC_PREFIX in resources:
  123. root_resource = RootOptionsRedirectResource(STATIC_PREFIX)
  124. else:
  125. root_resource = OptionsResource()
  126. ports = listen_http(
  127. self,
  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. if isinstance(listener, TCPListenerConfig):
  222. _base.listen_manhole(
  223. listener.bind_addresses,
  224. listener.port,
  225. manhole_settings=self.config.server.manhole_settings,
  226. manhole_globals={"hs": self},
  227. )
  228. else:
  229. raise ConfigError(
  230. "Can not use a unix socket for manhole at this time."
  231. )
  232. elif listener.type == "metrics":
  233. if not self.config.metrics.enable_metrics:
  234. logger.warning(
  235. "Metrics listener configured, but "
  236. "enable_metrics is not True!"
  237. )
  238. else:
  239. if isinstance(listener, TCPListenerConfig):
  240. _base.listen_metrics(
  241. listener.bind_addresses,
  242. listener.port,
  243. )
  244. else:
  245. raise ConfigError(
  246. "Can not use a unix socket for metrics at this time."
  247. )
  248. else:
  249. # this shouldn't happen, as the listener type should have been checked
  250. # during parsing
  251. logger.warning("Unrecognized listener type: %s", listener.type)
  252. def setup(config_options: List[str]) -> SynapseHomeServer:
  253. """
  254. Args:
  255. config_options_options: The options passed to Synapse. Usually `sys.argv[1:]`.
  256. Returns:
  257. A homeserver instance.
  258. """
  259. try:
  260. config = HomeServerConfig.load_or_generate_config(
  261. "Synapse Homeserver", config_options
  262. )
  263. except ConfigError as e:
  264. sys.stderr.write("\n")
  265. for f in format_config_error(e):
  266. sys.stderr.write(f)
  267. sys.stderr.write("\n")
  268. sys.exit(1)
  269. if not config:
  270. # If a config isn't returned, and an exception isn't raised, we're just
  271. # generating config files and shouldn't try to continue.
  272. sys.exit(0)
  273. if config.worker.worker_app:
  274. raise ConfigError(
  275. "You have specified `worker_app` in the config but are attempting to start a non-worker "
  276. "instance. Please use `python -m synapse.app.generic_worker` instead (or remove the option if this is the main process)."
  277. )
  278. sys.exit(1)
  279. events.USE_FROZEN_DICTS = config.server.use_frozen_dicts
  280. synapse.util.caches.TRACK_MEMORY_USAGE = config.caches.track_memory_usage
  281. if config.server.gc_seconds:
  282. synapse.metrics.MIN_TIME_BETWEEN_GCS = config.server.gc_seconds
  283. if (
  284. config.registration.enable_registration
  285. and not config.registration.enable_registration_without_verification
  286. ):
  287. if (
  288. not config.captcha.enable_registration_captcha
  289. and not config.registration.registrations_require_3pid
  290. and not config.registration.registration_requires_token
  291. ):
  292. raise ConfigError(
  293. "You have enabled open registration without any verification. This is a known vector for "
  294. "spam and abuse. If you would like to allow public registration, please consider adding email, "
  295. "captcha, or token-based verification. Otherwise this check can be removed by setting the "
  296. "`enable_registration_without_verification` config option to `true`."
  297. )
  298. hs = SynapseHomeServer(
  299. config.server.server_name,
  300. config=config,
  301. version_string=f"Synapse/{VERSION}",
  302. )
  303. synapse.config.logger.setup_logging(hs, config, use_worker_options=False)
  304. logger.info("Setting up server")
  305. try:
  306. hs.setup()
  307. except Exception as e:
  308. handle_startup_exception(e)
  309. async def start() -> None:
  310. # Load the OIDC provider metadatas, if OIDC is enabled.
  311. if hs.config.oidc.oidc_enabled:
  312. oidc = hs.get_oidc_handler()
  313. # Loading the provider metadata also ensures the provider config is valid.
  314. await oidc.load_metadata()
  315. await _base.start(hs)
  316. hs.get_datastores().main.db_pool.updates.start_doing_background_updates()
  317. register_start(start)
  318. return hs
  319. def run(hs: HomeServer) -> None:
  320. _base.start_reactor(
  321. "synapse-homeserver",
  322. soft_file_limit=hs.config.server.soft_file_limit,
  323. gc_thresholds=hs.config.server.gc_thresholds,
  324. pid_file=hs.config.server.pid_file,
  325. daemonize=hs.config.server.daemonize,
  326. print_pidfile=hs.config.server.print_pidfile,
  327. logger=logger,
  328. )
  329. def main() -> None:
  330. with LoggingContext("main"):
  331. # check base requirements
  332. check_requirements()
  333. hs = setup(sys.argv[1:])
  334. # redirect stdio to the logs, if configured.
  335. if not hs.config.logging.no_redirect_stdio:
  336. redirect_stdio_to_logs()
  337. run(hs)
  338. if __name__ == "__main__":
  339. main()