diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py
index 6d292ccf9a..44830e1325 100755
--- a/synapse/app/homeserver.py
+++ b/synapse/app/homeserver.py
@@ -20,7 +20,6 @@ from synapse.server import HomeServer
from twisted.internet import reactor
from twisted.enterprise import adbapi
-from twisted.python.log import PythonLoggingObserver
from twisted.web.resource import Resource
from twisted.web.static import File
from twisted.web.server import Site
@@ -29,16 +28,17 @@ from synapse.http.client import TwistedHttpClient
from synapse.api.urls import (
CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX
)
+from synapse.config.homeserver import HomeServerConfig
+from synapse.crypto import context_factory
from daemonize import Daemonize
import twisted.manhole.telnet
-import argparse
import logging
-import logging.config
import sqlite3
import os
import re
+import sys
logger = logging.getLogger(__name__)
@@ -62,7 +62,7 @@ SCHEMA_VERSION = 1
class SynapseHomeServer(HomeServer):
def build_http_client(self):
- return TwistedHttpClient()
+ return TwistedHttpClient(self)
def build_resource_for_client(self):
return JsonResource()
@@ -207,36 +207,10 @@ class SynapseHomeServer(HomeServer):
return "%s-%s" % (resource, path_seg)
def start_listening(self, port):
- reactor.listenTCP(port, Site(self.root_resource))
- logger.info("Synapse now listening on port %d", port)
-
-
-def setup_logging(verbosity=0, filename=None, config_path=None):
- """ Sets up logging with verbosity levels.
-
- Args:
- verbosity: The verbosity level.
- filename: Log to the given file rather than to the console.
- config_path: Path to a python logging config file.
- """
-
- if config_path is None:
- log_format = (
- '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s'
+ reactor.listenSSL(
+ port, Site(self.root_resource), self.tls_context_factory
)
-
- level = logging.INFO
- if verbosity:
- level = logging.DEBUG
-
- # FIXME: we need a logging.WARN for a -q quiet option
-
- logging.basicConfig(level=level, filename=filename, format=log_format)
- else:
- logging.config.fileConfig(config_path)
-
- observer = PythonLoggingObserver()
- observer.start()
+ logger.info("Synapse now listening on port %d", port)
def run():
@@ -244,78 +218,53 @@ def run():
def setup():
- parser = argparse.ArgumentParser()
- parser.add_argument("-p", "--port", dest="port", type=int, default=8080,
- help="The port to listen on.")
- parser.add_argument("-d", "--database", dest="db", default="homeserver.db",
- help="The database name.")
- parser.add_argument("-H", "--host", dest="host", default="localhost",
- help="The hostname of the server.")
- parser.add_argument('-v', '--verbose', dest="verbose", action='count',
- help="The verbosity level.")
- parser.add_argument('-f', '--log-file', dest="log_file", default=None,
- help="File to log to.")
- parser.add_argument('--log-config', dest="log_config", default=None,
- help="Python logging config")
- parser.add_argument('-D', '--daemonize', action='store_true',
- default=False, help="Daemonize the home server")
- parser.add_argument('--pid-file', dest="pid", help="When running as a "
- "daemon, the file to store the pid in",
- default="hs.pid")
- parser.add_argument("-W", "--webclient", dest="webclient", default=True,
- action="store_false", help="Don't host a web client.")
- parser.add_argument("--manhole", dest="manhole", type=int, default=None,
- help="Turn on the twisted telnet manhole service.")
- args = parser.parse_args()
-
- verbosity = int(args.verbose) if args.verbose else None
-
- # Because if/when we daemonize we change to root dir.
- db_name = os.path.abspath(args.db)
- log_file = args.log_file
- if log_file:
- log_file = os.path.abspath(log_file)
-
- setup_logging(
- verbosity=verbosity,
- filename=log_file,
- config_path=args.log_config,
+ config = HomeServerConfig.load_config(
+ "Synapse Homeserver",
+ sys.argv[1:],
+ generate_section="Homeserver"
)
- logger.info("Server hostname: %s", args.host)
+ config.setup_logging()
- if re.search(":[0-9]+$", args.host):
- domain_with_port = args.host
+ logger.info("Server hostname: %s", config.server_name)
+
+ if re.search(":[0-9]+$", config.server_name):
+ domain_with_port = config.server_name
else:
- domain_with_port = "%s:%s" % (args.host, args.port)
+ domain_with_port = "%s:%s" % (config.server_name, config.bind_port)
+
+ tls_context_factory = context_factory.ServerContextFactory(config)
hs = SynapseHomeServer(
- args.host,
+ config.server_name,
domain_with_port=domain_with_port,
upload_dir=os.path.abspath("uploads"),
- db_name=db_name,
+ db_name=config.database_path,
+ tls_context_factory=tls_context_factory,
)
hs.register_servlets()
hs.create_resource_tree(
- web_client=args.webclient,
- redirect_root_to_web_client=True)
- hs.start_listening(args.port)
+ web_client=config.webclient,
+ redirect_root_to_web_client=True,
+ )
+ hs.start_listening(config.bind_port)
hs.get_db_pool()
- if args.manhole:
+ if config.manhole:
f = twisted.manhole.telnet.ShellFactory()
f.username = "matrix"
f.password = "rabbithole"
f.namespace['hs'] = hs
- reactor.listenTCP(args.manhole, f, interface='127.0.0.1')
+ reactor.listenTCP(config.manhole, f, interface='127.0.0.1')
- if args.daemonize:
+ if config.daemonize:
+ print config.pid_file
daemon = Daemonize(
app="synapse-homeserver",
- pid=args.pid,
+ pid=config.pid_file,
action=run,
auto_close_fds=False,
verbose=True,
diff --git a/synapse/config/__init__.py b/synapse/config/__init__.py
new file mode 100644
index 0000000000..fe8a073cd3
--- /dev/null
+++ b/synapse/config/__init__.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 matrix.org
+#
+# 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.
diff --git a/synapse/config/_base.py b/synapse/config/_base.py
new file mode 100644
index 0000000000..78197e4a75
--- /dev/null
+++ b/synapse/config/_base.py
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 matrix.org
+#
+# 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 ConfigParser as configparser
+import argparse
+import sys
+import os
+
+
+class Config(object):
+ def __init__(self, args):
+ pass
+
+ @staticmethod
+ def abspath(file_path):
+ return os.path.abspath(file_path) if file_path else file_path
+
+ @staticmethod
+ def read_file(file_path):
+ with open(file_path) as file_stream:
+ return file_stream.read()
+
+ @staticmethod
+ def read_config_file(file_path):
+ config = configparser.SafeConfigParser()
+ config.read([file_path])
+ config_dict = {}
+ for section in config.sections():
+ config_dict.update(config.items(section))
+ return config_dict
+
+ @classmethod
+ def add_arguments(cls, parser):
+ pass
+
+ @classmethod
+ def generate_config(cls, args, config_dir_path):
+ pass
+
+ @classmethod
+ def load_config(cls, description, argv, generate_section=None):
+ config_parser = argparse.ArgumentParser(add_help=False)
+ config_parser.add_argument(
+ "-c", "--config-path",
+ metavar="CONFIG_FILE",
+ help="Specify config file"
+ )
+ config_parser.add_argument(
+ "--generate-config",
+ action="store_true",
+ help="Generate config file"
+ )
+ config_args, remaining_args = config_parser.parse_known_args(argv)
+
+ if config_args.generate_config:
+ if not config_args.config_path:
+ config_parser.error(
+ "Must specify where to generate the config file"
+ )
+ config_dir_path = os.path.dirname(config_args.config_path)
+ if os.path.exists(config_args.config_path):
+ defaults = cls.read_config_file(config_args.config_path)
+ else:
+ defaults = {}
+ else:
+ if config_args.config_path:
+ defaults = cls.read_config_file(config_args.config_path)
+ else:
+ defaults = {}
+
+ parser = argparse.ArgumentParser(
+ parents=[config_parser],
+ description=description,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ cls.add_arguments(parser)
+ parser.set_defaults(**defaults)
+
+ args = parser.parse_args(remaining_args)
+
+ if config_args.generate_config:
+ config_dir_path = os.path.dirname(config_args.config_path)
+ config_dir_path = os.path.abspath(config_dir_path)
+ cls.generate_config(args, config_dir_path)
+ config = configparser.SafeConfigParser()
+ config.add_section(generate_section)
+ for key, value in vars(args).items():
+ if (key not in set(["config_path", "generate_config"])
+ and value is not None):
+ print key, "=", value
+ config.set(generate_section, key, str(value))
+ with open(config_args.config_path, "w") as config_file:
+ config.write(config_file)
+ sys.exit(0)
+
+ return cls(args)
+
+
+
diff --git a/synapse/config/database.py b/synapse/config/database.py
new file mode 100644
index 0000000000..edf2361914
--- /dev/null
+++ b/synapse/config/database.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 matrix.org
+#
+# 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.
+
+from ._base import Config
+import os
+
+class DatabaseConfig(Config):
+ def __init__(self, args):
+ super(DatabaseConfig, self).__init__(args)
+ self.database_path = self.abspath(args.database_path)
+
+ @classmethod
+ def add_arguments(cls, parser):
+ super(DatabaseConfig, cls).add_arguments(parser)
+ db_group = parser.add_argument_group("database")
+ db_group.add_argument(
+ "-d", "--database-path", default="homeserver.db",
+ help="The database name."
+ )
+
+ @classmethod
+ def generate_config(cls, args, config_dir_path):
+ super(DatabaseConfig, cls).generate_config(args, config_dir_path)
+ args.database_path = os.path.abspath(args.database_path)
+
diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py
new file mode 100644
index 0000000000..18072e3196
--- /dev/null
+++ b/synapse/config/homeserver.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 matrix.org
+#
+# 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.
+
+from .tls import TlsConfig
+from .server import ServerConfig
+from .logger import LoggingConfig
+from .database import DatabaseConfig
+
+class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig):
+ pass
+
+if __name__=='__main__':
+ import sys
+ HomeServerConfig.load_config("Generate config", sys.argv[1:], "HomeServer")
diff --git a/synapse/config/logger.py b/synapse/config/logger.py
new file mode 100644
index 0000000000..8db6621ae8
--- /dev/null
+++ b/synapse/config/logger.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 matrix.org
+#
+# 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.
+
+from ._base import Config
+
+from twisted.python.log import PythonLoggingObserver
+import logging
+import logging.config
+
+class LoggingConfig(Config):
+ def __init__(self, args):
+ super(LoggingConfig, self).__init__(args)
+ self.verbosity = int(args.verbose) if args.verbose else None
+ self.log_config = self.abspath(args.log_config)
+ self.log_file = self.abspath(args.log_file)
+
+ @classmethod
+ def add_arguments(cls, parser):
+ super(LoggingConfig, cls).add_arguments(parser)
+ logging_group = parser.add_argument_group("logging")
+ logging_group.add_argument(
+ '-v', '--verbose', dest="verbose", action='count',
+ help="The verbosity level."
+ )
+ logging_group.add_argument(
+ '-f', '--log-file', dest="log_file", default=None,
+ help="File to log to."
+ )
+ logging_group.add_argument(
+ '--log-config', dest="log_config", default=None,
+ help="Python logging config file"
+ )
+
+ def setup_logging(self):
+ log_format = (
+ '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s'
+ )
+ if self.log_config is None:
+
+ level = logging.INFO
+ if self.verbosity:
+ level = logging.DEBUG
+
+ # FIXME: we need a logging.WARN for a -q quiet option
+
+ logging.basicConfig(
+ level=level,
+ filename=self.log_file,
+ format=log_format
+ )
+ else:
+ logging.config.fileConfig(self.log_config)
+
+ observer = PythonLoggingObserver()
+ observer.start()
diff --git a/synapse/config/server.py b/synapse/config/server.py
new file mode 100644
index 0000000000..7e8ff6a703
--- /dev/null
+++ b/synapse/config/server.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 matrix.org
+#
+# 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 nacl.signing
+import os
+from ._base import Config
+from syutil.base64util import encode_base64, decode_base64
+
+
+class ServerConfig(Config):
+ def __init__(self, args):
+ super(ServerConfig, self).__init__(args)
+ self.server_name = args.server_name
+ self.signing_key = self.read_signing_key(args.signing_key_path)
+ self.bind_port = args.bind_port
+ self.bind_host = args.bind_host
+ self.daemonize = args.daemonize
+ self.pid_file = self.abspath(args.pid_file)
+ self.webclient = args.no_webclient
+ self.manhole = args.manhole
+
+ @classmethod
+ def add_arguments(cls, parser):
+ super(ServerConfig, cls).add_arguments(parser)
+ server_group = parser.add_argument_group("server")
+ server_group.add_argument("-H", "--server-name", default="localhost",
+ help="The name of the server")
+ server_group.add_argument("--signing-key-path",
+ help="The signing key to sign messages with")
+ server_group.add_argument("-p", "--bind-port", type=int,
+ help="TCP port to listen on")
+ server_group.add_argument("--bind-host", default="",
+ help="Local interface to listen on")
+ server_group.add_argument("-D", "--daemonize", action='store_true',
+ help="Daemonize the home server")
+ server_group.add_argument('--pid-file', default="hs.pid",
+ help="When running as a daemon, the file to"
+ " store the pid in")
+ server_group.add_argument("-W", "--no-webclient", default=True,
+ action="store_false",
+ help="Don't host a web client.")
+ server_group.add_argument("--manhole", dest="manhole", type=int,
+ help="Turn on the twisted telnet manhole"
+ " service on the given port.")
+
+ def read_signing_key(self, signing_key_path):
+ signing_key_base64 = self.read_file(signing_key_path)
+ signing_key_bytes = decode_base64(signing_key_base64)
+ return nacl.signing.SigningKey(signing_key_bytes)
+
+ @classmethod
+ def generate_config(cls, args, config_dir_path):
+ super(ServerConfig, cls).generate_config(args, config_dir_path)
+ base_key_name = os.path.join(config_dir_path, args.server_name)
+
+ args.pid_file = os.path.abspath(args.pid_file)
+
+ if not args.signing_key_path:
+ args.signing_key_path = base_key_name + ".signing.key"
+
+ if not os.path.exists(args.signing_key_path):
+ with open(args.signing_key_path, "w") as signing_key_file:
+ key = nacl.signing.SigningKey.generate()
+ signing_key_file.write(encode_base64(key.encode()))
diff --git a/synapse/config/tls.py b/synapse/config/tls.py
new file mode 100644
index 0000000000..7a3d6e3a02
--- /dev/null
+++ b/synapse/config/tls.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 matrix.org
+#
+# 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.
+
+from ._base import Config
+
+from OpenSSL import crypto
+import subprocess
+import os
+
+class TlsConfig(Config):
+ def __init__(self, args):
+ super(TlsConfig, self).__init__(args)
+ self.tls_certificate = self.read_tls_certificate(
+ args.tls_certificate_path
+ )
+ self.tls_private_key = self.read_tls_private_key(
+ args.tls_private_key_path
+ )
+ self.tls_dh_params_path = self.abspath(args.tls_dh_params_path)
+
+ @classmethod
+ def add_arguments(cls, parser):
+ super(TlsConfig, cls).add_arguments(parser)
+ tls_group = parser.add_argument_group("tls")
+ tls_group.add_argument("--tls-certificate-path",
+ help="PEM encoded X509 certificate for TLS")
+ tls_group.add_argument("--tls-private-key-path",
+ help="PEM encoded private key for TLS")
+ tls_group.add_argument("--tls-dh-params-path",
+ help="PEM dh parameters for ephemeral keys")
+
+ def read_tls_certificate(self, cert_path):
+ cert_pem = self.read_file(cert_path)
+ return crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
+
+ def read_tls_private_key(self, private_key_path):
+ private_key_pem = self.read_file(private_key_path)
+ return crypto.load_privatekey(crypto.FILETYPE_PEM, private_key_pem)
+
+ @classmethod
+ def generate_config(cls, args, config_dir_path):
+ super(TlsConfig, cls).generate_config(args, config_dir_path)
+ base_key_name = os.path.join(config_dir_path, args.server_name)
+
+ if args.tls_certificate_path is None:
+ args.tls_certificate_path = base_key_name + ".tls.crt"
+
+ if args.tls_private_key_path is None:
+ args.tls_private_key_path = base_key_name + ".tls.key"
+
+ if args.tls_dh_params_path is None:
+ args.tls_dh_params_path = base_key_name + ".tls.dh"
+
+ if not os.path.exists(args.tls_private_key_path):
+ with open(args.tls_private_key_path, "w") as private_key_file:
+ tls_private_key = crypto.PKey()
+ tls_private_key.generate_key(crypto.TYPE_RSA, 2048)
+ private_key_pem = crypto.dump_privatekey(
+ crypto.FILETYPE_PEM, tls_private_key
+ )
+ private_key_file.write(private_key_pem)
+ else:
+ with open(args.tls_private_key_path) as private_key_file:
+ private_key_pem = private_key_file.read()
+ tls_private_key = crypto.load_privatekey(
+ crypto.FILETYPE_PEM, private_key_pem
+ )
+
+ if not os.path.exists(args.tls_certificate_path):
+ with open(args.tls_certificate_path, "w") as certifcate_file:
+ cert = crypto.X509()
+ subject = cert.get_subject()
+ subject.CN = args.server_name
+
+ cert.set_serial_number(1000)
+ cert.gmtime_adj_notBefore(0)
+ cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
+ cert.set_issuer(cert.get_subject())
+ cert.set_pubkey(tls_private_key)
+
+ cert.sign(tls_private_key, 'sha256')
+
+ cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
+
+ certifcate_file.write(cert_pem)
+
+ if not os.path.exists(args.tls_dh_params_path):
+ subprocess.check_call([
+ "openssl", "dhparam",
+ "-outform", "PEM",
+ "-out", args.tls_dh_params_path,
+ "2048"
+ ])
+
diff --git a/synapse/crypto/config.py b/synapse/crypto/config.py
deleted file mode 100644
index 2330133e71..0000000000
--- a/synapse/crypto/config.py
+++ /dev/null
@@ -1,160 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2014 matrix.org
-#
-# 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 ConfigParser as configparser
-import argparse
-import socket
-import sys
-import os
-from OpenSSL import crypto
-import nacl.signing
-from syutil.base64util import encode_base64
-import subprocess
-
-
-def load_config(description, argv):
- config_parser = argparse.ArgumentParser(add_help=False)
- config_parser.add_argument("-c", "--config-path", metavar="CONFIG_FILE",
- help="Specify config file")
- config_args, remaining_args = config_parser.parse_known_args(argv)
- if config_args.config_path:
- config = configparser.SafeConfigParser()
- config.read([config_args.config_path])
- defaults = dict(config.items("KeyServer"))
- else:
- defaults = {}
- parser = argparse.ArgumentParser(
- parents=[config_parser],
- description=description,
- formatter_class=argparse.RawDescriptionHelpFormatter,
- )
- parser.set_defaults(**defaults)
- parser.add_argument("--server-name", default=socket.getfqdn(),
- help="The name of the server")
- parser.add_argument("--signing-key-path",
- help="The signing key to sign responses with")
- parser.add_argument("--tls-certificate-path",
- help="PEM encoded X509 certificate for TLS")
- parser.add_argument("--tls-private-key-path",
- help="PEM encoded private key for TLS")
- parser.add_argument("--tls-dh-params-path",
- help="PEM encoded dh parameters for ephemeral keys")
- parser.add_argument("--bind-port", type=int,
- help="TCP port to listen on")
- parser.add_argument("--bind-host", default="",
- help="Local interface to listen on")
-
- args = parser.parse_args(remaining_args)
-
- server_config = vars(args)
- del server_config["config_path"]
- return server_config
-
-
-def generate_config(argv):
- parser = argparse.ArgumentParser()
- parser.add_argument("-c", "--config-path", help="Specify config file",
- metavar="CONFIG_FILE", required=True)
- parser.add_argument("--server-name", default=socket.getfqdn(),
- help="The name of the server")
- parser.add_argument("--signing-key-path",
- help="The signing key to sign responses with")
- parser.add_argument("--tls-certificate-path",
- help="PEM encoded X509 certificate for TLS")
- parser.add_argument("--tls-private-key-path",
- help="PEM encoded private key for TLS")
- parser.add_argument("--tls-dh-params-path",
- help="PEM encoded dh parameters for ephemeral keys")
- parser.add_argument("--bind-port", type=int, required=True,
- help="TCP port to listen on")
- parser.add_argument("--bind-host", default="",
- help="Local interface to listen on")
-
- args = parser.parse_args(argv)
-
- dir_name = os.path.dirname(args.config_path)
- base_key_name = os.path.join(dir_name, args.server_name)
-
- if args.signing_key_path is None:
- args.signing_key_path = base_key_name + ".signing.key"
-
- if args.tls_certificate_path is None:
- args.tls_certificate_path = base_key_name + ".tls.crt"
-
- if args.tls_private_key_path is None:
- args.tls_private_key_path = base_key_name + ".tls.key"
-
- if args.tls_dh_params_path is None:
- args.tls_dh_params_path = base_key_name + ".tls.dh"
-
- if not os.path.exists(args.signing_key_path):
- with open(args.signing_key_path, "w") as signing_key_file:
- key = nacl.signing.SigningKey.generate()
- signing_key_file.write(encode_base64(key.encode()))
-
- if not os.path.exists(args.tls_private_key_path):
- with open(args.tls_private_key_path, "w") as private_key_file:
- tls_private_key = crypto.PKey()
- tls_private_key.generate_key(crypto.TYPE_RSA, 2048)
- private_key_pem = crypto.dump_privatekey(
- crypto.FILETYPE_PEM, tls_private_key
- )
- private_key_file.write(private_key_pem)
- else:
- with open(args.tls_private_key_path) as private_key_file:
- private_key_pem = private_key_file.read()
- tls_private_key = crypto.load_privatekey(
- crypto.FILETYPE_PEM, private_key_pem
- )
-
- if not os.path.exists(args.tls_certificate_path):
- with open(args.tls_certificate_path, "w") as certifcate_file:
- cert = crypto.X509()
- subject = cert.get_subject()
- subject.CN = args.server_name
-
- cert.set_serial_number(1000)
- cert.gmtime_adj_notBefore(0)
- cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
- cert.set_issuer(cert.get_subject())
- cert.set_pubkey(tls_private_key)
-
- cert.sign(tls_private_key, 'sha256')
-
- cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
-
- certifcate_file.write(cert_pem)
-
- if not os.path.exists(args.tls_dh_params_path):
- subprocess.check_call([
- "openssl", "dhparam",
- "-outform", "PEM",
- "-out", args.tls_dh_params_path,
- "2048"
- ])
-
- config = configparser.SafeConfigParser()
- config.add_section("KeyServer")
- for key, value in vars(args).items():
- if key != "config_path":
- config.set("KeyServer", key, str(value))
-
- with open(args.config_path, "w") as config_file:
- config.write(config_file)
-
-
-if __name__ == "__main__":
- generate_config(sys.argv[1:])
diff --git a/synapse/crypto/context_factory.py b/synapse/crypto/context_factory.py
new file mode 100644
index 0000000000..fe58d65305
--- /dev/null
+++ b/synapse/crypto/context_factory.py
@@ -0,0 +1,23 @@
+from twisted.internet import reactor, ssl
+from OpenSSL import SSL
+
+
+class ServerContextFactory(ssl.ContextFactory):
+ """Factory for PyOpenSSL SSL contexts that are used to handle incoming
+ connections and to make connections to remote servers."""
+
+ def __init__(self, config):
+ self._context = SSL.Context(SSL.SSLv23_METHOD)
+ self.configure_context(self._context, config)
+
+ @staticmethod
+ def configure_context(context, config):
+ context.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
+ context.use_certificate(config.tls_certificate)
+ context.use_privatekey(config.tls_private_key)
+ context.load_tmp_dh(config.tls_dh_params_path)
+ context.set_cipher_list("!ADH:HIGH+kEDH:!AECDH:HIGH+kEECDH")
+
+ def getContext(self):
+ return self._context
+
diff --git a/synapse/http/client.py b/synapse/http/client.py
index 36ba2c6591..093bdf0e3f 100644
--- a/synapse/http/client.py
+++ b/synapse/http/client.py
@@ -113,8 +113,9 @@ class TwistedHttpClient(HttpClient):
requests.
"""
- def __init__(self):
+ def __init__(self, hs):
self.agent = MatrixHttpAgent(reactor)
+ self.hs = hs
@defer.inlineCallbacks
def put_json(self, destination, path, data):
@@ -177,7 +178,10 @@ class TwistedHttpClient(HttpClient):
retries_left = 5
# TODO: setup and pass in an ssl_context to enable TLS
- endpoint = matrix_endpoint(reactor, destination, timeout=10)
+ endpoint = matrix_endpoint(
+ reactor, destination, timeout=10,
+ ssl_context_factory=self.hs.tls_context_factory
+ )
while True:
try:
diff --git a/synapse/http/endpoint.py b/synapse/http/endpoint.py
index d91500b07d..a6ebe23567 100644
--- a/synapse/http/endpoint.py
+++ b/synapse/http/endpoint.py
@@ -53,7 +53,7 @@ def matrix_endpoint(reactor, destination, ssl_context_factory=None,
default_port = 8080
else:
transport_endpoint = SSL4ClientEndpoint
- endpoint_kw_args.update(ssl_context_factory=ssl_context_factory)
+ endpoint_kw_args.update(sslContextFactory=ssl_context_factory)
default_port = 443
if port is None:
diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py
index cc8b59f8da..7a441391f0 100644
--- a/synapse/storage/__init__.py
+++ b/synapse/storage/__init__.py
@@ -37,6 +37,7 @@ from .roommember import RoomMemberStore
from .stream import StreamStore
from .pdu import StatePduStore, PduStore
from .transactions import TransactionStore
+from .keys import KeyStore
import json
import logging
@@ -49,7 +50,7 @@ logger = logging.getLogger(__name__)
class DataStore(RoomMemberStore, RoomStore,
RegistrationStore, StreamStore, ProfileStore, FeedbackStore,
PresenceStore, PduStore, StatePduStore, TransactionStore,
- DirectoryStore):
+ DirectoryStore, KeyStore):
def __init__(self, hs):
super(DataStore, self).__init__(hs)
diff --git a/synapse/storage/keys.py b/synapse/storage/keys.py
new file mode 100644
index 0000000000..4d19b9f641
--- /dev/null
+++ b/synapse/storage/keys.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 matrix.org
+#
+# 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.
+
+from _base import SQLBaseStore
+
+from twisted.internet import defer
+
+import OpenSSL
+import nacl.signing
+
+class KeyStore(SQLBaseStore):
+ """Persistence for signature verification keys and tls X.509 certificates
+ """
+
+ @defer.inlineCallbacks
+ def get_server_certificate(self, server_name):
+ """Retrieve the TLS X.509 certificate for the given server
+ Args:
+ server_name (bytes): The name of the server.
+ Returns:
+ (OpenSSL.crypto.X509): The tls certificate.
+ """
+ tls_certificate_bytes, = yield self._simple_select_one(
+ table="server_tls_certificates",
+ keyvalues={"server_name": server_name},
+ retcols=("tls_certificate",),
+ )
+ tls_certificate = OpenSSL.crypto.load_certificate(
+ OpenSSL.crypto.FILETYPE_ASN1, tls_certificate_bytes,
+ )
+ defer.returnValue(tls_certificate)
+
+ def store_server_certificate(self, server_name, key_server, ts_now_ms,
+ tls_certificate):
+ """Stores the TLS X.509 certificate for the given server
+ Args:
+ server_name (bytes): The name of the server.
+ key_server (bytes): Where the certificate was looked up
+ ts_now_ms (int): The time now in milliseconds
+ tls_certificate (OpenSSL.crypto.X509): The X.509 certificate.
+ """
+ tls_certificate_bytes = OpenSSL.crypto.dump_certificate(
+ OpenSSL.crypto.FILETYPE_ASN1, tls_certificate
+ )
+ return self._simple_insert(
+ table="server_tls_certificates",
+ keyvalues={
+ "server_name": server_name,
+ "key_server": key_server,
+ "ts_added_ms": ts_now_ms,
+ "tls_certificate": tls_certificate_bytes,
+ },
+ )
+
+ @defer.inlineCallbacks
+ def get_server_verification_key(self, server_name):
+ """Retrieve the NACL verification key for a given server
+ Args:
+ server_name (bytes): The name of the server.
+ Returns:
+ (nacl.signing.VerifyKey): The verification key.
+ """
+ verification_key_bytes, = yield self._simple_select_one(
+ table="server_signature_keys",
+ key_values={"server_name": server_name},
+ retcols=("tls_certificate",),
+ )
+ verification_key = nacl.signing.VerifyKey(verification_key_bytes)
+ defer.returnValue(verification_key)
+
+ def store_server_verification_key(self, server_name, key_version,
+ key_server, ts_now_ms, verification_key):
+ """Stores a NACL verification key for the given server.
+ Args:
+ server_name (bytes): The name of the server.
+ key_version (bytes): The version of the key for the server.
+ key_server (bytes): Where the verification key was looked up
+ ts_now_ms (int): The time now in milliseconds
+ verification_key (nacl.signing.VerifyKey): The NACL verify key.
+ """
+ verification_key_bytes = verification_key.encode()
+ return self._simple_insert(
+ table="server_signature_keys",
+ key_values={
+ "server_name": server_name,
+ "key_version": key_version,
+ "key_server": key_server,
+ "ts_added_ms": ts_now_ms,
+ "verification_key": verification_key_bytes,
+ },
+ )
diff --git a/synapse/storage/schema/keys.sql b/synapse/storage/schema/keys.sql
new file mode 100644
index 0000000000..45cdbcecae
--- /dev/null
+++ b/synapse/storage/schema/keys.sql
@@ -0,0 +1,30 @@
+/* Copyright 2014 matrix.org
+ *
+ * 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.
+ */
+CREATE TABLE IF NOT EXISTS server_tls_certificates(
+ server_name TEXT, -- Server name.
+ key_server TEXT, -- Which key server the certificate was fetched from.
+ ts_added_ms INTEGER, -- When the certifcate was added.
+ tls_certificate BLOB, -- DER encoded x509 certificate.
+ CONSTRAINT uniqueness UNIQUE (server_name)
+);
+
+CREATE TABLE IF NOT EXISTS server_signature_keys(
+ server_name TEXT, -- Server name.
+ key_version TEXT, -- Key version.
+ key_server TEXT, -- Which key server the key was fetched form.
+ ts_added_ms INTEGER, -- When the key was added.
+ verification_key BLOB, -- NACL verification key.
+ CONSTRAINT uniqueness UNIQUE (server_name, key_version)
+);
|