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 16import logging 17import os 18import sys 19from typing import Dict, Iterable, Iterator, List 20 21from twisted.internet.tcp import Port 22from twisted.web.resource import EncodingResourceWrapper, Resource 23from twisted.web.server import GzipEncoderFactory 24from twisted.web.static import File 25 26import synapse 27import synapse.config.logger 28from synapse import events 29from synapse.api.urls import ( 30 FEDERATION_PREFIX, 31 LEGACY_MEDIA_PREFIX, 32 MEDIA_R0_PREFIX, 33 MEDIA_V3_PREFIX, 34 SERVER_KEY_V2_PREFIX, 35 STATIC_PREFIX, 36 WEB_CLIENT_PREFIX, 37) 38from synapse.app import _base 39from synapse.app._base import ( 40 handle_startup_exception, 41 listen_ssl, 42 listen_tcp, 43 max_request_body_size, 44 redirect_stdio_to_logs, 45 register_start, 46) 47from synapse.config._base import ConfigError 48from synapse.config.emailconfig import ThreepidBehaviour 49from synapse.config.homeserver import HomeServerConfig 50from synapse.config.server import ListenerConfig 51from synapse.federation.transport.server import TransportLayerServer 52from synapse.http.additional_resource import AdditionalResource 53from synapse.http.server import ( 54 OptionsResource, 55 RootOptionsRedirectResource, 56 RootRedirect, 57 StaticResource, 58) 59from synapse.http.site import SynapseSite 60from synapse.logging.context import LoggingContext 61from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy 62from synapse.python_dependencies import check_requirements 63from synapse.replication.http import REPLICATION_PREFIX, ReplicationRestResource 64from synapse.replication.tcp.resource import ReplicationStreamProtocolFactory 65from synapse.rest import ClientRestResource 66from synapse.rest.admin import AdminRestResource 67from synapse.rest.health import HealthResource 68from synapse.rest.key.v2 import KeyApiV2Resource 69from synapse.rest.synapse.client import build_synapse_client_resource_tree 70from synapse.rest.well_known import well_known_resource 71from synapse.server import HomeServer 72from synapse.storage import DataStore 73from synapse.util.httpresourcetree import create_resource_tree 74from synapse.util.module_loader import load_module 75from synapse.util.versionstring import get_version_string 76 77logger = logging.getLogger("synapse.app.homeserver") 78 79 80def gz_wrap(r: Resource) -> Resource: 81 return EncodingResourceWrapper(r, [GzipEncoderFactory()]) 82 83 84class SynapseHomeServer(HomeServer): 85 DATASTORE_CLASS = DataStore # type: ignore 86 87 def _listener_http( 88 self, config: HomeServerConfig, listener_config: ListenerConfig 89 ) -> Iterable[Port]: 90 port = listener_config.port 91 bind_addresses = listener_config.bind_addresses 92 tls = listener_config.tls 93 # Must exist since this is an HTTP listener. 94 assert listener_config.http_options is not None 95 site_tag = listener_config.http_options.tag 96 if site_tag is None: 97 site_tag = str(port) 98 99 # We always include a health resource. 100 resources: Dict[str, Resource] = {"/health": HealthResource()} 101 102 for res in listener_config.http_options.resources: 103 for name in res.names: 104 if name == "openid" and "federation" in res.names: 105 # Skip loading openid resource if federation is defined 106 # since federation resource will include openid 107 continue 108 resources.update(self._configure_named_resource(name, res.compress)) 109 110 additional_resources = listener_config.http_options.additional_resources 111 logger.debug("Configuring additional resources: %r", additional_resources) 112 module_api = self.get_module_api() 113 for path, resmodule in additional_resources.items(): 114 handler_cls, config = load_module( 115 resmodule, 116 ("listeners", site_tag, "additional_resources", "<%s>" % (path,)), 117 ) 118 handler = handler_cls(config, module_api) 119 if isinstance(handler, Resource): 120 resource = handler 121 elif hasattr(handler, "handle_request"): 122 resource = AdditionalResource(self, handler.handle_request) 123 else: 124 raise ConfigError( 125 "additional_resource %s does not implement a known interface" 126 % (resmodule["module"],) 127 ) 128 resources[path] = resource 129 130 # Attach additional resources registered by modules. 131 resources.update(self._module_web_resources) 132 self._module_web_resources_consumed = True 133 134 # try to find something useful to redirect '/' to 135 if WEB_CLIENT_PREFIX in resources: 136 root_resource: Resource = RootOptionsRedirectResource(WEB_CLIENT_PREFIX) 137 elif STATIC_PREFIX in resources: 138 root_resource = RootOptionsRedirectResource(STATIC_PREFIX) 139 else: 140 root_resource = OptionsResource() 141 142 site = SynapseSite( 143 "synapse.access.%s.%s" % ("https" if tls else "http", site_tag), 144 site_tag, 145 listener_config, 146 create_resource_tree(resources, root_resource), 147 self.version_string, 148 max_request_body_size=max_request_body_size(self.config), 149 reactor=self.get_reactor(), 150 ) 151 152 if tls: 153 # refresh_certificate should have been called before this. 154 assert self.tls_server_context_factory is not None 155 ports = listen_ssl( 156 bind_addresses, 157 port, 158 site, 159 self.tls_server_context_factory, 160 reactor=self.get_reactor(), 161 ) 162 logger.info("Synapse now listening on TCP port %d (TLS)", port) 163 164 else: 165 ports = listen_tcp( 166 bind_addresses, 167 port, 168 site, 169 reactor=self.get_reactor(), 170 ) 171 logger.info("Synapse now listening on TCP port %d", port) 172 173 return ports 174 175 def _configure_named_resource( 176 self, name: str, compress: bool = False 177 ) -> Dict[str, Resource]: 178 """Build a resource map for a named resource 179 180 Args: 181 name: named resource: one of "client", "federation", etc 182 compress: whether to enable gzip compression for this resource 183 184 Returns: 185 map from path to HTTP resource 186 """ 187 resources: Dict[str, Resource] = {} 188 if name == "client": 189 client_resource: Resource = ClientRestResource(self) 190 if compress: 191 client_resource = gz_wrap(client_resource) 192 193 resources.update( 194 { 195 "/_matrix/client/api/v1": client_resource, 196 "/_matrix/client/r0": client_resource, 197 "/_matrix/client/v1": client_resource, 198 "/_matrix/client/v3": client_resource, 199 "/_matrix/client/unstable": client_resource, 200 "/_matrix/client/v2_alpha": client_resource, 201 "/_matrix/client/versions": client_resource, 202 "/.well-known": well_known_resource(self), 203 "/_synapse/admin": AdminRestResource(self), 204 **build_synapse_client_resource_tree(self), 205 } 206 ) 207 208 if self.config.email.threepid_behaviour_email == ThreepidBehaviour.LOCAL: 209 from synapse.rest.synapse.client.password_reset import ( 210 PasswordResetSubmitTokenResource, 211 ) 212 213 resources[ 214 "/_synapse/client/password_reset/email/submit_token" 215 ] = PasswordResetSubmitTokenResource(self) 216 217 if name == "consent": 218 from synapse.rest.consent.consent_resource import ConsentResource 219 220 consent_resource: Resource = ConsentResource(self) 221 if compress: 222 consent_resource = gz_wrap(consent_resource) 223 resources.update({"/_matrix/consent": consent_resource}) 224 225 if name == "federation": 226 resources.update({FEDERATION_PREFIX: TransportLayerServer(self)}) 227 228 if name == "openid": 229 resources.update( 230 { 231 FEDERATION_PREFIX: TransportLayerServer( 232 self, servlet_groups=["openid"] 233 ) 234 } 235 ) 236 237 if name in ["static", "client"]: 238 resources.update( 239 { 240 STATIC_PREFIX: StaticResource( 241 os.path.join(os.path.dirname(synapse.__file__), "static") 242 ) 243 } 244 ) 245 246 if name in ["media", "federation", "client"]: 247 if self.config.server.enable_media_repo: 248 media_repo = self.get_media_repository_resource() 249 resources.update( 250 { 251 MEDIA_R0_PREFIX: media_repo, 252 MEDIA_V3_PREFIX: media_repo, 253 LEGACY_MEDIA_PREFIX: media_repo, 254 } 255 ) 256 elif name == "media": 257 raise ConfigError( 258 "'media' resource conflicts with enable_media_repo=False" 259 ) 260 261 if name in ["keys", "federation"]: 262 resources[SERVER_KEY_V2_PREFIX] = KeyApiV2Resource(self) 263 264 if name == "webclient": 265 webclient_loc = self.config.server.web_client_location 266 267 if webclient_loc is None: 268 logger.warning( 269 "Not enabling webclient resource, as web_client_location is unset." 270 ) 271 elif webclient_loc.startswith("http://") or webclient_loc.startswith( 272 "https://" 273 ): 274 resources[WEB_CLIENT_PREFIX] = RootRedirect(webclient_loc) 275 else: 276 logger.warning( 277 "Running webclient on the same domain is not recommended: " 278 "https://github.com/matrix-org/synapse#security-note - " 279 "after you move webclient to different host you can set " 280 "web_client_location to its full URL to enable redirection." 281 ) 282 # GZip is disabled here due to 283 # https://twistedmatrix.com/trac/ticket/7678 284 resources[WEB_CLIENT_PREFIX] = File(webclient_loc) 285 286 if name == "metrics" and self.config.metrics.enable_metrics: 287 resources[METRICS_PREFIX] = MetricsResource(RegistryProxy) 288 289 if name == "replication": 290 resources[REPLICATION_PREFIX] = ReplicationRestResource(self) 291 292 return resources 293 294 def start_listening(self) -> None: 295 if self.config.redis.redis_enabled: 296 # If redis is enabled we connect via the replication command handler 297 # in the same way as the workers (since we're effectively a client 298 # rather than a server). 299 self.get_tcp_replication().start_replication(self) 300 301 for listener in self.config.server.listeners: 302 if listener.type == "http": 303 self._listening_services.extend( 304 self._listener_http(self.config, listener) 305 ) 306 elif listener.type == "manhole": 307 _base.listen_manhole( 308 listener.bind_addresses, 309 listener.port, 310 manhole_settings=self.config.server.manhole_settings, 311 manhole_globals={"hs": self}, 312 ) 313 elif listener.type == "replication": 314 services = listen_tcp( 315 listener.bind_addresses, 316 listener.port, 317 ReplicationStreamProtocolFactory(self), 318 ) 319 for s in services: 320 self.get_reactor().addSystemEventTrigger( 321 "before", "shutdown", s.stopListening 322 ) 323 elif listener.type == "metrics": 324 if not self.config.metrics.enable_metrics: 325 logger.warning( 326 "Metrics listener configured, but " 327 "enable_metrics is not True!" 328 ) 329 else: 330 _base.listen_metrics(listener.bind_addresses, listener.port) 331 else: 332 # this shouldn't happen, as the listener type should have been checked 333 # during parsing 334 logger.warning("Unrecognized listener type: %s", listener.type) 335 336 337def setup(config_options: List[str]) -> SynapseHomeServer: 338 """ 339 Args: 340 config_options_options: The options passed to Synapse. Usually `sys.argv[1:]`. 341 342 Returns: 343 A homeserver instance. 344 """ 345 try: 346 config = HomeServerConfig.load_or_generate_config( 347 "Synapse Homeserver", config_options 348 ) 349 except ConfigError as e: 350 sys.stderr.write("\n") 351 for f in format_config_error(e): 352 sys.stderr.write(f) 353 sys.stderr.write("\n") 354 sys.exit(1) 355 356 if not config: 357 # If a config isn't returned, and an exception isn't raised, we're just 358 # generating config files and shouldn't try to continue. 359 sys.exit(0) 360 361 if config.worker.worker_app: 362 raise ConfigError( 363 "You have specified `worker_app` in the config but are attempting to start a non-worker " 364 "instance. Please use `python -m synapse.app.generic_worker` instead (or remove the option if this is the main process)." 365 ) 366 sys.exit(1) 367 368 events.USE_FROZEN_DICTS = config.server.use_frozen_dicts 369 synapse.util.caches.TRACK_MEMORY_USAGE = config.caches.track_memory_usage 370 371 if config.server.gc_seconds: 372 synapse.metrics.MIN_TIME_BETWEEN_GCS = config.server.gc_seconds 373 374 hs = SynapseHomeServer( 375 config.server.server_name, 376 config=config, 377 version_string="Synapse/" + get_version_string(synapse), 378 ) 379 380 synapse.config.logger.setup_logging(hs, config, use_worker_options=False) 381 382 logger.info("Setting up server") 383 384 try: 385 hs.setup() 386 except Exception as e: 387 handle_startup_exception(e) 388 389 async def start() -> None: 390 # Load the OIDC provider metadatas, if OIDC is enabled. 391 if hs.config.oidc.oidc_enabled: 392 oidc = hs.get_oidc_handler() 393 # Loading the provider metadata also ensures the provider config is valid. 394 await oidc.load_metadata() 395 396 await _base.start(hs) 397 398 hs.get_datastore().db_pool.updates.start_doing_background_updates() 399 400 register_start(start) 401 402 return hs 403 404 405def format_config_error(e: ConfigError) -> Iterator[str]: 406 """ 407 Formats a config error neatly 408 409 The idea is to format the immediate error, plus the "causes" of those errors, 410 hopefully in a way that makes sense to the user. For example: 411 412 Error in configuration at 'oidc_config.user_mapping_provider.config.display_name_template': 413 Failed to parse config for module 'JinjaOidcMappingProvider': 414 invalid jinja template: 415 unexpected end of template, expected 'end of print statement'. 416 417 Args: 418 e: the error to be formatted 419 420 Returns: An iterator which yields string fragments to be formatted 421 """ 422 yield "Error in configuration" 423 424 if e.path: 425 yield " at '%s'" % (".".join(e.path),) 426 427 yield ":\n %s" % (e.msg,) 428 429 parent_e = e.__cause__ 430 indent = 1 431 while parent_e: 432 indent += 1 433 yield ":\n%s%s" % (" " * indent, str(parent_e)) 434 parent_e = parent_e.__cause__ 435 436 437def run(hs: HomeServer) -> None: 438 _base.start_reactor( 439 "synapse-homeserver", 440 soft_file_limit=hs.config.server.soft_file_limit, 441 gc_thresholds=hs.config.server.gc_thresholds, 442 pid_file=hs.config.server.pid_file, 443 daemonize=hs.config.server.daemonize, 444 print_pidfile=hs.config.server.print_pidfile, 445 logger=logger, 446 ) 447 448 449def main() -> None: 450 with LoggingContext("main"): 451 # check base requirements 452 check_requirements() 453 hs = setup(sys.argv[1:]) 454 455 # redirect stdio to the logs, if configured. 456 if not hs.config.logging.no_redirect_stdio: 457 redirect_stdio_to_logs() 458 459 run(hs) 460 461 462if __name__ == "__main__": 463 main() 464