diff --git a/synapse/config/__main__.py b/synapse/config/__main__.py
index 79fe9c3dac..fca35b008c 100644
--- a/synapse/config/__main__.py
+++ b/synapse/config/__main__.py
@@ -16,7 +16,7 @@ from synapse.config._base import ConfigError
if __name__ == "__main__":
import sys
- from homeserver import HomeServerConfig
+ from synapse.config.homeserver import HomeServerConfig
action = sys.argv[1]
diff --git a/synapse/config/_base.py b/synapse/config/_base.py
index 14dae65ea0..5aec43b702 100644
--- a/synapse/config/_base.py
+++ b/synapse/config/_base.py
@@ -135,10 +135,6 @@ class Config(object):
return file_stream.read()
@staticmethod
- def default_path(name):
- return os.path.abspath(os.path.join(os.path.curdir, name))
-
- @staticmethod
def read_config_file(file_path):
with open(file_path) as file_stream:
return yaml.load(file_stream)
@@ -151,8 +147,39 @@ class Config(object):
return results
def generate_config(
- self, config_dir_path, server_name, is_generating_file, report_stats=None
+ self,
+ config_dir_path,
+ data_dir_path,
+ server_name,
+ generate_secrets=False,
+ report_stats=None,
):
+ """Build a default configuration file
+
+ This is used both when the user explicitly asks us to generate a config file
+ (eg with --generate_config), and before loading the config at runtime (to give
+ a base which the config files override)
+
+ Args:
+ config_dir_path (str): The path where the config files are kept. Used to
+ create filenames for things like the log config and the signing key.
+
+ data_dir_path (str): The path where the data files are kept. Used to create
+ filenames for things like the database and media store.
+
+ server_name (str): The server name. Used to initialise the server_name
+ config param, but also used in the names of some of the config files.
+
+ generate_secrets (bool): True if we should generate new secrets for things
+ like the macaroon_secret_key. If False, these parameters will be left
+ unset.
+
+ report_stats (bool|None): Initial setting for the report_stats setting.
+ If None, report_stats will be left unset.
+
+ Returns:
+ str: the yaml config file
+ """
default_config = "# vim:ft=yaml\n"
default_config += "\n\n".join(
@@ -160,15 +187,14 @@ class Config(object):
for conf in self.invoke_all(
"default_config",
config_dir_path=config_dir_path,
+ data_dir_path=data_dir_path,
server_name=server_name,
- is_generating_file=is_generating_file,
+ generate_secrets=generate_secrets,
report_stats=report_stats,
)
)
- config = yaml.load(default_config)
-
- return default_config, config
+ return default_config
@classmethod
def load_config(cls, description, argv):
@@ -231,7 +257,7 @@ class Config(object):
"--keys-directory",
metavar="DIRECTORY",
help="Used with 'generate-*' options to specify where files such as"
- " certs and signing keys should be stored in, unless explicitly"
+ " signing keys should be stored, unless explicitly"
" specified in the config.",
)
config_parser.add_argument(
@@ -274,27 +300,24 @@ class Config(object):
if not cls.path_exists(config_dir_path):
os.makedirs(config_dir_path)
with open(config_path, "w") as config_file:
- config_str, config = obj.generate_config(
+ config_str = obj.generate_config(
config_dir_path=config_dir_path,
+ data_dir_path=os.getcwd(),
server_name=server_name,
report_stats=(config_args.report_stats == "yes"),
- is_generating_file=True,
+ generate_secrets=True,
)
+ config = yaml.load(config_str)
obj.invoke_all("generate_files", config)
config_file.write(config_str)
print(
(
"A config file has been generated in %r for server name"
- " %r with corresponding SSL keys and self-signed"
- " certificates. Please review this file and customise it"
+ " %r. Please review this file and customise it"
" to your needs."
)
% (config_path, server_name)
)
- print(
- "If this server name is incorrect, you will need to"
- " regenerate the SSL certificates"
- )
return
else:
print(
@@ -339,7 +362,7 @@ class Config(object):
if not keys_directory:
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 = {}
for config_file in config_files:
@@ -350,11 +373,13 @@ class Config(object):
raise ConfigError(MISSING_SERVER_NAME)
server_name = specified_config["server_name"]
- _, config = self.generate_config(
- config_dir_path=config_dir_path,
+ config_string = self.generate_config(
+ config_dir_path=self.config_dir_path,
+ data_dir_path=os.getcwd(),
server_name=server_name,
- is_generating_file=False,
+ generate_secrets=False,
)
+ config = yaml.load(config_string)
config.pop("log_config")
config.update(specified_config)
diff --git a/synapse/config/api.py b/synapse/config/api.py
index 403d96ba76..e8a753f002 100644
--- a/synapse/config/api.py
+++ b/synapse/config/api.py
@@ -24,6 +24,7 @@ class ApiConfig(Config):
EventTypes.JoinRules,
EventTypes.CanonicalAlias,
EventTypes.RoomAvatar,
+ EventTypes.RoomEncryption,
EventTypes.Name,
])
@@ -32,9 +33,11 @@ class ApiConfig(Config):
## API Configuration ##
# A list of event types that will be included in the room_invite_state
+ #
room_invite_state_types:
- "{JoinRules}"
- "{CanonicalAlias}"
- "{RoomAvatar}"
+ - "{RoomEncryption}"
- "{Name}"
""".format(**vars(EventTypes))
diff --git a/synapse/config/appservice.py b/synapse/config/appservice.py
index 3b161d708a..c260d59464 100644
--- a/synapse/config/appservice.py
+++ b/synapse/config/appservice.py
@@ -33,11 +33,18 @@ class AppServiceConfig(Config):
def read_config(self, config):
self.app_service_config_files = config.get("app_service_config_files", [])
self.notify_appservices = config.get("notify_appservices", True)
+ self.track_appservice_user_ips = config.get("track_appservice_user_ips", False)
def default_config(cls, **kwargs):
return """\
# A list of application service config file to use
+ #
app_service_config_files: []
+
+ # Whether or not to track application service IP addresses. Implicitly
+ # enables MAU tracking for application service users.
+ #
+ track_appservice_user_ips: False
"""
diff --git a/synapse/config/captcha.py b/synapse/config/captcha.py
index 7ba0c2de6a..4064891ffb 100644
--- a/synapse/config/captcha.py
+++ b/synapse/config/captcha.py
@@ -30,14 +30,17 @@ class CaptchaConfig(Config):
# See docs/CAPTCHA_SETUP for full details of configuring this.
# This Home Server's ReCAPTCHA public key.
+ #
recaptcha_public_key: "YOUR_PUBLIC_KEY"
# This Home Server's ReCAPTCHA private key.
+ #
recaptcha_private_key: "YOUR_PRIVATE_KEY"
# Enables ReCaptcha checks when registering, preventing signup
# unless a captcha is answered. Requires a valid ReCaptcha
# public/private key.
+ #
enable_registration_captcha: False
# A secret key used to bypass the captcha test entirely.
diff --git a/synapse/config/cas.py b/synapse/config/cas.py
index 8109e5f95e..609c0815c8 100644
--- a/synapse/config/cas.py
+++ b/synapse/config/cas.py
@@ -38,6 +38,7 @@ class CasConfig(Config):
def default_config(self, config_dir_path, server_name, **kwargs):
return """
# Enable CAS for registration and login.
+ #
#cas_config:
# enabled: true
# server_url: "https://cas-server.com"
diff --git a/synapse/config/consent_config.py b/synapse/config/consent_config.py
index e22c731aad..abeb0180d3 100644
--- a/synapse/config/consent_config.py
+++ b/synapse/config/consent_config.py
@@ -13,6 +13,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+from os import path
+
+from synapse.config import ConfigError
+
from ._base import Config
DEFAULT_CONFIG = """\
@@ -42,18 +46,28 @@ DEFAULT_CONFIG = """\
# until the user consents to the privacy policy. The value of the setting is
# used as the text of the error.
#
-# user_consent:
-# template_dir: res/templates/privacy
-# version: 1.0
-# server_notice_content:
-# msgtype: m.text
-# body: >-
-# To continue using this homeserver you must review and agree to the
-# terms and conditions at %(consent_uri)s
-# send_server_notice_to_guests: True
-# block_events_error: >-
-# To continue using this homeserver you must review and agree to the
-# terms and conditions at %(consent_uri)s
+# 'require_at_registration', if enabled, will add a step to the registration
+# process, similar to how captcha works. Users will be required to accept the
+# policy before their account is created.
+#
+# 'policy_name' is the display name of the policy users will see when registering
+# for an account. Has no effect unless `require_at_registration` is enabled.
+# Defaults to "Privacy Policy".
+#
+#user_consent:
+# template_dir: res/templates/privacy
+# version: 1.0
+# server_notice_content:
+# msgtype: m.text
+# body: >-
+# To continue using this homeserver you must review and agree to the
+# terms and conditions at %(consent_uri)s
+# send_server_notice_to_guests: True
+# block_events_error: >-
+# To continue using this homeserver you must review and agree to the
+# terms and conditions at %(consent_uri)s
+# require_at_registration: False
+# policy_name: Privacy Policy
#
"""
@@ -67,13 +81,23 @@ class ConsentConfig(Config):
self.user_consent_server_notice_content = None
self.user_consent_server_notice_to_guests = False
self.block_events_without_consent_error = None
+ self.user_consent_at_registration = False
+ self.user_consent_policy_name = "Privacy Policy"
def read_config(self, config):
consent_config = config.get("user_consent")
if consent_config is None:
return
self.user_consent_version = str(consent_config["version"])
- self.user_consent_template_dir = consent_config["template_dir"]
+ self.user_consent_template_dir = self.abspath(
+ consent_config["template_dir"]
+ )
+ if not path.isdir(self.user_consent_template_dir):
+ raise ConfigError(
+ "Could not find template directory '%s'" % (
+ self.user_consent_template_dir,
+ ),
+ )
self.user_consent_server_notice_content = consent_config.get(
"server_notice_content",
)
@@ -83,6 +107,12 @@ class ConsentConfig(Config):
self.user_consent_server_notice_to_guests = bool(consent_config.get(
"send_server_notice_to_guests", False,
))
+ self.user_consent_at_registration = bool(consent_config.get(
+ "require_at_registration", False,
+ ))
+ self.user_consent_policy_name = consent_config.get(
+ "policy_name", "Privacy Policy",
+ )
def default_config(self, **kwargs):
return DEFAULT_CONFIG
diff --git a/synapse/config/database.py b/synapse/config/database.py
index e915d9d09b..c8890147a6 100644
--- a/synapse/config/database.py
+++ b/synapse/config/database.py
@@ -12,6 +12,7 @@
# 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 os
from ._base import Config
@@ -45,8 +46,8 @@ class DatabaseConfig(Config):
self.set_databasepath(config.get("database_path"))
- def default_config(self, **kwargs):
- database_path = self.abspath("homeserver.db")
+ def default_config(self, data_dir_path, **kwargs):
+ database_path = os.path.join(data_dir_path, "homeserver.db")
return """\
# Database configuration
database:
diff --git a/synapse/config/groups.py b/synapse/config/groups.py
index 997fa2881f..46933a904c 100644
--- a/synapse/config/groups.py
+++ b/synapse/config/groups.py
@@ -24,9 +24,11 @@ class GroupsConfig(Config):
def default_config(self, **kwargs):
return """\
# Whether to allow non server admins to create groups on this server
+ #
enable_group_creation: false
# If enabled, non server admins can only create groups with local parts
# starting with this prefix
- # group_creation_prefix: "unofficial/"
+ #
+ #group_creation_prefix: "unofficial/"
"""
diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py
index 10dd40159f..727fdc54d8 100644
--- a/synapse/config/homeserver.py
+++ b/synapse/config/homeserver.py
@@ -32,7 +32,7 @@ from .ratelimiting import RatelimitConfig
from .registration import RegistrationConfig
from .repository import ContentRepositoryConfig
from .room_directory import RoomDirectoryConfig
-from .saml2 import SAML2Config
+from .saml2_config import SAML2Config
from .server import ServerConfig
from .server_notices_config import ServerNoticesConfig
from .spam_checker import SpamCheckerConfig
@@ -42,7 +42,7 @@ from .voip import VoipConfig
from .workers import WorkerConfig
-class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
+class HomeServerConfig(ServerConfig, TlsConfig, DatabaseConfig, LoggingConfig,
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
VoipConfig, RegistrationConfig, MetricsConfig, ApiConfig,
AppServiceConfig, KeyConfig, SAML2Config, CasConfig,
@@ -53,10 +53,3 @@ class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
ServerNoticesConfig, RoomDirectoryConfig,
):
pass
-
-
-if __name__ == '__main__':
- import sys
- sys.stdout.write(
- HomeServerConfig().generate_config(sys.argv[1], sys.argv[2], True)[0]
- )
diff --git a/synapse/config/jwt_config.py b/synapse/config/jwt_config.py
index 51e7f7e003..ecb4124096 100644
--- a/synapse/config/jwt_config.py
+++ b/synapse/config/jwt_config.py
@@ -46,8 +46,8 @@ class JWTConfig(Config):
return """\
# The JWT needs to contain a globally unique "sub" (subject) claim.
#
- # jwt_config:
- # enabled: true
- # secret: "a secret"
- # algorithm: "HS256"
+ #jwt_config:
+ # enabled: true
+ # secret: "a secret"
+ # algorithm: "HS256"
"""
diff --git a/synapse/config/key.py b/synapse/config/key.py
index 279c47bb48..35f05fa974 100644
--- a/synapse/config/key.py
+++ b/synapse/config/key.py
@@ -40,7 +40,7 @@ class KeyConfig(Config):
def read_config(self, config):
self.signing_key = self.read_signing_key(config["signing_key_path"])
self.old_signing_keys = self.read_old_signing_keys(
- config["old_signing_keys"]
+ config.get("old_signing_keys", {})
)
self.key_refresh_interval = self.parse_duration(
config["key_refresh_interval"]
@@ -56,9 +56,9 @@ class KeyConfig(Config):
if not self.macaroon_secret_key:
# Unfortunately, there are people out there that don't have this
# set. Lets just be "nice" and derive one from their secret key.
- logger.warn("Config is missing missing macaroon_secret_key")
- seed = self.signing_key[0].seed
- self.macaroon_secret_key = hashlib.sha256(seed)
+ logger.warn("Config is missing macaroon_secret_key")
+ seed = bytes(self.signing_key[0])
+ self.macaroon_secret_key = hashlib.sha256(seed).digest()
self.expire_access_token = config.get("expire_access_token", False)
@@ -66,35 +66,46 @@ class KeyConfig(Config):
# falsification of values
self.form_secret = config.get("form_secret", None)
- def default_config(self, config_dir_path, server_name, is_generating_file=False,
+ def default_config(self, config_dir_path, server_name, generate_secrets=False,
**kwargs):
base_key_name = os.path.join(config_dir_path, server_name)
- if is_generating_file:
- macaroon_secret_key = random_string_with_symbols(50)
- form_secret = '"%s"' % random_string_with_symbols(50)
+ if generate_secrets:
+ macaroon_secret_key = 'macaroon_secret_key: "%s"' % (
+ random_string_with_symbols(50),
+ )
+ form_secret = 'form_secret: "%s"' % random_string_with_symbols(50)
else:
- macaroon_secret_key = None
- form_secret = 'null'
+ macaroon_secret_key = "# macaroon_secret_key: <PRIVATE STRING>"
+ form_secret = "# form_secret: <PRIVATE STRING>"
return """\
- macaroon_secret_key: "%(macaroon_secret_key)s"
+ # a secret which is used to sign access tokens. If none is specified,
+ # the registration_shared_secret is used, if one is given; otherwise,
+ # a secret key is derived from the signing key.
+ #
+ %(macaroon_secret_key)s
# Used to enable access token expiration.
+ #
expire_access_token: False
# a secret which is used to calculate HMACs for form values, to stop
- # falsification of values
- form_secret: %(form_secret)s
+ # falsification of values. Must be specified for the User Consent
+ # forms to work.
+ #
+ %(form_secret)s
## Signing Keys ##
# Path to the signing key to sign messages with
+ #
signing_key_path: "%(base_key_name)s.signing.key"
# The keys that the server used to sign messages with but won't use
# to sign new messages. E.g. it has lost its private key
- old_signing_keys: {}
+ #
+ #old_signing_keys:
# "ed25519:auto":
# # Base64 encoded public key
# key: "The public part of your old signing key."
@@ -105,9 +116,11 @@ class KeyConfig(Config):
# Used to set the valid_until_ts in /key/v2 APIs.
# Determines how quickly servers will query to check which keys
# are still valid.
+ #
key_refresh_interval: "1d" # 1 Day.
# The trusted servers to download signing keys from.
+ #
perspectives:
servers:
"matrix.org":
diff --git a/synapse/config/logger.py b/synapse/config/logger.py
index e9a936118d..f6940b65fd 100644
--- a/synapse/config/logger.py
+++ b/synapse/config/logger.py
@@ -15,7 +15,6 @@
import logging
import logging.config
import os
-import signal
import sys
from string import Template
@@ -24,6 +23,7 @@ import yaml
from twisted.logger import STDLibLogObserver, globalLogBeginner
import synapse
+from synapse.app import _base as appbase
from synapse.util.logcontext import LoggingContextFilter
from synapse.util.versionstring import get_version_string
@@ -50,6 +50,7 @@ handlers:
maxBytes: 104857600
backupCount: 10
filters: [context]
+ encoding: utf8
console:
class: logging.StreamHandler
formatter: precise
@@ -79,11 +80,10 @@ class LoggingConfig(Config):
self.log_file = self.abspath(config.get("log_file"))
def default_config(self, config_dir_path, server_name, **kwargs):
- log_config = self.abspath(
- os.path.join(config_dir_path, server_name + ".log.config")
- )
+ log_config = os.path.join(config_dir_path, server_name + ".log.config")
return """
# A yaml python logging config file
+ #
log_config: "%(log_config)s"
""" % locals()
@@ -137,6 +137,9 @@ def setup_logging(config, use_worker_options=False):
use_worker_options (bool): True to use 'worker_log_config' and
'worker_log_file' options instead of 'log_config' and 'log_file'.
+
+ register_sighup (func | None): Function to call to register a
+ sighup handler.
"""
log_config = (config.worker_log_config if use_worker_options
else config.log_config)
@@ -179,7 +182,7 @@ def setup_logging(config, use_worker_options=False):
else:
handler = logging.StreamHandler()
- def sighup(signum, stack):
+ def sighup(*args):
pass
handler.setFormatter(formatter)
@@ -192,20 +195,14 @@ def setup_logging(config, use_worker_options=False):
with open(log_config, 'r') as f:
logging.config.dictConfig(yaml.load(f))
- def sighup(signum, stack):
+ def sighup(*args):
# it might be better to use a file watcher or something for this.
load_log_config()
logging.info("Reloaded log config from %s due to SIGHUP", log_config)
load_log_config()
- # TODO(paul): obviously this is a terrible mechanism for
- # stealing SIGHUP, because it means no other part of synapse
- # can use it instead. If we want to catch SIGHUP anywhere
- # else as well, I'd suggest we find a nicer way to broadcast
- # it around.
- if getattr(signal, "SIGHUP"):
- signal.signal(signal.SIGHUP, sighup)
+ appbase.register_sighup(sighup)
# make sure that the first thing we log is a thing we can grep backwards
# for
@@ -246,3 +243,5 @@ def setup_logging(config, use_worker_options=False):
[_log],
redirectStandardIO=not config.no_redirect_stdio,
)
+ if not config.no_redirect_stdio:
+ print("Redirected stdout/stderr to logs")
diff --git a/synapse/config/metrics.py b/synapse/config/metrics.py
index 61155c99d0..ed0498c634 100644
--- a/synapse/config/metrics.py
+++ b/synapse/config/metrics.py
@@ -13,7 +13,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from ._base import Config
+from ._base import Config, ConfigError
+
+MISSING_SENTRY = (
+ """Missing sentry-sdk library. This is required to enable sentry
+ integration.
+ """
+)
class MetricsConfig(Config):
@@ -23,11 +29,43 @@ class MetricsConfig(Config):
self.metrics_port = config.get("metrics_port")
self.metrics_bind_host = config.get("metrics_bind_host", "127.0.0.1")
+ self.sentry_enabled = "sentry" in config
+ if self.sentry_enabled:
+ try:
+ import sentry_sdk # noqa F401
+ except ImportError:
+ raise ConfigError(MISSING_SENTRY)
+
+ self.sentry_dsn = config["sentry"].get("dsn")
+ if not self.sentry_dsn:
+ raise ConfigError(
+ "sentry.dsn field is required when sentry integration is enabled",
+ )
+
def default_config(self, report_stats=None, **kwargs):
- suffix = "" if report_stats is None else "report_stats: %(report_stats)s\n"
- return ("""\
+ res = """\
## Metrics ###
# Enable collection and rendering of performance metrics
+ #
enable_metrics: False
- """ + suffix) % locals()
+
+ # Enable sentry integration
+ # NOTE: While attempts are made to ensure that the logs don't contain
+ # any sensitive information, this cannot be guaranteed. By enabling
+ # this option the sentry server may therefore receive sensitive
+ # information, and it in turn may then diseminate sensitive information
+ # through insecure notification channels if so configured.
+ #
+ #sentry:
+ # dsn: "..."
+
+ # Whether or not to report anonymized homeserver usage statistics.
+ """
+
+ if report_stats is None:
+ res += "# report_stats: true|false\n"
+ else:
+ res += "report_stats: %s\n" % ('true' if report_stats else 'false')
+
+ return res
diff --git a/synapse/config/password.py b/synapse/config/password.py
index a4bd171399..2a52b9db54 100644
--- a/synapse/config/password.py
+++ b/synapse/config/password.py
@@ -28,6 +28,7 @@ class PasswordConfig(Config):
def default_config(self, config_dir_path, server_name, **kwargs):
return """
# Enable password for login.
+ #
password_config:
enabled: true
# Uncomment and change to a secret random string for extra security.
diff --git a/synapse/config/password_auth_providers.py b/synapse/config/password_auth_providers.py
index f4066abc28..f0a6be0679 100644
--- a/synapse/config/password_auth_providers.py
+++ b/synapse/config/password_auth_providers.py
@@ -52,18 +52,18 @@ class PasswordAuthProviderConfig(Config):
def default_config(self, **kwargs):
return """\
- # password_providers:
- # - module: "ldap_auth_provider.LdapAuthProvider"
- # config:
- # enabled: true
- # uri: "ldap://ldap.example.com:389"
- # start_tls: true
- # base: "ou=users,dc=example,dc=com"
- # attributes:
- # uid: "cn"
- # mail: "email"
- # name: "givenName"
- # #bind_dn:
- # #bind_password:
- # #filter: "(objectClass=posixAccount)"
+ #password_providers:
+ # - module: "ldap_auth_provider.LdapAuthProvider"
+ # config:
+ # enabled: true
+ # uri: "ldap://ldap.example.com:389"
+ # start_tls: true
+ # base: "ou=users,dc=example,dc=com"
+ # attributes:
+ # uid: "cn"
+ # mail: "email"
+ # name: "givenName"
+ # #bind_dn:
+ # #bind_password:
+ # #filter: "(objectClass=posixAccount)"
"""
diff --git a/synapse/config/push.py b/synapse/config/push.py
index b7e0d46afa..62c0060c9c 100644
--- a/synapse/config/push.py
+++ b/synapse/config/push.py
@@ -51,11 +51,11 @@ class PushConfig(Config):
# notification request includes the content of the event (other details
# like the sender are still included). For `event_id_only` push, it
# has no effect.
-
+ #
# For modern android devices the notification content will still appear
# because it is loaded by the app. iPhone, however will send a
# notification saying only that a message arrived and who it came from.
#
#push:
- # include_content: true
+ # include_content: true
"""
diff --git a/synapse/config/ratelimiting.py b/synapse/config/ratelimiting.py
index 83b22dc199..54b71e6841 100644
--- a/synapse/config/ratelimiting.py
+++ b/synapse/config/ratelimiting.py
@@ -32,27 +32,34 @@ class RatelimitConfig(Config):
## Ratelimiting ##
# Number of messages a client can send per second
+ #
rc_messages_per_second: 0.2
# Number of message a client can send before being throttled
+ #
rc_message_burst_count: 10.0
# The federation window size in milliseconds
+ #
federation_rc_window_size: 1000
# The number of federation requests from a single server in a window
# before the server will delay processing the request.
+ #
federation_rc_sleep_limit: 10
# The duration in milliseconds to delay processing events from
# remote servers by if they go over the sleep limit.
+ #
federation_rc_sleep_delay: 500
# The maximum number of concurrent federation requests allowed
# from a single server
+ #
federation_rc_reject_limit: 50
# The number of federation requests to concurrently process from a
# single server
+ #
federation_rc_concurrent: 3
"""
diff --git a/synapse/config/registration.py b/synapse/config/registration.py
index 7480ed5145..2881482f96 100644
--- a/synapse/config/registration.py
+++ b/synapse/config/registration.py
@@ -37,6 +37,7 @@ class RegistrationConfig(Config):
self.bcrypt_rounds = config.get("bcrypt_rounds", 12)
self.trusted_third_party_id_servers = config["trusted_third_party_id_servers"]
+ self.default_identity_server = config.get("default_identity_server")
self.allow_guest_access = config.get("allow_guest_access", False)
self.invite_3pid_guest = (
@@ -49,8 +50,17 @@ class RegistrationConfig(Config):
raise ConfigError('Invalid auto_join_rooms entry %s' % (room_alias,))
self.autocreate_auto_join_rooms = config.get("autocreate_auto_join_rooms", True)
- def default_config(self, **kwargs):
- registration_shared_secret = random_string_with_symbols(50)
+ self.disable_msisdn_registration = (
+ config.get("disable_msisdn_registration", False)
+ )
+
+ def default_config(self, generate_secrets=False, **kwargs):
+ if generate_secrets:
+ registration_shared_secret = 'registration_shared_secret: "%s"' % (
+ random_string_with_symbols(50),
+ )
+ else:
+ registration_shared_secret = '# registration_shared_secret: <PRIVATE STRING>'
return """\
## Registration ##
@@ -60,54 +70,75 @@ class RegistrationConfig(Config):
# The user must provide all of the below types of 3PID when registering.
#
- # registrations_require_3pid:
- # - email
- # - msisdn
+ #registrations_require_3pid:
+ # - email
+ # - msisdn
+
+ # Explicitly disable asking for MSISDNs from the registration
+ # flow (overrides registrations_require_3pid if MSISDNs are set as required)
+ #
+ #disable_msisdn_registration: True
# Mandate that users are only allowed to associate certain formats of
# 3PIDs with accounts on this server.
#
- # allowed_local_3pids:
- # - medium: email
- # pattern: ".*@matrix\\.org"
- # - medium: email
- # pattern: ".*@vector\\.im"
- # - medium: msisdn
- # pattern: "\\+44"
+ #allowed_local_3pids:
+ # - medium: email
+ # pattern: '.*@matrix\\.org'
+ # - medium: email
+ # pattern: '.*@vector\\.im'
+ # - medium: msisdn
+ # pattern: '\\+44'
# If set, allows registration by anyone who also has the shared
# secret, even if registration is otherwise disabled.
- registration_shared_secret: "%(registration_shared_secret)s"
+ #
+ %(registration_shared_secret)s
# Set the number of bcrypt rounds used to generate password hash.
# Larger numbers increase the work factor needed to generate the hash.
# The default number is 12 (which equates to 2^12 rounds).
# N.B. that increasing this will exponentially increase the time required
# to register or login - e.g. 24 => 2^24 rounds which will take >20 mins.
+ #
bcrypt_rounds: 12
# Allows users to register as guests without a password/email/etc, and
# participate in rooms hosted on this server which have been made
# accessible to anonymous users.
+ #
allow_guest_access: False
+ # The identity server which we suggest that clients should use when users log
+ # in on this server.
+ #
+ # (By default, no suggestion is made, so it is left up to the client.
+ # This setting is ignored unless public_baseurl is also set.)
+ #
+ #default_identity_server: https://matrix.org
+
# The list of identity servers trusted to verify third party
# identifiers by this server.
+ #
+ # Also defines the ID server which will be called when an account is
+ # deactivated (one will be picked arbitrarily).
+ #
trusted_third_party_id_servers:
- - matrix.org
- - vector.im
- - riot.im
+ - matrix.org
+ - vector.im
# Users who register on this homeserver will automatically be joined
# to these rooms
+ #
#auto_join_rooms:
- # - "#example:example.com"
+ # - "#example:example.com"
# Where auto_join_rooms are specified, setting this flag ensures that the
# the rooms exist by creating them when the first user on the
# homeserver registers.
# Setting to false means that if the rooms are not manually created,
# users cannot be auto-joined since they do not exist.
+ #
autocreate_auto_join_rooms: true
""" % locals()
diff --git a/synapse/config/repository.py b/synapse/config/repository.py
index 06c62ab62c..97db2a5b7a 100644
--- a/synapse/config/repository.py
+++ b/synapse/config/repository.py
@@ -12,7 +12,7 @@
# 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 os
from collections import namedtuple
from synapse.util.module_loader import load_module
@@ -175,34 +175,39 @@ class ContentRepositoryConfig(Config):
"url_preview_url_blacklist", ()
)
- def default_config(self, **kwargs):
- media_store = self.default_path("media_store")
- uploads_path = self.default_path("uploads")
+ def default_config(self, data_dir_path, **kwargs):
+ media_store = os.path.join(data_dir_path, "media_store")
+ uploads_path = os.path.join(data_dir_path, "uploads")
return r"""
# Directory where uploaded images and attachments are stored.
+ #
media_store_path: "%(media_store)s"
# Media storage providers allow media to be stored in different
# locations.
- # media_storage_providers:
- # - module: file_system
- # # Whether to write new local files.
- # store_local: false
- # # Whether to write new remote media
- # store_remote: false
- # # Whether to block upload requests waiting for write to this
- # # provider to complete
- # store_synchronous: false
- # config:
- # directory: /mnt/some/other/directory
+ #
+ #media_storage_providers:
+ # - module: file_system
+ # # Whether to write new local files.
+ # store_local: false
+ # # Whether to write new remote media
+ # store_remote: false
+ # # Whether to block upload requests waiting for write to this
+ # # provider to complete
+ # store_synchronous: false
+ # config:
+ # directory: /mnt/some/other/directory
# Directory where in-progress uploads are stored.
+ #
uploads_path: "%(uploads_path)s"
# The largest allowed upload size in bytes
+ #
max_upload_size: "10M"
# Maximum number of pixels that will be thumbnailed
+ #
max_image_pixels: "32M"
# Whether to generate new thumbnails on the fly to precisely match
@@ -210,9 +215,11 @@ class ContentRepositoryConfig(Config):
# a new resolution is requested by the client the server will
# generate a new thumbnail. If false the server will pick a thumbnail
# from a precalculated list.
+ #
dynamic_thumbnails: false
- # List of thumbnail to precalculate when an image is uploaded.
+ # List of thumbnails to precalculate when an image is uploaded.
+ #
thumbnail_sizes:
- width: 32
height: 32
@@ -233,6 +240,7 @@ class ContentRepositoryConfig(Config):
# Is the preview URL API enabled? If enabled, you *must* specify
# an explicit url_preview_ip_range_blacklist of IPs that the spider is
# denied from accessing.
+ #
url_preview_enabled: False
# List of IP address CIDR ranges that the URL preview spider is denied
@@ -243,16 +251,16 @@ class ContentRepositoryConfig(Config):
# synapse to issue arbitrary GET requests to your internal services,
# causing serious security issues.
#
- # url_preview_ip_range_blacklist:
- # - '127.0.0.0/8'
- # - '10.0.0.0/8'
- # - '172.16.0.0/12'
- # - '192.168.0.0/16'
- # - '100.64.0.0/10'
- # - '169.254.0.0/16'
- # - '::1/128'
- # - 'fe80::/64'
- # - 'fc00::/7'
+ #url_preview_ip_range_blacklist:
+ # - '127.0.0.0/8'
+ # - '10.0.0.0/8'
+ # - '172.16.0.0/12'
+ # - '192.168.0.0/16'
+ # - '100.64.0.0/10'
+ # - '169.254.0.0/16'
+ # - '::1/128'
+ # - 'fe80::/64'
+ # - 'fc00::/7'
#
# List of IP address CIDR ranges that the URL preview spider is allowed
# to access even if they are specified in url_preview_ip_range_blacklist.
@@ -260,8 +268,8 @@ class ContentRepositoryConfig(Config):
# target IP ranges - e.g. for enabling URL previews for a specific private
# website only visible in your network.
#
- # url_preview_ip_range_whitelist:
- # - '192.168.1.1'
+ #url_preview_ip_range_whitelist:
+ # - '192.168.1.1'
# Optional list of URL matches that the URL preview spider is
# denied from accessing. You should use url_preview_ip_range_blacklist
@@ -279,26 +287,25 @@ class ContentRepositoryConfig(Config):
# specified component matches for a given list item succeed, the URL is
# blacklisted.
#
- # url_preview_url_blacklist:
- # # blacklist any URL with a username in its URI
- # - username: '*'
+ #url_preview_url_blacklist:
+ # # blacklist any URL with a username in its URI
+ # - username: '*'
#
- # # blacklist all *.google.com URLs
- # - netloc: 'google.com'
- # - netloc: '*.google.com'
+ # # blacklist all *.google.com URLs
+ # - netloc: 'google.com'
+ # - netloc: '*.google.com'
#
- # # blacklist all plain HTTP URLs
- # - scheme: 'http'
+ # # blacklist all plain HTTP URLs
+ # - scheme: 'http'
#
- # # blacklist http(s)://www.acme.com/foo
- # - netloc: 'www.acme.com'
- # path: '/foo'
+ # # blacklist http(s)://www.acme.com/foo
+ # - netloc: 'www.acme.com'
+ # path: '/foo'
#
- # # blacklist any URL with a literal IPv4 address
- # - netloc: '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'
+ # # blacklist any URL with a literal IPv4 address
+ # - netloc: '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'
# The largest allowed URL preview spidering size in bytes
max_spider_size: "10M"
-
""" % locals()
diff --git a/synapse/config/room_directory.py b/synapse/config/room_directory.py
index 08d1d26e58..3322cf2eea 100644
--- a/synapse/config/room_directory.py
+++ b/synapse/config/room_directory.py
@@ -20,12 +20,37 @@ from ._base import Config, ConfigError
class RoomDirectoryConfig(Config):
def read_config(self, config):
- alias_creation_rules = config["alias_creation_rules"]
+ alias_creation_rules = config.get("alias_creation_rules")
- self._alias_creation_rules = [
- _AliasRule(rule)
- for rule in alias_creation_rules
- ]
+ if alias_creation_rules is not None:
+ self._alias_creation_rules = [
+ _RoomDirectoryRule("alias_creation_rules", rule)
+ for rule in alias_creation_rules
+ ]
+ else:
+ self._alias_creation_rules = [
+ _RoomDirectoryRule(
+ "alias_creation_rules", {
+ "action": "allow",
+ }
+ )
+ ]
+
+ room_list_publication_rules = config.get("room_list_publication_rules")
+
+ if room_list_publication_rules is not None:
+ self._room_list_publication_rules = [
+ _RoomDirectoryRule("room_list_publication_rules", rule)
+ for rule in room_list_publication_rules
+ ]
+ else:
+ self._room_list_publication_rules = [
+ _RoomDirectoryRule(
+ "room_list_publication_rules", {
+ "action": "allow",
+ }
+ )
+ ]
self.allow_non_federated_in_public_rooms = config.get(
"allow_non_federated_in_public_rooms", True,
@@ -37,67 +62,146 @@ class RoomDirectoryConfig(Config):
# on this server.
#
# The format of this option is a list of rules that contain globs that
- # match against user_id and the new alias (fully qualified with server
- # name). The action in the first rule that matches is taken, which can
- # currently either be "allow" or "deny".
+ # match against user_id, room_id and the new alias (fully qualified with
+ # server name). The action in the first rule that matches is taken,
+ # which can currently either be "allow" or "deny".
+ #
+ # Missing user_id/room_id/alias fields default to "*".
+ #
+ # If no rules match the request is denied. An empty list means no one
+ # can create aliases.
+ #
+ # Options for the rules include:
+ #
+ # user_id: Matches against the creator of the alias
+ # alias: Matches against the alias being created
+ # room_id: Matches against the room ID the alias is being pointed at
+ # action: Whether to "allow" or "deny" the request if the rule matches
+ #
+ # The default is:
+ #
+ #alias_creation_rules:
+ # - user_id: "*"
+ # alias: "*"
+ # room_id: "*"
+ # action: allow
+
+ # The `room_list_publication_rules` option controls who can publish and
+ # which rooms can be published in the public room list.
+ #
+ # The format of this option is the same as that for
+ # `alias_creation_rules`.
+ #
+ # If the room has one or more aliases associated with it, only one of
+ # the aliases needs to match the alias rule. If there are no aliases
+ # then only rules with `alias: *` match.
#
- # If no rules match the request is denied.
- alias_creation_rules:
- - user_id: "*"
- alias: "*"
- action: allow
+ # If no rules match the request is denied. An empty list means no one
+ # can publish rooms.
+ #
+ # Options for the rules include:
+ #
+ # user_id: Matches agaisnt the creator of the alias
+ # room_id: Matches against the room ID being published
+ # alias: Matches against any current local or canonical aliases
+ # associated with the room
+ # action: Whether to "allow" or "deny" the request if the rule matches
+ #
+ # The default is:
+ #
+ #room_list_publication_rules:
+ # - user_id: "*"
+ # alias: "*"
+ # room_id: "*"
+ # action: allow
# Specify whether rooms that only allow local users to join should be
# shown in the federation public room directory.
- #
+ #
# Note that this does not affect the room directory shown to users on
# this homeserver, only those on other homeservers.
+ #
#allow_non_federated_in_public_rooms: True
"""
- def is_alias_creation_allowed(self, user_id, alias):
+ def is_alias_creation_allowed(self, user_id, room_id, alias):
"""Checks if the given user is allowed to create the given alias
Args:
user_id (str)
+ room_id (str)
alias (str)
Returns:
boolean: True if user is allowed to crate the alias
"""
for rule in self._alias_creation_rules:
- if rule.matches(user_id, alias):
+ if rule.matches(user_id, room_id, [alias]):
+ return rule.action == "allow"
+
+ return False
+
+ def is_publishing_room_allowed(self, user_id, room_id, aliases):
+ """Checks if the given user is allowed to publish the room
+
+ Args:
+ user_id (str)
+ room_id (str)
+ aliases (list[str]): any local aliases associated with the room
+
+ Returns:
+ boolean: True if user can publish room
+ """
+ for rule in self._room_list_publication_rules:
+ if rule.matches(user_id, room_id, aliases):
return rule.action == "allow"
return False
-class _AliasRule(object):
- def __init__(self, rule):
+class _RoomDirectoryRule(object):
+ """Helper class to test whether a room directory action is allowed, like
+ creating an alias or publishing a room.
+ """
+
+ def __init__(self, option_name, rule):
+ """
+ Args:
+ option_name (str): Name of the config option this rule belongs to
+ rule (dict): The rule as specified in the config
+ """
+
action = rule["action"]
- user_id = rule["user_id"]
- alias = rule["alias"]
+ user_id = rule.get("user_id", "*")
+ room_id = rule.get("room_id", "*")
+ alias = rule.get("alias", "*")
if action in ("allow", "deny"):
self.action = action
else:
raise ConfigError(
- "alias_creation_rules rules can only have action of 'allow'"
- " or 'deny'"
+ "%s rules can only have action of 'allow'"
+ " or 'deny'" % (option_name,)
)
+ self._alias_matches_all = alias == "*"
+
try:
self._user_id_regex = glob_to_regex(user_id)
self._alias_regex = glob_to_regex(alias)
+ self._room_id_regex = glob_to_regex(room_id)
except Exception as e:
raise ConfigError("Failed to parse glob into regex: %s", e)
- def matches(self, user_id, alias):
- """Tests if this rule matches the given user_id and alias.
+ def matches(self, user_id, room_id, aliases):
+ """Tests if this rule matches the given user_id, room_id and aliases.
Args:
user_id (str)
- alias (str)
+ room_id (str)
+ aliases (list[str]): The associated aliases to the room. Will be a
+ single element for testing alias creation, and can be empty for
+ testing room publishing.
Returns:
boolean
@@ -107,7 +211,22 @@ class _AliasRule(object):
if not self._user_id_regex.match(user_id):
return False
- if not self._alias_regex.match(alias):
+ if not self._room_id_regex.match(room_id):
return False
- return True
+ # We only have alias checks left, so we can short circuit if the alias
+ # rule matches everything.
+ if self._alias_matches_all:
+ return True
+
+ # If we are not given any aliases then this rule only matches if the
+ # alias glob matches all aliases, which we checked above.
+ if not aliases:
+ return False
+
+ # Otherwise, we just need one alias to match
+ for alias in aliases:
+ if self._alias_regex.match(alias):
+ return True
+
+ return False
diff --git a/synapse/config/saml2.py b/synapse/config/saml2.py
deleted file mode 100644
index 8d7f443021..0000000000
--- a/synapse/config/saml2.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2015 Ericsson
-#
-# 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
-
-
-class SAML2Config(Config):
- """SAML2 Configuration
- Synapse uses pysaml2 libraries for providing SAML2 support
-
- config_path: Path to the sp_conf.py configuration file
- idp_redirect_url: Identity provider URL which will redirect
- the user back to /login/saml2 with proper info.
-
- sp_conf.py file is something like:
- https://github.com/rohe/pysaml2/blob/master/example/sp-repoze/sp_conf.py.example
-
- More information: https://pythonhosted.org/pysaml2/howto/config.html
- """
-
- def read_config(self, config):
- saml2_config = config.get("saml2_config", None)
- if saml2_config:
- self.saml2_enabled = saml2_config.get("enabled", True)
- self.saml2_config_path = saml2_config["config_path"]
- self.saml2_idp_redirect_url = saml2_config["idp_redirect_url"]
- else:
- self.saml2_enabled = False
- self.saml2_config_path = None
- self.saml2_idp_redirect_url = None
-
- def default_config(self, config_dir_path, server_name, **kwargs):
- return """
- # Enable SAML2 for registration and login. Uses pysaml2
- # config_path: Path to the sp_conf.py configuration file
- # idp_redirect_url: Identity provider URL which will redirect
- # the user back to /login/saml2 with proper info.
- # See pysaml2 docs for format of config.
- #saml2_config:
- # enabled: true
- # config_path: "%s/sp_conf.py"
- # idp_redirect_url: "http://%s/idp"
- """ % (config_dir_path, server_name)
diff --git a/synapse/config/saml2_config.py b/synapse/config/saml2_config.py
new file mode 100644
index 0000000000..aff0a1f00c
--- /dev/null
+++ b/synapse/config/saml2_config.py
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+# Copyright 2018 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.
+
+from ._base import Config, ConfigError
+
+
+class SAML2Config(Config):
+ def read_config(self, config):
+ self.saml2_enabled = False
+
+ saml2_config = config.get("saml2_config")
+
+ if not saml2_config or not saml2_config.get("enabled", True):
+ return
+
+ self.saml2_enabled = True
+
+ import saml2.config
+ self.saml2_sp_config = saml2.config.SPConfig()
+ self.saml2_sp_config.load(self._default_saml_config_dict())
+ self.saml2_sp_config.load(saml2_config.get("sp_config", {}))
+
+ config_path = saml2_config.get("config_path", None)
+ if config_path is not None:
+ self.saml2_sp_config.load_file(config_path)
+
+ def _default_saml_config_dict(self):
+ import saml2
+
+ public_baseurl = self.public_baseurl
+ if public_baseurl is None:
+ raise ConfigError(
+ "saml2_config requires a public_baseurl to be set"
+ )
+
+ metadata_url = public_baseurl + "_matrix/saml2/metadata.xml"
+ response_url = public_baseurl + "_matrix/saml2/authn_response"
+ return {
+ "entityid": metadata_url,
+
+ "service": {
+ "sp": {
+ "endpoints": {
+ "assertion_consumer_service": [
+ (response_url, saml2.BINDING_HTTP_POST),
+ ],
+ },
+ "required_attributes": ["uid"],
+ "optional_attributes": ["mail", "surname", "givenname"],
+ },
+ }
+ }
+
+ def default_config(self, config_dir_path, server_name, **kwargs):
+ return """
+ # Enable SAML2 for registration and login. Uses pysaml2.
+ #
+ # `sp_config` is the configuration for the pysaml2 Service Provider.
+ # See pysaml2 docs for format of config.
+ #
+ # Default values will be used for the 'entityid' and 'service' settings,
+ # so it is not normally necessary to specify them unless you need to
+ # override them.
+ #
+ #saml2_config:
+ # sp_config:
+ # # point this to the IdP's metadata. You can use either a local file or
+ # # (preferably) a URL.
+ # metadata:
+ # #local: ["saml2/idp.xml"]
+ # remote:
+ # - url: https://our_idp/metadata.xml
+ #
+ # # The rest of sp_config is just used to generate our metadata xml, and you
+ # # may well not need it, depending on your setup. Alternatively you
+ # # may need a whole lot more detail - see the pysaml2 docs!
+ #
+ # description: ["My awesome SP", "en"]
+ # name: ["Test SP", "en"]
+ #
+ # organization:
+ # name: Example com
+ # display_name:
+ # - ["Example co", "en"]
+ # url: "http://example.com"
+ #
+ # contact_person:
+ # - given_name: Bob
+ # sur_name: "the Sysadmin"
+ # email_address": ["admin@example.com"]
+ # contact_type": technical
+ #
+ # # Instead of putting the config inline as above, you can specify a
+ # # separate pysaml2 configuration file:
+ # #
+ # config_path: "%(config_dir_path)s/sp_conf.py"
+ """ % {"config_dir_path": config_dir_path}
diff --git a/synapse/config/server.py b/synapse/config/server.py
index c1c7c0105e..4200f10da3 100644
--- a/synapse/config/server.py
+++ b/synapse/config/server.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2014-2016 OpenMarket Ltd
-# Copyright 2017 New Vector Ltd
+# Copyright 2017-2018 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.
@@ -15,13 +15,23 @@
# limitations under the License.
import logging
+import os.path
from synapse.http.endpoint import parse_and_validate_server_name
+from synapse.python_dependencies import DependencyException, check_requirements
from ._base import Config, ConfigError
logger = logging.Logger(__name__)
+# by default, we attempt to listen on both '::' *and* '0.0.0.0' because some OSes
+# (Windows, macOS, other BSD/Linux where net.ipv6.bindv6only is set) will only listen
+# on IPv6 when '::' is set.
+#
+# We later check for errors when binding to 0.0.0.0 and ignore them if :: is also in
+# in the list.
+DEFAULT_BIND_ADDRESSES = ['::', '0.0.0.0']
+
class ServerConfig(Config):
@@ -34,7 +44,6 @@ class ServerConfig(Config):
raise ConfigError(str(e))
self.pid_file = self.abspath(config.get("pid_file"))
- self.web_client = config["web_client"]
self.web_client_location = config.get("web_client_location", None)
self.soft_file_limit = config["soft_file_limit"]
self.daemonize = config.get("daemonize")
@@ -62,6 +71,11 @@ class ServerConfig(Config):
# master, potentially causing inconsistency.
self.enable_media_repo = config.get("enable_media_repo", True)
+ # whether to enable search. If disabled, new entries will not be inserted
+ # into the search tables and they will not be indexed. Users will receive
+ # errors when attempting to search for messages.
+ self.enable_search = config.get("enable_search", True)
+
self.filter_timeline_limit = config.get("filter_timeline_limit", -1)
# Whether we should block invites sent to users on this server
@@ -77,6 +91,7 @@ class ServerConfig(Config):
self.max_mau_value = config.get(
"max_mau_value", 0,
)
+ self.mau_stats_only = config.get("mau_stats_only", False)
self.mau_limits_reserved_threepids = config.get(
"mau_limit_reserved_threepids", []
@@ -111,27 +126,53 @@ class ServerConfig(Config):
self.public_baseurl += '/'
self.start_pushers = config.get("start_pushers", True)
- self.listeners = config.get("listeners", [])
+ self.listeners = []
+ for listener in config.get("listeners", []):
+ if not isinstance(listener.get("port", None), int):
+ raise ConfigError(
+ "Listener configuration is lacking a valid 'port' option"
+ )
+
+ if listener.setdefault("tls", False):
+ # no_tls is not really supported any more, but let's grandfather it in
+ # here.
+ if config.get("no_tls", False):
+ logger.info(
+ "Ignoring TLS-enabled listener on port %i due to no_tls"
+ )
+ continue
- for listener in self.listeners:
bind_address = listener.pop("bind_address", None)
bind_addresses = listener.setdefault("bind_addresses", [])
+ # if bind_address was specified, add it to the list of addresses
if bind_address:
bind_addresses.append(bind_address)
- elif not bind_addresses:
- bind_addresses.append('')
+
+ # if we still have an empty list of addresses, use the default list
+ if not bind_addresses:
+ if listener['type'] == 'metrics':
+ # the metrics listener doesn't support IPv6
+ bind_addresses.append('0.0.0.0')
+ else:
+ bind_addresses.extend(DEFAULT_BIND_ADDRESSES)
+
+ self.listeners.append(listener)
+
+ if not self.web_client_location:
+ _warn_if_webclient_configured(self.listeners)
self.gc_thresholds = read_gc_thresholds(config.get("gc_thresholds", None))
bind_port = config.get("bind_port")
if bind_port:
+ if config.get("no_tls", False):
+ raise ConfigError("no_tls is incompatible with bind_port")
+
self.listeners = []
bind_host = config.get("bind_host", "")
gzip_responses = config.get("gzip_responses", True)
- names = ["client", "webclient"] if self.web_client else ["client"]
-
self.listeners.append({
"port": bind_port,
"bind_addresses": [bind_host],
@@ -139,7 +180,7 @@ class ServerConfig(Config):
"type": "http",
"resources": [
{
- "names": names,
+ "names": ["client"],
"compress": gzip_responses,
},
{
@@ -158,7 +199,7 @@ class ServerConfig(Config):
"type": "http",
"resources": [
{
- "names": names,
+ "names": ["client"],
"compress": gzip_responses,
},
{
@@ -174,6 +215,7 @@ class ServerConfig(Config):
"port": manhole,
"bind_addresses": ["127.0.0.1"],
"type": "manhole",
+ "tls": False,
})
metrics_port = config.get("metrics_port")
@@ -197,7 +239,12 @@ class ServerConfig(Config):
]
})
- def default_config(self, server_name, **kwargs):
+ _check_resource_config(self.listeners)
+
+ def has_tls_listener(self):
+ return any(l["tls"] for l in self.listeners)
+
+ def default_config(self, server_name, data_dir_path, **kwargs):
_, bind_port = parse_and_validate_server_name(server_name)
if bind_port is not None:
unsecure_port = bind_port - 400
@@ -205,7 +252,7 @@ class ServerConfig(Config):
bind_port = 8448
unsecure_port = 8008
- pid_file = self.abspath("homeserver.pid")
+ pid_file = os.path.join(data_dir_path, "homeserver.pid")
return """\
## Server ##
@@ -239,19 +286,20 @@ class ServerConfig(Config):
#
# This setting requires the affinity package to be installed!
#
- # cpu_affinity: 0xFFFFFFFF
-
- # Whether to serve a web client from the HTTP/HTTPS root resource.
- web_client: True
+ #cpu_affinity: 0xFFFFFFFF
- # The root directory to server for the above web client.
- # If left undefined, synapse will serve the matrix-angular-sdk web client.
- # Make sure matrix-angular-sdk is installed with pip if web_client is True
- # and web_client_location is undefined
- # web_client_location: "/path/to/web/root"
+ # The path to the web client which will be served at /_matrix/client/
+ # if 'webclient' is configured under the 'listeners' configuration.
+ #
+ #web_client_location: "/path/to/web/root"
- # The public-facing base URL for the client API (not including _matrix/...)
- # public_baseurl: https://example.com:8448/
+ # The public-facing base URL that clients use to access this HS
+ # (not including _matrix/...). This is the same URL a user would
+ # enter into the 'custom HS URL' field on their client. If you
+ # use synapse with a reverse proxy, this should be the URL to reach
+ # synapse via the proxy.
+ #
+ #public_baseurl: https://example.com/
# Set the soft limit on the number of file descriptors synapse can use
# Zero is used to indicate synapse should set the soft limit to the
@@ -262,15 +310,25 @@ class ServerConfig(Config):
use_presence: true
# The GC threshold parameters to pass to `gc.set_threshold`, if defined
- # gc_thresholds: [700, 10, 10]
+ #
+ #gc_thresholds: [700, 10, 10]
# Set the limit on the returned events in the timeline in the get
# and sync operations. The default value is -1, means no upper limit.
- # filter_timeline_limit: 5000
+ #
+ #filter_timeline_limit: 5000
# Whether room invites to users on this server should be blocked
# (except those sent by local server admins). The default is False.
- # block_non_admin_invites: True
+ #
+ #block_non_admin_invites: True
+
+ # Room searching
+ #
+ # If disabled, new messages will not be indexed for searching and users
+ # will receive errors when searching for messages. Defaults to enabled.
+ #
+ #enable_search: false
# Restrict federation to the following whitelist of domains.
# N.B. we recommend also firewalling your federation listener to limit
@@ -278,107 +336,145 @@ class ServerConfig(Config):
# purely on this application-layer restriction. If not specified, the
# default is to whitelist everything.
#
- # federation_domain_whitelist:
+ #federation_domain_whitelist:
# - lon.example.com
# - nyc.example.com
# - syd.example.com
# List of ports that Synapse should listen on, their purpose and their
# configuration.
+ #
+ # Options for each listener include:
+ #
+ # port: the TCP port to bind to
+ #
+ # bind_addresses: a list of local addresses to listen on. The default is
+ # 'all local interfaces'.
+ #
+ # type: the type of listener. Normally 'http', but other valid options are:
+ # 'manhole' (see docs/manhole.md),
+ # 'metrics' (see docs/metrics-howto.rst),
+ # 'replication' (see docs/workers.rst).
+ #
+ # tls: set to true to enable TLS for this listener. Will use the TLS
+ # key/cert specified in tls_private_key_path / tls_certificate_path.
+ #
+ # x_forwarded: Only valid for an 'http' listener. Set to true to use the
+ # X-Forwarded-For header as the client IP. Useful when Synapse is
+ # behind a reverse-proxy.
+ #
+ # resources: Only valid for an 'http' listener. A list of resources to host
+ # on this port. Options for each resource are:
+ #
+ # names: a list of names of HTTP resources. See below for a list of
+ # valid resource names.
+ #
+ # compress: set to true to enable HTTP comression for this resource.
+ #
+ # additional_resources: Only valid for an 'http' listener. A map of
+ # additional endpoints which should be loaded via dynamic modules.
+ #
+ # Valid resource names are:
+ #
+ # client: the client-server API (/_matrix/client). Also implies 'media' and
+ # 'static'.
+ #
+ # consent: user consent forms (/_matrix/consent). See
+ # docs/consent_tracking.md.
+ #
+ # federation: the server-server API (/_matrix/federation). Also implies
+ # 'media', 'keys', 'openid'
+ #
+ # keys: the key discovery API (/_matrix/keys).
+ #
+ # media: the media API (/_matrix/media).
+ #
+ # metrics: the metrics interface. See docs/metrics-howto.rst.
+ #
+ # openid: OpenID authentication.
+ #
+ # replication: the HTTP replication API (/_synapse/replication). See
+ # docs/workers.rst.
+ #
+ # static: static resources under synapse/static (/_matrix/static). (Mostly
+ # useful for 'fallback authentication'.)
+ #
+ # webclient: A web client. Requires web_client_location to be set.
+ #
listeners:
- # Main HTTPS listener
- # For when matrix traffic is sent directly to synapse.
- -
- # The port to listen for HTTPS requests on.
- port: %(bind_port)s
-
- # Local addresses to listen on.
- # On Linux and Mac OS, `::` will listen on all IPv4 and IPv6
- # addresses by default. For most other OSes, this will only listen
- # on IPv6.
- bind_addresses:
- - '::'
- - '0.0.0.0'
-
- # This is a 'http' listener, allows us to specify 'resources'.
- type: http
-
- tls: true
-
- # Use the X-Forwarded-For (XFF) header as the client IP and not the
- # actual client IP.
- x_forwarded: false
-
- # List of HTTP resources to serve on this listener.
- resources:
- -
- # List of resources to host on this listener.
- names:
- - client # The client-server APIs, both v1 and v2
- - webclient # The bundled webclient.
-
- # Should synapse compress HTTP responses to clients that support it?
- # This should be disabled if running synapse behind a load balancer
- # that can do automatic compression.
- compress: true
-
- - names: [federation] # Federation APIs
- compress: false
-
- # optional list of additional endpoints which can be loaded via
- # dynamic modules
- # additional_resources:
- # "/_matrix/my/custom/endpoint":
- # module: my_module.CustomRequestHandler
- # config: {}
-
- # Unsecure HTTP listener,
- # For when matrix traffic passes through loadbalancer that unwraps TLS.
+ # TLS-enabled listener: for when matrix traffic is sent directly to synapse.
+ #
+ # Disabled by default. To enable it, uncomment the following. (Note that you
+ # will also need to give Synapse a TLS key and certificate: see the TLS section
+ # below.)
+ #
+ #- port: %(bind_port)s
+ # type: http
+ # tls: true
+ # resources:
+ # - names: [client, federation]
+
+ # Unsecure HTTP listener: for when matrix traffic passes through a reverse proxy
+ # that unwraps TLS.
+ #
+ # If you plan to use a reverse proxy, please see
+ # https://github.com/matrix-org/synapse/blob/master/docs/reverse_proxy.rst.
+ #
- port: %(unsecure_port)s
tls: false
- bind_addresses: ['::', '0.0.0.0']
+ bind_addresses: ['::1', '127.0.0.1']
type: http
-
- x_forwarded: false
+ x_forwarded: true
resources:
- - names: [client, webclient]
- compress: true
- - names: [federation]
+ - names: [client, federation]
compress: false
+ # example additonal_resources:
+ #
+ #additional_resources:
+ # "/_matrix/my/custom/endpoint":
+ # module: my_module.CustomRequestHandler
+ # config: {}
+
# Turn on the twisted ssh manhole service on localhost on the given
# port.
- # - port: 9000
- # bind_addresses: ['::1', '127.0.0.1']
- # type: manhole
+ #
+ #- port: 9000
+ # bind_addresses: ['::1', '127.0.0.1']
+ # type: manhole
- # Homeserver blocking
- #
- # How to reach the server admin, used in ResourceLimitError
- # admin_contact: 'mailto:admin@server.com'
- #
- # Global block config
- #
- # hs_disabled: False
- # hs_disabled_message: 'Human readable reason for why the HS is blocked'
- # hs_disabled_limit_type: 'error code(str), to help clients decode reason'
- #
- # Monthly Active User Blocking
- #
- # Enables monthly active user checking
- # limit_usage_by_mau: False
- # max_mau_value: 50
- # mau_trial_days: 2
- #
- # Sometimes the server admin will want to ensure certain accounts are
- # never blocked by mau checking. These accounts are specified here.
- #
- # mau_limit_reserved_threepids:
- # - medium: 'email'
- # address: 'reserved_user@example.com'
+ ## Homeserver blocking ##
+ # How to reach the server admin, used in ResourceLimitError
+ #
+ #admin_contact: 'mailto:admin@server.com'
+
+ # Global blocking
+ #
+ #hs_disabled: False
+ #hs_disabled_message: 'Human readable reason for why the HS is blocked'
+ #hs_disabled_limit_type: 'error code(str), to help clients decode reason'
+
+ # Monthly Active User Blocking
+ #
+ #limit_usage_by_mau: False
+ #max_mau_value: 50
+ #mau_trial_days: 2
+
+ # If enabled, the metrics for the number of monthly active users will
+ # be populated, however no one will be limited. If limit_usage_by_mau
+ # is true, this is implied to be true.
+ #
+ #mau_stats_only: False
+
+ # Sometimes the server admin will want to ensure certain accounts are
+ # never blocked by mau checking. These accounts are specified here.
+ #
+ #mau_limit_reserved_threepids:
+ # - medium: 'email'
+ # address: 'reserved_user@example.com'
""" % locals()
def read_arguments(self, args):
@@ -404,19 +500,18 @@ class ServerConfig(Config):
" service on the given port.")
-def is_threepid_reserved(config, threepid):
+def is_threepid_reserved(reserved_threepids, threepid):
"""Check the threepid against the reserved threepid config
Args:
- config(ServerConfig) - to access server config attributes
+ reserved_threepids([dict]) - list of reserved threepids
threepid(dict) - The threepid to test for
Returns:
boolean Is the threepid undertest reserved_user
"""
- for tp in config.mau_limits_reserved_threepids:
- if (threepid['medium'] == tp['medium']
- and threepid['address'] == tp['address']):
+ for tp in reserved_threepids:
+ if (threepid['medium'] == tp['medium'] and threepid['address'] == tp['address']):
return True
return False
@@ -436,3 +531,53 @@ def read_gc_thresholds(thresholds):
raise ConfigError(
"Value of `gc_threshold` must be a list of three integers if set"
)
+
+
+NO_MORE_WEB_CLIENT_WARNING = """
+Synapse no longer includes a web client. To enable a web client, configure
+web_client_location. To remove this warning, remove 'webclient' from the 'listeners'
+configuration.
+"""
+
+
+def _warn_if_webclient_configured(listeners):
+ for listener in listeners:
+ for res in listener.get("resources", []):
+ for name in res.get("names", []):
+ if name == 'webclient':
+ logger.warning(NO_MORE_WEB_CLIENT_WARNING)
+ return
+
+
+KNOWN_RESOURCES = (
+ 'client',
+ 'consent',
+ 'federation',
+ 'keys',
+ 'media',
+ 'metrics',
+ 'openid',
+ 'replication',
+ 'static',
+ 'webclient',
+)
+
+
+def _check_resource_config(listeners):
+ resource_names = set(
+ res_name
+ for listener in listeners
+ for res in listener.get("resources", [])
+ for res_name in res.get("names", [])
+ )
+
+ for resource in resource_names:
+ if resource not in KNOWN_RESOURCES:
+ raise ConfigError(
+ "Unknown listener resource '%s'" % (resource, )
+ )
+ if resource == "consent":
+ try:
+ check_requirements('resources.consent')
+ except DependencyException as e:
+ raise ConfigError(e.message)
diff --git a/synapse/config/server_notices_config.py b/synapse/config/server_notices_config.py
index 3c39850ac6..529dc0a617 100644
--- a/synapse/config/server_notices_config.py
+++ b/synapse/config/server_notices_config.py
@@ -30,11 +30,11 @@ DEFAULT_CONFIG = """\
# It's also possible to override the room name, the display name of the
# "notices" user, and the avatar for the user.
#
-# server_notices:
-# system_mxid_localpart: notices
-# system_mxid_display_name: "Server Notices"
-# system_mxid_avatar_url: "mxc://server.com/oumMVlgDnLYFaPVkExemNVVZ"
-# room_name: "Server Notices"
+#server_notices:
+# system_mxid_localpart: notices
+# system_mxid_display_name: "Server Notices"
+# system_mxid_avatar_url: "mxc://server.com/oumMVlgDnLYFaPVkExemNVVZ"
+# room_name: "Server Notices"
"""
diff --git a/synapse/config/spam_checker.py b/synapse/config/spam_checker.py
index 3fec42bdb0..1502e9faba 100644
--- a/synapse/config/spam_checker.py
+++ b/synapse/config/spam_checker.py
@@ -28,8 +28,8 @@ class SpamCheckerConfig(Config):
def default_config(self, **kwargs):
return """\
- # spam_checker:
- # module: "my_custom_project.SuperSpamChecker"
- # config:
- # example_option: 'things'
+ #spam_checker:
+ # module: "my_custom_project.SuperSpamChecker"
+ # config:
+ # example_option: 'things'
"""
diff --git a/synapse/config/tls.py b/synapse/config/tls.py
index fef1ea99cb..8d5d287357 100644
--- a/synapse/config/tls.py
+++ b/synapse/config/tls.py
@@ -13,51 +13,58 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import logging
import os
-import subprocess
+import warnings
+from datetime import datetime
from hashlib import sha256
from unpaddedbase64 import encode_base64
from OpenSSL import crypto
-from ._base import Config
+from synapse.config._base import Config, ConfigError
-GENERATE_DH_PARAMS = False
+logger = logging.getLogger(__name__)
class TlsConfig(Config):
def read_config(self, config):
- self.tls_certificate = self.read_tls_certificate(
- config.get("tls_certificate_path")
+
+ acme_config = config.get("acme", None)
+ if acme_config is None:
+ acme_config = {}
+
+ self.acme_enabled = acme_config.get("enabled", False)
+ self.acme_url = acme_config.get(
+ "url", u"https://acme-v01.api.letsencrypt.org/directory"
)
- self.tls_certificate_file = config.get("tls_certificate_path")
+ self.acme_port = acme_config.get("port", 80)
+ self.acme_bind_addresses = acme_config.get("bind_addresses", ['::', '0.0.0.0'])
+ self.acme_reprovision_threshold = acme_config.get("reprovision_threshold", 30)
+ self.acme_domain = acme_config.get("domain", config.get("server_name"))
- self.no_tls = config.get("no_tls", False)
+ self.tls_certificate_file = self.abspath(config.get("tls_certificate_path"))
+ self.tls_private_key_file = self.abspath(config.get("tls_private_key_path"))
- 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")
- )
+ if self.has_tls_listener():
+ if not self.tls_certificate_file:
+ raise ConfigError(
+ "tls_certificate_path must be specified if TLS-enabled listeners are "
+ "configured."
+ )
+ if not self.tls_private_key_file:
+ raise ConfigError(
+ "tls_certificate_path must be specified if TLS-enabled listeners are "
+ "configured."
+ )
- self.tls_dh_params_path = self.check_file(
- config.get("tls_dh_params_path"), "tls_dh_params"
- )
+ self._original_tls_fingerprints = config.get("tls_fingerprints", [])
- self.tls_fingerprints = config["tls_fingerprints"]
+ if self._original_tls_fingerprints is None:
+ 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})
+ self.tls_fingerprints = list(self._original_tls_fingerprints)
# This config option applies to non-federation HTTP clients
# (e.g. for talking to recaptcha, identity servers, and such)
@@ -67,29 +74,176 @@ class TlsConfig(Config):
"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, allow_self_signed=True):
+ """
+ Is the certificate we have on disk valid, and if so, for how long?
+
+ Args:
+ allow_self_signed (bool): Should we allow the certificate we
+ read to be self signed?
+
+ 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
+
+ if not allow_self_signed:
+ if tls_certificate.get_subject() == tls_certificate.get_issuer():
+ raise ValueError(
+ "TLS Certificate is self signed, and this is not permitted"
+ )
+
+ # 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, require_cert_and_key):
+ """
+ Read the certificates and private key from disk.
+
+ Args:
+ require_cert_and_key (bool): set to True to throw an error if the certificate
+ and key file are not given
+ """
+ if require_cert_and_key:
+ self.tls_private_key = self.read_tls_private_key()
+ self.tls_certificate = self.read_tls_certificate()
+ elif self.tls_certificate_file:
+ # we only need the certificate for the tls_fingerprints. Reload it if we
+ # can, but it's not a fatal error if we can't.
+ try:
+ self.tls_certificate = self.read_tls_certificate()
+ except Exception as e:
+ logger.info(
+ "Unable to read TLS certificate (%s). Ignoring as no "
+ "tls listeners enabled.", e,
+ )
+
+ self.tls_fingerprints = list(self._original_tls_fingerprints)
+
+ if self.tls_certificate:
+ # 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):
base_key_name = os.path.join(config_dir_path, server_name)
tls_certificate_path = base_key_name + ".tls.crt"
tls_private_key_path = base_key_name + ".tls.key"
- tls_dh_params_path = base_key_name + ".tls.dh"
- return """\
- # PEM encoded X509 certificate for TLS.
- # You can replace the self-signed certificate that synapse
- # autogenerates on launch with your own SSL certificate + key pair
- # if you like. Any required intermediary certificates can be
- # appended after the primary certificate in hierarchical order.
- tls_certificate_path: "%(tls_certificate_path)s"
+ # this is to avoid the max line length. Sorrynotsorry
+ proxypassline = (
+ 'ProxyPass /.well-known/acme-challenge '
+ 'http://localhost:8009/.well-known/acme-challenge'
+ )
+
+ return (
+ """\
+ ## TLS ##
+
+ # PEM-encoded X509 certificate for TLS.
+ # This certificate, as of Synapse 1.0, will need to be a valid and verifiable
+ # certificate, signed by a recognised Certificate Authority.
+ #
+ # See 'ACME support' below to enable auto-provisioning this certificate via
+ # Let's Encrypt.
+ #
+ #tls_certificate_path: "%(tls_certificate_path)s"
+
+ # PEM-encoded private key for TLS
+ #
+ #tls_private_key_path: "%(tls_private_key_path)s"
+
+ # ACME support: This will configure Synapse to request a valid TLS certificate
+ # for your configured `server_name` via Let's Encrypt.
+ #
+ # Note that provisioning a certificate in this way requires port 80 to be
+ # routed to Synapse so that it can complete the http-01 ACME challenge.
+ # By default, if you enable ACME support, Synapse will attempt to listen on
+ # port 80 for incoming http-01 challenges - however, this will likely fail
+ # with 'Permission denied' or a similar error.
+ #
+ # There are a couple of potential solutions to this:
+ #
+ # * If you already have an Apache, Nginx, or similar listening on port 80,
+ # you can configure Synapse to use an alternate port, and have your web
+ # server forward the requests. For example, assuming you set 'port: 8009'
+ # below, on Apache, you would write:
+ #
+ # %(proxypassline)s
+ #
+ # * Alternatively, you can use something like `authbind` to give Synapse
+ # permission to listen on port 80.
+ #
+ acme:
+ # ACME support is disabled by default. Uncomment the following line
+ # (and tls_certificate_path and tls_private_key_path above) to enable it.
+ #
+ #enabled: true
+
+ # Endpoint to use to request certificates. If you only want to test,
+ # use Let's Encrypt's staging url:
+ # https://acme-staging.api.letsencrypt.org/directory
+ #
+ #url: https://acme-v01.api.letsencrypt.org/directory
+
+ # Port number to listen on for the HTTP-01 challenge. Change this if
+ # you are forwarding connections through Apache/Nginx/etc.
+ #
+ #port: 80
- # PEM encoded private key for TLS
- tls_private_key_path: "%(tls_private_key_path)s"
+ # Local addresses to listen on for incoming connections.
+ # Again, you may want to change this if you are forwarding connections
+ # through Apache/Nginx/etc.
+ #
+ #bind_addresses: ['::', '0.0.0.0']
- # PEM dh parameters for ephemeral keys
- tls_dh_params_path: "%(tls_dh_params_path)s"
+ # How many days remaining on a certificate before it is renewed.
+ #
+ #reprovision_threshold: 30
- # Don't bind to the https port
- no_tls: False
+ # The domain that the certificate should be for. Normally this
+ # should be the same as your Matrix domain (i.e., 'server_name'), but,
+ # by putting a file at 'https://<server_name>/.well-known/matrix/server',
+ # you can delegate incoming traffic to another server. If you do that,
+ # you should give the target of the delegation here.
+ #
+ # For example: if your 'server_name' is 'example.com', but
+ # 'https://example.com/.well-known/matrix/server' delegates to
+ # 'matrix.example.com', you should put 'matrix.example.com' here.
+ #
+ # If not set, defaults to your 'server_name'.
+ #
+ #domain: matrix.example.com
# List of allowed TLS fingerprints for this server to publish along
# with the signing keys for this server. Other matrix servers that
@@ -116,80 +270,44 @@ class TlsConfig(Config):
# openssl x509 -outform DER | openssl sha256 -binary | base64 | tr -d '='
# or by checking matrix.org/federationtester/api/report?server_name=$host
#
- tls_fingerprints: []
- # tls_fingerprints: [{"sha256": "<base64_encoded_sha256_fingerprint>"}]
- """ % locals()
+ #tls_fingerprints: [{"sha256": "<base64_encoded_sha256_fingerprint>"}]
- def read_tls_certificate(self, cert_path):
- cert_pem = self.read_file(cert_path, "tls_certificate")
- return crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
+ """
+ % locals()
+ )
- def read_tls_private_key(self, private_key_path):
- private_key_pem = self.read_file(private_key_path, "tls_private_key")
- return crypto.load_privatekey(crypto.FILETYPE_PEM, private_key_pem)
+ def read_tls_certificate(self):
+ """Reads the TLS certificate from the configured file, and returns it
- def generate_files(self, config):
- tls_certificate_path = config["tls_certificate_path"]
- tls_private_key_path = config["tls_private_key_path"]
- tls_dh_params_path = config["tls_dh_params_path"]
-
- if not self.path_exists(tls_private_key_path):
- with open(tls_private_key_path, "wb") 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(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
+ Also checks if it is self-signed, and warns if so
+
+ Returns:
+ OpenSSL.crypto.X509: the certificate
+ """
+ cert_path = self.tls_certificate_file
+ logger.info("Loading TLS certificate from %s", cert_path)
+ cert_pem = self.read_file(cert_path, "tls_certificate_path")
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
+
+ # Check if it is self-signed, and issue a warning if so.
+ if cert.get_issuer() == cert.get_subject():
+ warnings.warn(
+ (
+ "Self-signed TLS certificates will not be accepted by Synapse 1.0. "
+ "Please either provide a valid certificate, or use Synapse's ACME "
+ "support to provision one."
)
+ )
- if not self.path_exists(tls_certificate_path):
- with open(tls_certificate_path, "wb") as certificate_file:
- cert = crypto.X509()
- subject = cert.get_subject()
- subject.CN = config["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)
-
- certificate_file.write(cert_pem)
-
- if not self.path_exists(tls_dh_params_path):
- if GENERATE_DH_PARAMS:
- subprocess.check_call([
- "openssl", "dhparam",
- "-outform", "PEM",
- "-out", tls_dh_params_path,
- "2048"
- ])
- else:
- with open(tls_dh_params_path, "w") as dh_params_file:
- dh_params_file.write(
- "2048-bit DH parameters taken from rfc3526\n"
- "-----BEGIN DH PARAMETERS-----\n"
- "MIIBCAKCAQEA///////////JD9qiIWjC"
- "NMTGYouA3BzRKQJOCIpnzHQCC76mOxOb\n"
- "IlFKCHmONATd75UZs806QxswKwpt8l8U"
- "N0/hNW1tUcJF5IW1dmJefsb0TELppjft\n"
- "awv/XLb0Brft7jhr+1qJn6WunyQRfEsf"
- "5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXT\n"
- "mmkWP6j9JM9fg2VdI9yjrZYcYvNWIIVS"
- "u57VKQdwlpZtZww1Tkq8mATxdGwIyhgh\n"
- "fDKQXkYuNs474553LBgOhgObJ4Oi7Aei"
- "j7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq\n"
- "5RXSJhiY+gUQFXKOWoqsqmj/////////"
- "/wIBAg==\n"
- "-----END DH PARAMETERS-----\n"
- )
+ return cert
+
+ def read_tls_private_key(self):
+ """Reads the TLS private key from the configured file, and returns it
+
+ Returns:
+ OpenSSL.crypto.PKey: the private key
+ """
+ private_key_path = self.tls_private_key_file
+ logger.info("Loading TLS key from %s", private_key_path)
+ private_key_pem = self.read_file(private_key_path, "tls_private_key_path")
+ return crypto.load_privatekey(crypto.FILETYPE_PEM, private_key_pem)
diff --git a/synapse/config/user_directory.py b/synapse/config/user_directory.py
index 38e8947843..fab3a7d1c8 100644
--- a/synapse/config/user_directory.py
+++ b/synapse/config/user_directory.py
@@ -40,5 +40,5 @@ class UserDirectoryConfig(Config):
# on your database to tell it to rebuild the user_directory search indexes.
#
#user_directory:
- # search_all_users: false
+ # search_all_users: false
"""
diff --git a/synapse/config/voip.py b/synapse/config/voip.py
index d07bd24ffd..257f7c86e7 100644
--- a/synapse/config/voip.py
+++ b/synapse/config/voip.py
@@ -27,20 +27,24 @@ class VoipConfig(Config):
def default_config(self, **kwargs):
return """\
- ## Turn ##
+ ## TURN ##
# The public URIs of the TURN server to give to clients
+ #
#turn_uris: []
# The shared secret used to compute passwords for the TURN server
+ #
#turn_shared_secret: "YOUR_SHARED_SECRET"
# The Username and password if the TURN server needs them and
# does not use a token
+ #
#turn_username: "TURNSERVER_USERNAME"
#turn_password: "TURNSERVER_PASSWORD"
# How long generated TURN credentials last
+ #
turn_user_lifetime: "1h"
# Whether guests should be allowed to use the TURN server.
@@ -48,5 +52,6 @@ class VoipConfig(Config):
# However, it does introduce a slight security risk as it allows users to
# connect to arbitrary endpoints without having first signed up for a
# valid account (e.g. by passing a CAPTCHA).
+ #
turn_allow_guests: True
"""
|