Example #1
0
    def __init__(self, cfg, reactor=twisted.internet.reactor):
        self.reactor = reactor
        self.config_file = get_config_file_path()

        self.cfg = cfg

        logger.info("Starting Sydent server")

        self.pidfile = self.cfg.get('general', "pidfile.path")

        self.db = SqliteDatabase(self).db

        self.server_name = self.cfg.get('general', 'server.name')
        if self.server_name == '':
            self.server_name = os.uname()[1]
            logger.warn((
                "You had not specified a server name. I have guessed that this server is called '%s' "
                +
                "and saved this in the config file. If this is incorrect, you should edit server.name in "
                + "the config file.") % (self.server_name, ))
            self.cfg.set('general', 'server.name', self.server_name)
            self.save_config()

        if self.cfg.has_option("general", "sentry_dsn"):
            # Only import and start sentry SDK if configured.
            import sentry_sdk
            sentry_sdk.init(dsn=self.cfg.get("general", "sentry_dsn"), )
            with sentry_sdk.configure_scope() as scope:
                scope.set_tag("sydent_server_name", self.server_name)

        if self.cfg.has_option("general", "prometheus_port"):
            import prometheus_client
            prometheus_client.start_http_server(
                port=self.cfg.getint("general", "prometheus_port"),
                addr=self.cfg.get("general", "prometheus_addr"),
            )

        self.enable_v1_associations = parse_cfg_bool(
            self.cfg.get("general", "enable_v1_associations"))

        self.delete_tokens_on_bind = parse_cfg_bool(
            self.cfg.get("general", "delete_tokens_on_bind"))

        # See if a pepper already exists in the database
        # Note: This MUST be run before we start serving requests, otherwise lookups for
        # 3PID hashes may come in before we've completed generating them
        hashing_metadata_store = HashingMetadataStore(self)
        lookup_pepper = hashing_metadata_store.get_lookup_pepper()
        if not lookup_pepper:
            # No pepper defined in the database, generate one
            lookup_pepper = generateAlphanumericTokenOfLength(5)

            # Store it in the database and rehash 3PIDs
            hashing_metadata_store.store_lookup_pepper(
                sha256_and_url_safe_base64, lookup_pepper)

        self.validators = Validators()
        self.validators.email = EmailValidator(self)
        self.validators.msisdn = MsisdnValidator(self)

        self.keyring = Keyring()
        self.keyring.ed25519 = SydentEd25519(self).signing_key
        self.keyring.ed25519.alg = 'ed25519'

        self.sig_verifier = Verifier(self)

        self.servlets = Servlets()
        self.servlets.v1 = V1Servlet(self)
        self.servlets.v2 = V2Servlet(self)
        self.servlets.emailRequestCode = EmailRequestCodeServlet(self)
        self.servlets.emailValidate = EmailValidateCodeServlet(self)
        self.servlets.msisdnRequestCode = MsisdnRequestCodeServlet(self)
        self.servlets.msisdnValidate = MsisdnValidateCodeServlet(self)
        self.servlets.lookup = LookupServlet(self)
        self.servlets.bulk_lookup = BulkLookupServlet(self)
        self.servlets.hash_details = HashDetailsServlet(self, lookup_pepper)
        self.servlets.lookup_v2 = LookupV2Servlet(self, lookup_pepper)
        self.servlets.pubkey_ed25519 = Ed25519Servlet(self)
        self.servlets.pubkeyIsValid = PubkeyIsValidServlet(self)
        self.servlets.ephemeralPubkeyIsValid = EphemeralPubkeyIsValidServlet(
            self)
        self.servlets.threepidBind = ThreePidBindServlet(self)
        self.servlets.threepidUnbind = ThreePidUnbindServlet(self)
        self.servlets.replicationPush = ReplicationPushServlet(self)
        self.servlets.getValidated3pid = GetValidated3pidServlet(self)
        self.servlets.storeInviteServlet = StoreInviteServlet(self)
        self.servlets.blindlySignStuffServlet = BlindlySignStuffServlet(self)
        self.servlets.termsServlet = TermsServlet(self)
        self.servlets.accountServlet = AccountServlet(self)
        self.servlets.registerServlet = RegisterServlet(self)
        self.servlets.logoutServlet = LogoutServlet(self)

        self.threepidBinder = ThreepidBinder(self)

        self.sslComponents = SslComponents(self)

        self.clientApiHttpServer = ClientApiHttpServer(self)
        self.replicationHttpsServer = ReplicationHttpsServer(self)
        self.replicationHttpsClient = ReplicationHttpsClient(self)

        self.pusher = Pusher(self)

        # A dedicated validation session store just to clean up old sessions every N minutes
        self.cleanupValSession = ThreePidValSessionStore(self)
        cb = task.LoopingCall(self.cleanupValSession.deleteOldSessions)
        cb.clock = self.reactor
        cb.start(10 * 60.0)
