Support ACME for certificate provisioning (#4384)
This commit is contained in:
parent
12699a701f
commit
6129e52f43
|
@ -0,0 +1 @@
|
||||||
|
Synapse can now automatically provision TLS certificates via ACME (the protocol used by CAs like Let's Encrypt).
|
|
@ -10,12 +10,12 @@
|
||||||
# can be passed on the commandline for debugging.
|
# can be passed on the commandline for debugging.
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
DISTS = (
|
DISTS = (
|
||||||
"debian:stretch",
|
"debian:stretch",
|
||||||
|
|
|
@ -13,10 +13,12 @@
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import gc
|
import gc
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
|
|
||||||
|
@ -324,17 +326,12 @@ def setup(config_options):
|
||||||
|
|
||||||
events.USE_FROZEN_DICTS = config.use_frozen_dicts
|
events.USE_FROZEN_DICTS = config.use_frozen_dicts
|
||||||
|
|
||||||
tls_server_context_factory = context_factory.ServerContextFactory(config)
|
|
||||||
tls_client_options_factory = context_factory.ClientTLSOptionsFactory(config)
|
|
||||||
|
|
||||||
database_engine = create_engine(config.database_config)
|
database_engine = create_engine(config.database_config)
|
||||||
config.database_config["args"]["cp_openfun"] = database_engine.on_new_connection
|
config.database_config["args"]["cp_openfun"] = database_engine.on_new_connection
|
||||||
|
|
||||||
hs = SynapseHomeServer(
|
hs = SynapseHomeServer(
|
||||||
config.server_name,
|
config.server_name,
|
||||||
db_config=config.database_config,
|
db_config=config.database_config,
|
||||||
tls_server_context_factory=tls_server_context_factory,
|
|
||||||
tls_client_options_factory=tls_client_options_factory,
|
|
||||||
config=config,
|
config=config,
|
||||||
version_string="Synapse/" + get_version_string(synapse),
|
version_string="Synapse/" + get_version_string(synapse),
|
||||||
database_engine=database_engine,
|
database_engine=database_engine,
|
||||||
|
@ -361,12 +358,53 @@ def setup(config_options):
|
||||||
logger.info("Database prepared in %s.", config.database_config['name'])
|
logger.info("Database prepared in %s.", config.database_config['name'])
|
||||||
|
|
||||||
hs.setup()
|
hs.setup()
|
||||||
hs.start_listening()
|
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
def start():
|
def start():
|
||||||
hs.get_pusherpool().start()
|
try:
|
||||||
hs.get_datastore().start_profiling()
|
# Check if the certificate is still valid.
|
||||||
hs.get_datastore().start_doing_background_updates()
|
cert_days_remaining = hs.config.is_disk_cert_valid()
|
||||||
|
|
||||||
|
if hs.config.acme_enabled:
|
||||||
|
# If ACME is enabled, we might need to provision a certificate
|
||||||
|
# before starting.
|
||||||
|
acme = hs.get_acme_handler()
|
||||||
|
|
||||||
|
# Start up the webservices which we will respond to ACME
|
||||||
|
# challenges with.
|
||||||
|
yield acme.start_listening()
|
||||||
|
|
||||||
|
# We want to reprovision if cert_days_remaining is None (meaning no
|
||||||
|
# certificate exists), or the days remaining number it returns
|
||||||
|
# is less than our re-registration threshold.
|
||||||
|
if (cert_days_remaining is None) or (
|
||||||
|
not cert_days_remaining > hs.config.acme_reprovision_threshold
|
||||||
|
):
|
||||||
|
yield acme.provision_certificate()
|
||||||
|
|
||||||
|
# Read the certificate from disk and build the context factories for
|
||||||
|
# TLS.
|
||||||
|
hs.config.read_certificate_from_disk()
|
||||||
|
hs.tls_server_context_factory = context_factory.ServerContextFactory(config)
|
||||||
|
hs.tls_client_options_factory = context_factory.ClientTLSOptionsFactory(
|
||||||
|
config
|
||||||
|
)
|
||||||
|
|
||||||
|
# It is now safe to start your Synapse.
|
||||||
|
hs.start_listening()
|
||||||
|
hs.get_pusherpool().start()
|
||||||
|
hs.get_datastore().start_profiling()
|
||||||
|
hs.get_datastore().start_doing_background_updates()
|
||||||
|
except Exception as e:
|
||||||
|
# If a DeferredList failed (like in listening on the ACME listener),
|
||||||
|
# we need to print the subfailure explicitly.
|
||||||
|
if isinstance(e, defer.FirstError):
|
||||||
|
e.subFailure.printTraceback(sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Something else went wrong when starting. Print it and bail out.
|
||||||
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
reactor.callWhenRunning(start)
|
reactor.callWhenRunning(start)
|
||||||
|
|
||||||
|
|
|
@ -367,7 +367,7 @@ class Config(object):
|
||||||
if not keys_directory:
|
if not keys_directory:
|
||||||
keys_directory = os.path.dirname(config_files[-1])
|
keys_directory = os.path.dirname(config_files[-1])
|
||||||
|
|
||||||
config_dir_path = os.path.abspath(keys_directory)
|
self.config_dir_path = os.path.abspath(keys_directory)
|
||||||
|
|
||||||
specified_config = {}
|
specified_config = {}
|
||||||
for config_file in config_files:
|
for config_file in config_files:
|
||||||
|
@ -379,7 +379,7 @@ class Config(object):
|
||||||
|
|
||||||
server_name = specified_config["server_name"]
|
server_name = specified_config["server_name"]
|
||||||
config_string = self.generate_config(
|
config_string = self.generate_config(
|
||||||
config_dir_path=config_dir_path,
|
config_dir_path=self.config_dir_path,
|
||||||
data_dir_path=os.getcwd(),
|
data_dir_path=os.getcwd(),
|
||||||
server_name=server_name,
|
server_name=server_name,
|
||||||
generate_secrets=False,
|
generate_secrets=False,
|
||||||
|
|
|
@ -13,45 +13,38 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from datetime import datetime
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
|
|
||||||
from unpaddedbase64 import encode_base64
|
from unpaddedbase64 import encode_base64
|
||||||
|
|
||||||
from OpenSSL import crypto
|
from OpenSSL import crypto
|
||||||
|
|
||||||
from ._base import Config
|
from synapse.config._base import Config
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
class TlsConfig(Config):
|
class TlsConfig(Config):
|
||||||
def read_config(self, config):
|
def read_config(self, config):
|
||||||
self.tls_certificate = self.read_tls_certificate(
|
|
||||||
config.get("tls_certificate_path")
|
|
||||||
)
|
|
||||||
self.tls_certificate_file = config.get("tls_certificate_path")
|
|
||||||
|
|
||||||
|
acme_config = config.get("acme", {})
|
||||||
|
self.acme_enabled = acme_config.get("enabled", False)
|
||||||
|
self.acme_url = acme_config.get(
|
||||||
|
"url", "https://acme-v01.api.letsencrypt.org/directory"
|
||||||
|
)
|
||||||
|
self.acme_port = acme_config.get("port", 8449)
|
||||||
|
self.acme_bind_addresses = acme_config.get("bind_addresses", ["127.0.0.1"])
|
||||||
|
self.acme_reprovision_threshold = acme_config.get("reprovision_threshold", 30)
|
||||||
|
|
||||||
|
self.tls_certificate_file = os.path.abspath(config.get("tls_certificate_path"))
|
||||||
|
self.tls_private_key_file = os.path.abspath(config.get("tls_private_key_path"))
|
||||||
|
self._original_tls_fingerprints = config["tls_fingerprints"]
|
||||||
|
self.tls_fingerprints = list(self._original_tls_fingerprints)
|
||||||
self.no_tls = config.get("no_tls", False)
|
self.no_tls = config.get("no_tls", False)
|
||||||
|
|
||||||
if self.no_tls:
|
|
||||||
self.tls_private_key = None
|
|
||||||
else:
|
|
||||||
self.tls_private_key = self.read_tls_private_key(
|
|
||||||
config.get("tls_private_key_path")
|
|
||||||
)
|
|
||||||
|
|
||||||
self.tls_fingerprints = config["tls_fingerprints"]
|
|
||||||
|
|
||||||
# Check that our own certificate is included in the list of fingerprints
|
|
||||||
# and include it if it is not.
|
|
||||||
x509_certificate_bytes = crypto.dump_certificate(
|
|
||||||
crypto.FILETYPE_ASN1,
|
|
||||||
self.tls_certificate
|
|
||||||
)
|
|
||||||
sha256_fingerprint = encode_base64(sha256(x509_certificate_bytes).digest())
|
|
||||||
sha256_fingerprints = set(f["sha256"] for f in self.tls_fingerprints)
|
|
||||||
if sha256_fingerprint not in sha256_fingerprints:
|
|
||||||
self.tls_fingerprints.append({u"sha256": sha256_fingerprint})
|
|
||||||
|
|
||||||
# This config option applies to non-federation HTTP clients
|
# This config option applies to non-federation HTTP clients
|
||||||
# (e.g. for talking to recaptcha, identity servers, and such)
|
# (e.g. for talking to recaptcha, identity servers, and such)
|
||||||
# It should never be used in production, and is intended for
|
# It should never be used in production, and is intended for
|
||||||
|
@ -60,13 +53,70 @@ class TlsConfig(Config):
|
||||||
"use_insecure_ssl_client_just_for_testing_do_not_use"
|
"use_insecure_ssl_client_just_for_testing_do_not_use"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.tls_certificate = None
|
||||||
|
self.tls_private_key = None
|
||||||
|
|
||||||
|
def is_disk_cert_valid(self):
|
||||||
|
"""
|
||||||
|
Is the certificate we have on disk valid, and if so, for how long?
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Days remaining of certificate validity.
|
||||||
|
None: No certificate exists.
|
||||||
|
"""
|
||||||
|
if not os.path.exists(self.tls_certificate_file):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.tls_certificate_file, 'rb') as f:
|
||||||
|
cert_pem = f.read()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to read existing certificate off disk!")
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
tls_certificate = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to parse existing certificate off disk!")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# YYYYMMDDhhmmssZ -- in UTC
|
||||||
|
expires_on = datetime.strptime(
|
||||||
|
tls_certificate.get_notAfter().decode('ascii'), "%Y%m%d%H%M%SZ"
|
||||||
|
)
|
||||||
|
now = datetime.utcnow()
|
||||||
|
days_remaining = (expires_on - now).days
|
||||||
|
return days_remaining
|
||||||
|
|
||||||
|
def read_certificate_from_disk(self):
|
||||||
|
"""
|
||||||
|
Read the certificates from disk.
|
||||||
|
"""
|
||||||
|
self.tls_certificate = self.read_tls_certificate(self.tls_certificate_file)
|
||||||
|
|
||||||
|
if not self.no_tls:
|
||||||
|
self.tls_private_key = self.read_tls_private_key(self.tls_private_key_file)
|
||||||
|
|
||||||
|
self.tls_fingerprints = list(self._original_tls_fingerprints)
|
||||||
|
|
||||||
|
# Check that our own certificate is included in the list of fingerprints
|
||||||
|
# and include it if it is not.
|
||||||
|
x509_certificate_bytes = crypto.dump_certificate(
|
||||||
|
crypto.FILETYPE_ASN1, self.tls_certificate
|
||||||
|
)
|
||||||
|
sha256_fingerprint = encode_base64(sha256(x509_certificate_bytes).digest())
|
||||||
|
sha256_fingerprints = set(f["sha256"] for f in self.tls_fingerprints)
|
||||||
|
if sha256_fingerprint not in sha256_fingerprints:
|
||||||
|
self.tls_fingerprints.append({u"sha256": sha256_fingerprint})
|
||||||
|
|
||||||
def default_config(self, config_dir_path, server_name, **kwargs):
|
def default_config(self, config_dir_path, server_name, **kwargs):
|
||||||
base_key_name = os.path.join(config_dir_path, server_name)
|
base_key_name = os.path.join(config_dir_path, server_name)
|
||||||
|
|
||||||
tls_certificate_path = base_key_name + ".tls.crt"
|
tls_certificate_path = base_key_name + ".tls.crt"
|
||||||
tls_private_key_path = base_key_name + ".tls.key"
|
tls_private_key_path = base_key_name + ".tls.key"
|
||||||
|
|
||||||
return """\
|
return (
|
||||||
|
"""\
|
||||||
# PEM encoded X509 certificate for TLS.
|
# PEM encoded X509 certificate for TLS.
|
||||||
# You can replace the self-signed certificate that synapse
|
# You can replace the self-signed certificate that synapse
|
||||||
# autogenerates on launch with your own SSL certificate + key pair
|
# autogenerates on launch with your own SSL certificate + key pair
|
||||||
|
@ -107,7 +157,24 @@ class TlsConfig(Config):
|
||||||
#
|
#
|
||||||
tls_fingerprints: []
|
tls_fingerprints: []
|
||||||
# tls_fingerprints: [{"sha256": "<base64_encoded_sha256_fingerprint>"}]
|
# tls_fingerprints: [{"sha256": "<base64_encoded_sha256_fingerprint>"}]
|
||||||
""" % locals()
|
|
||||||
|
## Support for ACME certificate auto-provisioning.
|
||||||
|
# acme:
|
||||||
|
# enabled: false
|
||||||
|
## ACME path.
|
||||||
|
## If you only want to test, use the staging url:
|
||||||
|
## https://acme-staging.api.letsencrypt.org/directory
|
||||||
|
# url: 'https://acme-v01.api.letsencrypt.org/directory'
|
||||||
|
## Port number (to listen for the HTTP-01 challenge).
|
||||||
|
## Using port 80 requires utilising something like authbind, or proxying to it.
|
||||||
|
# port: 8449
|
||||||
|
## Hosts to bind to.
|
||||||
|
# bind_addresses: ['127.0.0.1']
|
||||||
|
## How many days remaining on a certificate before it is renewed.
|
||||||
|
# reprovision_threshold: 30
|
||||||
|
"""
|
||||||
|
% locals()
|
||||||
|
)
|
||||||
|
|
||||||
def read_tls_certificate(self, cert_path):
|
def read_tls_certificate(self, cert_path):
|
||||||
cert_pem = self.read_file(cert_path, "tls_certificate")
|
cert_pem = self.read_file(cert_path, "tls_certificate")
|
||||||
|
|
|
@ -0,0 +1,147 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2019 New Vector Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import attr
|
||||||
|
from zope.interface import implementer
|
||||||
|
|
||||||
|
from twisted.internet import defer
|
||||||
|
from twisted.internet.endpoints import serverFromString
|
||||||
|
from twisted.python.filepath import FilePath
|
||||||
|
from twisted.python.url import URL
|
||||||
|
from twisted.web import server, static
|
||||||
|
from twisted.web.resource import Resource
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from txacme.interfaces import ICertificateStore
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
@implementer(ICertificateStore)
|
||||||
|
class ErsatzStore(object):
|
||||||
|
"""
|
||||||
|
A store that only stores in memory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
certs = attr.ib(default=attr.Factory(dict))
|
||||||
|
|
||||||
|
def store(self, server_name, pem_objects):
|
||||||
|
self.certs[server_name] = [o.as_bytes() for o in pem_objects]
|
||||||
|
return defer.succeed(None)
|
||||||
|
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
# txacme is missing
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AcmeHandler(object):
|
||||||
|
def __init__(self, hs):
|
||||||
|
self.hs = hs
|
||||||
|
self.reactor = hs.get_reactor()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def start_listening(self):
|
||||||
|
|
||||||
|
# Configure logging for txacme, if you need to debug
|
||||||
|
# from eliot import add_destinations
|
||||||
|
# from eliot.twisted import TwistedDestination
|
||||||
|
#
|
||||||
|
# add_destinations(TwistedDestination())
|
||||||
|
|
||||||
|
from txacme.challenges import HTTP01Responder
|
||||||
|
from txacme.service import AcmeIssuingService
|
||||||
|
from txacme.endpoint import load_or_create_client_key
|
||||||
|
from txacme.client import Client
|
||||||
|
from josepy.jwa import RS256
|
||||||
|
|
||||||
|
self._store = ErsatzStore()
|
||||||
|
responder = HTTP01Responder()
|
||||||
|
|
||||||
|
self._issuer = AcmeIssuingService(
|
||||||
|
cert_store=self._store,
|
||||||
|
client_creator=(
|
||||||
|
lambda: Client.from_url(
|
||||||
|
reactor=self.reactor,
|
||||||
|
url=URL.from_text(self.hs.config.acme_url),
|
||||||
|
key=load_or_create_client_key(
|
||||||
|
FilePath(self.hs.config.config_dir_path)
|
||||||
|
),
|
||||||
|
alg=RS256,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
clock=self.reactor,
|
||||||
|
responders=[responder],
|
||||||
|
)
|
||||||
|
|
||||||
|
well_known = Resource()
|
||||||
|
well_known.putChild(b'acme-challenge', responder.resource)
|
||||||
|
responder_resource = Resource()
|
||||||
|
responder_resource.putChild(b'.well-known', well_known)
|
||||||
|
responder_resource.putChild(b'check', static.Data(b'OK', b'text/plain'))
|
||||||
|
|
||||||
|
srv = server.Site(responder_resource)
|
||||||
|
|
||||||
|
listeners = []
|
||||||
|
|
||||||
|
for host in self.hs.config.acme_bind_addresses:
|
||||||
|
logger.info(
|
||||||
|
"Listening for ACME requests on %s:%s", host, self.hs.config.acme_port
|
||||||
|
)
|
||||||
|
endpoint = serverFromString(
|
||||||
|
self.reactor, "tcp:%s:interface=%s" % (self.hs.config.acme_port, host)
|
||||||
|
)
|
||||||
|
listeners.append(endpoint.listen(srv))
|
||||||
|
|
||||||
|
# Make sure we are registered to the ACME server. There's no public API
|
||||||
|
# for this, it is usually triggered by startService, but since we don't
|
||||||
|
# want it to control where we save the certificates, we have to reach in
|
||||||
|
# and trigger the registration machinery ourselves.
|
||||||
|
self._issuer._registered = False
|
||||||
|
yield self._issuer._ensure_registered()
|
||||||
|
|
||||||
|
# Return a Deferred that will fire when all the servers have started up.
|
||||||
|
yield defer.DeferredList(listeners, fireOnOneErrback=True, consumeErrors=True)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def provision_certificate(self):
|
||||||
|
|
||||||
|
logger.warning("Reprovisioning %s", self.hs.hostname)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield self._issuer.issue_cert(self.hs.hostname)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Fail!")
|
||||||
|
raise
|
||||||
|
logger.warning("Reprovisioned %s, saving.", self.hs.hostname)
|
||||||
|
cert_chain = self._store.certs[self.hs.hostname]
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.hs.config.tls_private_key_file, "wb") as private_key_file:
|
||||||
|
for x in cert_chain:
|
||||||
|
if x.startswith(b"-----BEGIN RSA PRIVATE KEY-----"):
|
||||||
|
private_key_file.write(x)
|
||||||
|
|
||||||
|
with open(self.hs.config.tls_certificate_file, "wb") as certificate_file:
|
||||||
|
for x in cert_chain:
|
||||||
|
if x.startswith(b"-----BEGIN CERTIFICATE-----"):
|
||||||
|
certificate_file.write(x)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed saving!")
|
||||||
|
raise
|
||||||
|
|
||||||
|
defer.returnValue(True)
|
|
@ -79,6 +79,10 @@ CONDITIONAL_REQUIREMENTS = {
|
||||||
# ConsentResource uses select_autoescape, which arrived in jinja 2.9
|
# ConsentResource uses select_autoescape, which arrived in jinja 2.9
|
||||||
"resources.consent": ["Jinja2>=2.9"],
|
"resources.consent": ["Jinja2>=2.9"],
|
||||||
|
|
||||||
|
# ACME support is required to provision TLS certificates from authorities
|
||||||
|
# that use the protocol, such as Let's Encrypt.
|
||||||
|
"acme": ["txacme>=0.9.2"],
|
||||||
|
|
||||||
"saml2": ["pysaml2>=4.5.0"],
|
"saml2": ["pysaml2>=4.5.0"],
|
||||||
"url_preview": ["lxml>=3.5.0"],
|
"url_preview": ["lxml>=3.5.0"],
|
||||||
"test": ["mock>=2.0"],
|
"test": ["mock>=2.0"],
|
||||||
|
|
|
@ -46,6 +46,7 @@ from synapse.federation.transport.client import TransportLayerClient
|
||||||
from synapse.groups.attestations import GroupAttestationSigning, GroupAttestionRenewer
|
from synapse.groups.attestations import GroupAttestationSigning, GroupAttestionRenewer
|
||||||
from synapse.groups.groups_server import GroupsServerHandler
|
from synapse.groups.groups_server import GroupsServerHandler
|
||||||
from synapse.handlers import Handlers
|
from synapse.handlers import Handlers
|
||||||
|
from synapse.handlers.acme import AcmeHandler
|
||||||
from synapse.handlers.appservice import ApplicationServicesHandler
|
from synapse.handlers.appservice import ApplicationServicesHandler
|
||||||
from synapse.handlers.auth import AuthHandler, MacaroonGenerator
|
from synapse.handlers.auth import AuthHandler, MacaroonGenerator
|
||||||
from synapse.handlers.deactivate_account import DeactivateAccountHandler
|
from synapse.handlers.deactivate_account import DeactivateAccountHandler
|
||||||
|
@ -129,6 +130,7 @@ class HomeServer(object):
|
||||||
'sync_handler',
|
'sync_handler',
|
||||||
'typing_handler',
|
'typing_handler',
|
||||||
'room_list_handler',
|
'room_list_handler',
|
||||||
|
'acme_handler',
|
||||||
'auth_handler',
|
'auth_handler',
|
||||||
'device_handler',
|
'device_handler',
|
||||||
'e2e_keys_handler',
|
'e2e_keys_handler',
|
||||||
|
@ -310,6 +312,9 @@ class HomeServer(object):
|
||||||
def build_e2e_room_keys_handler(self):
|
def build_e2e_room_keys_handler(self):
|
||||||
return E2eRoomKeysHandler(self)
|
return E2eRoomKeysHandler(self)
|
||||||
|
|
||||||
|
def build_acme_handler(self):
|
||||||
|
return AcmeHandler(self)
|
||||||
|
|
||||||
def build_application_service_api(self):
|
def build_application_service_api(self):
|
||||||
return ApplicationServiceApi(self)
|
return ApplicationServiceApi(self)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue