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