Example #2
0
class Sydent:
    def __init__(self, cfg, reactor=twisted.internet.reactor):
        self.reactor = reactor
        self.config_file = get_config_file_path()

        self.cfg = cfg

        logger.info("Starting Sydent server")

        self.pidfile = self.cfg.get('general', "pidfile.path")

        self.db = SqliteDatabase(self).db

        self.server_name = self.cfg.get('general', 'server.name')
        if self.server_name == '':
            self.server_name = os.uname()[1]
            logger.warn((
                "You had not specified a server name. I have guessed that this server is called '%s' "
                +
                "and saved this in the config file. If this is incorrect, you should edit server.name in "
                + "the config file.") % (self.server_name, ))
            self.cfg.set('general', 'server.name', self.server_name)
            self.save_config()

        if self.cfg.has_option("general", "sentry_dsn"):
            # Only import and start sentry SDK if configured.
            import sentry_sdk
            sentry_sdk.init(dsn=self.cfg.get("general", "sentry_dsn"), )
            with sentry_sdk.configure_scope() as scope:
                scope.set_tag("sydent_server_name", self.server_name)

        if self.cfg.has_option("general", "prometheus_port"):
            import prometheus_client
            prometheus_client.start_http_server(
                port=self.cfg.getint("general", "prometheus_port"),
                addr=self.cfg.get("general", "prometheus_addr"),
            )

        self.enable_v1_associations = parse_cfg_bool(
            self.cfg.get("general", "enable_v1_associations"))

        self.delete_tokens_on_bind = parse_cfg_bool(
            self.cfg.get("general", "delete_tokens_on_bind"))

        self.username_obfuscate_characters = int(
            self.cfg.get(
                "email",
                "email.third_party_invite_username_obfuscate_characters"))
        self.domain_obfuscate_characters = int(
            self.cfg.get(
                "email",
                "email.third_party_invite_domain_obfuscate_characters"))

        # See if a pepper already exists in the database
        # Note: This MUST be run before we start serving requests, otherwise lookups for
        # 3PID hashes may come in before we've completed generating them
        hashing_metadata_store = HashingMetadataStore(self)
        lookup_pepper = hashing_metadata_store.get_lookup_pepper()
        if not lookup_pepper:
            # No pepper defined in the database, generate one
            lookup_pepper = generateAlphanumericTokenOfLength(5)

            # Store it in the database and rehash 3PIDs
            hashing_metadata_store.store_lookup_pepper(
                sha256_and_url_safe_base64, lookup_pepper)

        self.validators = Validators()
        self.validators.email = EmailValidator(self)
        self.validators.msisdn = MsisdnValidator(self)

        self.keyring = Keyring()
        self.keyring.ed25519 = SydentEd25519(self).signing_key
        self.keyring.ed25519.alg = 'ed25519'

        self.sig_verifier = Verifier(self)

        self.servlets = Servlets()
        self.servlets.v1 = V1Servlet(self)
        self.servlets.v2 = V2Servlet(self)
        self.servlets.emailRequestCode = EmailRequestCodeServlet(self)
        self.servlets.emailValidate = EmailValidateCodeServlet(self)
        self.servlets.msisdnRequestCode = MsisdnRequestCodeServlet(self)
        self.servlets.msisdnValidate = MsisdnValidateCodeServlet(self)
        self.servlets.lookup = LookupServlet(self)
        self.servlets.bulk_lookup = BulkLookupServlet(self)
        self.servlets.hash_details = HashDetailsServlet(self, lookup_pepper)
        self.servlets.lookup_v2 = LookupV2Servlet(self, lookup_pepper)
        self.servlets.pubkey_ed25519 = Ed25519Servlet(self)
        self.servlets.pubkeyIsValid = PubkeyIsValidServlet(self)
        self.servlets.ephemeralPubkeyIsValid = EphemeralPubkeyIsValidServlet(
            self)
        self.servlets.threepidBind = ThreePidBindServlet(self)
        self.servlets.threepidUnbind = ThreePidUnbindServlet(self)
        self.servlets.replicationPush = ReplicationPushServlet(self)
        self.servlets.getValidated3pid = GetValidated3pidServlet(self)
        self.servlets.storeInviteServlet = StoreInviteServlet(self)
        self.servlets.blindlySignStuffServlet = BlindlySignStuffServlet(self)
        self.servlets.termsServlet = TermsServlet(self)
        self.servlets.accountServlet = AccountServlet(self)
        self.servlets.registerServlet = RegisterServlet(self)
        self.servlets.logoutServlet = LogoutServlet(self)

        self.threepidBinder = ThreepidBinder(self)

        self.sslComponents = SslComponents(self)

        self.clientApiHttpServer = ClientApiHttpServer(self)
        self.replicationHttpsServer = ReplicationHttpsServer(self)
        self.replicationHttpsClient = ReplicationHttpsClient(self)

        self.pusher = Pusher(self)

        # A dedicated validation session store just to clean up old sessions every N minutes
        self.cleanupValSession = ThreePidValSessionStore(self)
        cb = task.LoopingCall(self.cleanupValSession.deleteOldSessions)
        cb.clock = self.reactor
        cb.start(10 * 60.0)

        # workaround for https://github.com/getsentry/sentry-python/issues/803: we
        # disable automatic GC and run it periodically instead.
        gc.disable()
        cb = task.LoopingCall(run_gc)
        cb.clock = self.reactor
        cb.start(1.0)

    def save_config(self):
        fp = open(self.config_file, 'w')
        self.cfg.write(fp)
        fp.close()

    def run(self):
        self.clientApiHttpServer.setup()
        self.replicationHttpsServer.setup()
        self.pusher.setup()

        internalport = self.cfg.get('http', 'internalapi.http.port')
        if internalport:
            try:
                interface = self.cfg.get('http',
                                         'internalapi.http.bind_address')
            except configparser.NoOptionError:
                interface = '::1'
            self.internalApiHttpServer = InternalApiHttpServer(self)
            self.internalApiHttpServer.setup(interface, int(internalport))

        if self.pidfile:
            with open(self.pidfile, 'w') as pidfile:
                pidfile.write(str(os.getpid()) + "\n")

        self.reactor.run()

    def ip_from_request(self, request):
        if (self.cfg.get('http', 'obey_x_forwarded_for')
                and request.requestHeaders.hasHeader("X-Forwarded-For")):
            return request.requestHeaders.getRawHeaders("X-Forwarded-For")[0]
        return request.getClientIP()
Example #3
0
    def __init__(
        self,
        sydent_config: SydentConfig,
        reactor: SydentReactor = twisted.internet.reactor,  # type: ignore[assignment]
        use_tls_for_federation: bool = True,
    ):
        self.config = sydent_config

        self.reactor = reactor
        self.use_tls_for_federation = use_tls_for_federation

        logger.info("Starting Sydent server")

        self.db: sqlite3.Connection = SqliteDatabase(self).db

        if self.config.general.sentry_enabled:
            import sentry_sdk

            sentry_sdk.init(
                dsn=self.config.general.sentry_dsn, release=get_version_string()
            )
            with sentry_sdk.configure_scope() as scope:
                scope.set_tag("sydent_server_name", self.config.general.server_name)

            # workaround for https://github.com/getsentry/sentry-python/issues/803: we
            # disable automatic GC and run it periodically instead.
            gc.disable()
            cb = task.LoopingCall(run_gc)
            cb.clock = self.reactor
            cb.start(1.0)

        # See if a pepper already exists in the database
        # Note: This MUST be run before we start serving requests, otherwise lookups for
        # 3PID hashes may come in before we've completed generating them
        hashing_metadata_store = HashingMetadataStore(self)
        lookup_pepper = hashing_metadata_store.get_lookup_pepper()
        if not lookup_pepper:
            # No pepper defined in the database, generate one
            lookup_pepper = generateAlphanumericTokenOfLength(5)

            # Store it in the database and rehash 3PIDs
            hashing_metadata_store.store_lookup_pepper(
                sha256_and_url_safe_base64, lookup_pepper
            )

        self.validators: Validators = Validators(
            EmailValidator(self), MsisdnValidator(self)
        )

        self.keyring: Keyring = Keyring(self.config.crypto.signing_key)
        self.keyring.ed25519.alg = "ed25519"

        self.sig_verifier: Verifier = Verifier(self)

        self.servlets: Servlets = Servlets(self, lookup_pepper)

        self.threepidBinder: ThreepidBinder = ThreepidBinder(self)

        self.sslComponents: SslComponents = SslComponents(self)

        self.clientApiHttpServer = ClientApiHttpServer(self)
        self.replicationHttpsServer = ReplicationHttpsServer(self)
        self.replicationHttpsClient: ReplicationHttpsClient = ReplicationHttpsClient(
            self
        )

        self.pusher: Pusher = Pusher(self)
Example #4
0
class Sydent:
    def __init__(self, cfg, reactor=twisted.internet.reactor):
        self.reactor = reactor
        self.config_file = get_config_file_path()

        self.cfg = cfg

        logger.info("Starting Sydent server")

        self.pidfile = self.cfg.get('general', "pidfile.path");

        self.db = SqliteDatabase(self).db

        self.server_name = self.cfg.get('general', 'server.name')
        if self.server_name == '':
            self.server_name = os.uname()[1]
            logger.warn(("You had not specified a server name. I have guessed that this server is called '%s' "
                        + "and saved this in the config file. If this is incorrect, you should edit server.name in "
                        + "the config file.") % (self.server_name,))
            self.cfg.set('general', 'server.name', self.server_name)
            self.save_config()

        if self.cfg.has_option("general", "sentry_dsn"):
            # Only import and start sentry SDK if configured.
            import sentry_sdk
            sentry_sdk.init(
                dsn=self.cfg.get("general", "sentry_dsn"),
            )
            with sentry_sdk.configure_scope() as scope:
                scope.set_tag("sydent_server_name", self.server_name)

        if self.cfg.has_option("general", "prometheus_port"):
            import prometheus_client
            prometheus_client.start_http_server(
                port=self.cfg.getint("general", "prometheus_port"),
                addr=self.cfg.get("general", "prometheus_addr"),
            )

        if self.cfg.has_option("general", "templates.path"):
            # Get the possible brands by looking at directories under the
            # templates.path directory.
            root_template_path = self.cfg.get("general", "templates.path")
            if os.path.exists(root_template_path):
                self.valid_brands = {
                    p for p in os.listdir(root_template_path) if os.path.isdir(os.path.join(root_template_path, p))
                }
            else:
                # This is a legacy code-path and assumes that verify_response_template,
                # email.template, and email.invite_template are defined.
                self.valid_brands = set()

        self.enable_v1_associations = parse_cfg_bool(
            self.cfg.get("general", "enable_v1_associations")
        )

        self.delete_tokens_on_bind = parse_cfg_bool(
            self.cfg.get("general", "delete_tokens_on_bind")
        )

        self.default_web_client_location = self.cfg.get(
            "email", "email.default_web_client_location"
        )
        self.username_obfuscate_characters = int(self.cfg.get(
            "email", "email.third_party_invite_username_obfuscate_characters"
        ))
        self.domain_obfuscate_characters = int(self.cfg.get(
            "email", "email.third_party_invite_domain_obfuscate_characters"
        ))

        # See if a pepper already exists in the database
        # Note: This MUST be run before we start serving requests, otherwise lookups for
        # 3PID hashes may come in before we've completed generating them
        hashing_metadata_store = HashingMetadataStore(self)
        lookup_pepper = hashing_metadata_store.get_lookup_pepper()
        if not lookup_pepper:
            # No pepper defined in the database, generate one
            lookup_pepper = generateAlphanumericTokenOfLength(5)

            # Store it in the database and rehash 3PIDs
            hashing_metadata_store.store_lookup_pepper(sha256_and_url_safe_base64,
                                                       lookup_pepper)

        self.validators = Validators()
        self.validators.email = EmailValidator(self)
        self.validators.msisdn = MsisdnValidator(self)

        self.keyring = Keyring()
        self.keyring.ed25519 = SydentEd25519(self).signing_key
        self.keyring.ed25519.alg = 'ed25519'

        self.sig_verifier = Verifier(self)

        self.servlets = Servlets()
        self.servlets.v1 = V1Servlet(self)
        self.servlets.v2 = V2Servlet(self)
        self.servlets.emailRequestCode = EmailRequestCodeServlet(self)
        self.servlets.emailValidate = EmailValidateCodeServlet(self)
        self.servlets.msisdnRequestCode = MsisdnRequestCodeServlet(self)
        self.servlets.msisdnValidate = MsisdnValidateCodeServlet(self)
        self.servlets.lookup = LookupServlet(self)
        self.servlets.bulk_lookup = BulkLookupServlet(self)
        self.servlets.hash_details = HashDetailsServlet(self, lookup_pepper)
        self.servlets.lookup_v2 = LookupV2Servlet(self, lookup_pepper)
        self.servlets.pubkey_ed25519 = Ed25519Servlet(self)
        self.servlets.pubkeyIsValid = PubkeyIsValidServlet(self)
        self.servlets.ephemeralPubkeyIsValid = EphemeralPubkeyIsValidServlet(self)
        self.servlets.threepidBind = ThreePidBindServlet(self)
        self.servlets.threepidUnbind = ThreePidUnbindServlet(self)
        self.servlets.replicationPush = ReplicationPushServlet(self)
        self.servlets.getValidated3pid = GetValidated3pidServlet(self)
        self.servlets.storeInviteServlet = StoreInviteServlet(self)
        self.servlets.blindlySignStuffServlet = BlindlySignStuffServlet(self)
        self.servlets.termsServlet = TermsServlet(self)
        self.servlets.accountServlet = AccountServlet(self)
        self.servlets.registerServlet = RegisterServlet(self)
        self.servlets.logoutServlet = LogoutServlet(self)

        self.threepidBinder = ThreepidBinder(self)

        self.sslComponents = SslComponents(self)

        self.clientApiHttpServer = ClientApiHttpServer(self)
        self.replicationHttpsServer = ReplicationHttpsServer(self)
        self.replicationHttpsClient = ReplicationHttpsClient(self)

        self.pusher = Pusher(self)

        # A dedicated validation session store just to clean up old sessions every N minutes
        self.cleanupValSession = ThreePidValSessionStore(self)
        cb = task.LoopingCall(self.cleanupValSession.deleteOldSessions)
        cb.clock = self.reactor
        cb.start(10 * 60.0)

        # workaround for https://github.com/getsentry/sentry-python/issues/803: we
        # disable automatic GC and run it periodically instead.
        gc.disable()
        cb = task.LoopingCall(run_gc)
        cb.clock = self.reactor
        cb.start(1.0)

    def save_config(self):
        fp = open(self.config_file, 'w')
        self.cfg.write(fp)
        fp.close()

    def run(self):
        self.clientApiHttpServer.setup()
        self.replicationHttpsServer.setup()
        self.pusher.setup()

        internalport = self.cfg.get('http', 'internalapi.http.port')
        if internalport:
            try:
                interface = self.cfg.get('http', 'internalapi.http.bind_address')
            except configparser.NoOptionError:
                interface = '::1'
            self.internalApiHttpServer = InternalApiHttpServer(self)
            self.internalApiHttpServer.setup(interface, int(internalport))

        if self.pidfile:
            with open(self.pidfile, 'w') as pidfile:
                pidfile.write(str(os.getpid()) + "\n")

        self.reactor.run()

    def ip_from_request(self, request):
        if (self.cfg.get('http', 'obey_x_forwarded_for') and
                request.requestHeaders.hasHeader("X-Forwarded-For")):
            return request.requestHeaders.getRawHeaders("X-Forwarded-For")[0]
        return request.getClientIP()

    def brand_from_request(self, request):
        """
        If the brand GET parameter is passed, returns that as a string, otherwise returns None.

        :param request: The incoming request.
        :type request: twisted.web.http.Request

        :return: The brand to use or None if no hint is found.
        :rtype: str or None
        """
        if b"brand" in request.args:
            return request.args[b"brand"][0].decode("utf-8")
        return None

    def get_branded_template(self, brand, template_name, deprecated_template_name):
        """
        Calculate a (maybe) branded template filename to use.

        If the deprecated email.template setting is defined, always use it.
        Otherwise, attempt to use the hinted brand from the request if the brand
        is valid. Otherwise, fallback to the default brand.

        :param brand: The hint of which brand to use.
        :type brand: str or None
        :param template_name: The name of the template file to load.
        :type template_name: str
        :param deprecated_template_name: The deprecated setting to use, if provided.
        :type deprecated_template_name: Tuple[str]

        :return: The template filename to use.
        :rtype: str
        """

        # If the deprecated setting is defined, return it.
        try:
            return self.cfg.get(*deprecated_template_name)
        except configparser.NoOptionError:
            pass

        # If a brand hint is provided, attempt to use it if it is valid.
        if brand:
            if brand not in self.valid_brands:
                brand = None

        # If the brand hint is not valid, or not provided, fallback to the default brand.
        if not brand:
            brand = self.cfg.get("general", "brand.default")

        root_template_path = self.cfg.get("general", "templates.path")
        return os.path.join(root_template_path, brand, template_name)
Example #5
0
class Sydent:
    def __init__(
        self,
        sydent_config: SydentConfig,
        reactor: SydentReactor = twisted.internet.reactor,  # type: ignore[assignment]
        use_tls_for_federation: bool = True,
    ):
        self.config = sydent_config

        self.reactor = reactor
        self.use_tls_for_federation = use_tls_for_federation

        logger.info("Starting Sydent server")

        self.db: sqlite3.Connection = SqliteDatabase(self).db

        if self.config.general.sentry_enabled:
            import sentry_sdk

            sentry_sdk.init(
                dsn=self.config.general.sentry_dsn, release=get_version_string()
            )
            with sentry_sdk.configure_scope() as scope:
                scope.set_tag("sydent_server_name", self.config.general.server_name)

            # workaround for https://github.com/getsentry/sentry-python/issues/803: we
            # disable automatic GC and run it periodically instead.
            gc.disable()
            cb = task.LoopingCall(run_gc)
            cb.clock = self.reactor
            cb.start(1.0)

        # See if a pepper already exists in the database
        # Note: This MUST be run before we start serving requests, otherwise lookups for
        # 3PID hashes may come in before we've completed generating them
        hashing_metadata_store = HashingMetadataStore(self)
        lookup_pepper = hashing_metadata_store.get_lookup_pepper()
        if not lookup_pepper:
            # No pepper defined in the database, generate one
            lookup_pepper = generateAlphanumericTokenOfLength(5)

            # Store it in the database and rehash 3PIDs
            hashing_metadata_store.store_lookup_pepper(
                sha256_and_url_safe_base64, lookup_pepper
            )

        self.validators: Validators = Validators(
            EmailValidator(self), MsisdnValidator(self)
        )

        self.keyring: Keyring = Keyring(self.config.crypto.signing_key)
        self.keyring.ed25519.alg = "ed25519"

        self.sig_verifier: Verifier = Verifier(self)

        self.servlets: Servlets = Servlets(self, lookup_pepper)

        self.threepidBinder: ThreepidBinder = ThreepidBinder(self)

        self.sslComponents: SslComponents = SslComponents(self)

        self.clientApiHttpServer = ClientApiHttpServer(self)
        self.replicationHttpsServer = ReplicationHttpsServer(self)
        self.replicationHttpsClient: ReplicationHttpsClient = ReplicationHttpsClient(
            self
        )

        self.pusher: Pusher = Pusher(self)

    def run(self) -> None:
        self.clientApiHttpServer.setup()
        self.replicationHttpsServer.setup()
        self.pusher.setup()
        self.maybe_start_prometheus_server()

        # A dedicated validation session store just to clean up old sessions every N minutes
        self.cleanupValSession = ThreePidValSessionStore(self)
        cb = task.LoopingCall(self.cleanupValSession.deleteOldSessions)
        cb.clock = self.reactor
        cb.start(10 * 60.0)

        if self.config.http.internal_port is not None:
            internalport = self.config.http.internal_port
            interface = self.config.http.internal_bind_address

            self.internalApiHttpServer = InternalApiHttpServer(self)
            self.internalApiHttpServer.setup(interface, internalport)

        if self.config.general.pidfile:
            with open(self.config.general.pidfile, "w") as pidfile:
                pidfile.write(str(os.getpid()) + "\n")

        self.reactor.run()

    def maybe_start_prometheus_server(self) -> None:
        if self.config.general.prometheus_enabled:
            import prometheus_client

            prometheus_client.start_http_server(
                port=self.config.general.prometheus_port,
                addr=self.config.general.prometheus_addr,
            )

    def ip_from_request(self, request: Request) -> Optional[str]:
        if self.config.http.obey_x_forwarded_for and request.requestHeaders.hasHeader(
            "X-Forwarded-For"
        ):
            # Type safety: hasHeaders returning True means that getRawHeaders
            # returns a nonempty list
            return request.requestHeaders.getRawHeaders("X-Forwarded-For")[0]  # type: ignore[index]
        client = request.getClientAddress()
        if isinstance(client, (address.IPv4Address, address.IPv6Address)):
            return client.host
        else:
            return None

    def brand_from_request(self, request: Request) -> Optional[str]:
        """
        If the brand GET parameter is passed, returns that as a string, otherwise returns None.

        :param request: The incoming request.

        :return: The brand to use or None if no hint is found.
        """
        if b"brand" in request.args:
            return request.args[b"brand"][0].decode("utf-8")
        return None

    def get_branded_template(
        self,
        brand: Optional[str],
        template_name: str,
    ) -> str:
        """
        Calculate a branded template filename to use.

        Attempt to use the hinted brand from the request if the brand
        is valid. Otherwise, fallback to the default brand.

        :param brand: The hint of which brand to use.
        :type brand: str or None
        :param template_name: The name of the template file to load.
        :type template_name: str

        :return: The template filename to use.
        :rtype: str
        """

        # If a brand hint is provided, attempt to use it if it is valid.
        if brand:
            if brand not in self.config.general.valid_brands:
                brand = None

        # If the brand hint is not valid, or not provided, fallback to the default brand.
        if not brand:
            brand = self.config.general.default_brand

        root_template_path = self.config.general.templates_path

        # Grab jinja template if it exists
        if os.path.exists(
            os.path.join(root_template_path, brand, template_name + ".j2")
        ):
            return os.path.join(brand, template_name + ".j2")
        else:
            return os.path.join(root_template_path, brand, template_name)