1
0

Compare commits

..

9 Commits

Author SHA1 Message Date
Mathieu Velten
f19f47b44b Add test image for complement with workers 2021-01-19 11:12:35 +01:00
Andrew Morgan
9115c47dc3 Temp: add script for building and running worker-mode synapse in complement 2021-01-07 00:22:27 -05:00
Andrew Morgan
ce591bf75b Remove MoveToComplement.Dockerfile
Also added some debug logging in order to try and figure out why the
main homeserver config file isn't getting generated by start.py
2021-01-07 00:08:29 -05:00
Mathieu Velten
1acb2d9ee1 Remove replication listener from the global template 2020-12-31 17:39:24 +01:00
Mathieu Velten
f73e9db981 Various fixes + force TLS disabled 2020-12-31 15:15:07 +01:00
Mathieu Velten
cbe335d2f0 Add more workers 2020-12-31 00:38:05 +01:00
Mathieu Velten
ee138d87db Move client_max_body_size to server and increase to 100M 2020-12-31 00:21:03 +01:00
Mathieu Velten
dfd5e8079b Add more workers config 2020-12-31 00:16:11 +01:00
Mathieu Velten
80db995e33 Change to more dynamic workers config 2020-12-30 20:07:01 +01:00
25 changed files with 598 additions and 541 deletions

View File

@@ -5,11 +5,13 @@ FROM matrixdotorg/synapse
RUN apt-get update
RUN apt-get install -y supervisor redis nginx
RUN rm /etc/nginx/sites-enabled/default
# Copy the worker process and log configuration files
COPY ./docker/workers /conf/workers/
COPY ./docker/worker.yaml.j2 /conf/worker.yaml.j2
# Expose nginx listener port
EXPOSE 8008/tcp
EXPOSE 8080/tcp
# Volume for user-editable config files, logs etc.
VOLUME ["/data"]

View File

@@ -0,0 +1,31 @@
# Inherit from the workers Synapse docker image
FROM matrixdotorg/synapse:workers
RUN apt-get update
RUN apt-get install -y postgresql
RUN pg_ctlcluster 11 main start && su postgres -c "echo \
\"ALTER USER postgres PASSWORD 'somesecret'; \
CREATE DATABASE synapse \
ENCODING 'UTF8' \
LC_COLLATE='C' \
LC_CTYPE='C' \
template=template0;\" | psql" && pg_ctlcluster 11 main stop
WORKDIR /root
RUN curl -OL "https://github.com/caddyserver/caddy/releases/download/v2.3.0/caddy_2.3.0_linux_amd64.tar.gz" && \
tar xzf caddy_2.3.0_linux_amd64.tar.gz && rm caddy_2.3.0_linux_amd64.tar.gz
COPY ./docker/caddy.complement.json /root/caddy.json
EXPOSE 8008 8448
ENTRYPOINT sed -i "s/{{ server_name }}/${SERVER_NAME}/g" /root/caddy.json && \
pg_ctlcluster 11 main start > /dev/null && \
/root/caddy start --config /root/caddy.json > /dev/null && \
SYNAPSE_SERVER_NAME=${SERVER_NAME} \
SYNAPSE_REPORT_STATS=no \
POSTGRES_PASSWORD=somesecret POSTGRES_USER=postgres POSTGRES_HOST=localhost \
SYNAPSE_WORKERS=synchrotron \
/configure_workers_and_start.py

View File

@@ -1,35 +0,0 @@
# This dockerfile builds on top of Dockerfile-worker and includes a built-in postgres instance.
# It is intended to be used for Complement testing
FROM matrixdotorg/synapse:workers
# Install postgres
RUN apt-get update
RUN apt-get install -y postgres
# Create required databases in postgres
# Create a user without a password
RUN sudo -u postgres createuser -w synapse_user
# Then set their password
RUN sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'somesecret';"
# Create the synapse database
RUN sudo -u postgres psql -c "CREATE DATABASE synapse \
ENCODING 'UTF8' \
LC_COLLATE='C' \
LC_CTYPE='C' \
template=template0 \
OWNER synapse_user;"
# Modify Synapse's database config to point to the local postgres
COPY ./docker/synapse_use_local_postgres.py /synapse_use_local_postgres.py
RUN /synapse_use_local_postgres.py
VOLUME ["/data"]
EXPOSE 8008/tcp 8009/tcp 8448/tcp
# Start supervisord
CMD ["/usr/bin/supervisord"]

View File

@@ -0,0 +1,76 @@
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8448"
],
"routes": [
{
"match": [
{
"host": [
"{{ server_name }}"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "localhost:80"
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"{{ server_name }}"
],
"issuers": [
{
"module": "internal"
}
],
"on_demand": true
}
]
}
},
"pki": {
"certificate_authorities": {
"local": {
"name": "Complement CA",
"root": {
"certificate": "/ca/ca.crt",
"private_key": "/ca/ca.key"
},
"intermediate": {
"certificate": "/ca/ca.crt",
"private_key": "/ca/ca.key"
}
}
}
}
}
}

View File

@@ -43,7 +43,7 @@ listeners:
tls: false
bind_addresses: ['::']
type: http
x_forwarded: false
x_forwarded: true
resources:
- names: [client]
@@ -51,15 +51,6 @@ listeners:
- names: [federation]
compress: false
{% if SYNAPSE_WORKERS %}
# The HTTP replication port
- port: 9093
bind_address: '127.0.0.1'
type: http
resources:
- names: [replication]
{% endif %}
## Database ##
{% if POSTGRES_PASSWORD %}

View File

@@ -21,7 +21,94 @@ import os
import sys
import subprocess
import jinja2
import yaml
DEFAULT_LISTENER_RESOURCES = ["client", "federation"]
WORKERS_CONFIG = {
"pusher": {
"app": "synapse.app.pusher",
"listener_resources": [],
"endpoint_patterns": [],
"shared_extra_conf": "start_pushers: false"
},
"user_dir": {
"app": "synapse.app.user_dir",
"listener_resources": DEFAULT_LISTENER_RESOURCES,
"endpoint_patterns": [
"^/_matrix/client/(api/v1|r0|unstable)/user_directory/search$"
],
"shared_extra_conf": "update_user_directory: false"
},
"media_repository": {
"app": "synapse.app.media_repository",
"listener_resources": ["media"],
"endpoint_patterns": [
"^/_synapse/admin/v1/purge_media_cache$",
"^/_synapse/admin/v1/room/.*/media.*$",
"^/_synapse/admin/v1/user/.*/media.*$",
"^/_synapse/admin/v1/media/.*$",
"^/_synapse/admin/v1/quarantine_media/.*$",
],
"shared_extra_conf": "enable_media_repo: false"
},
"appservice": {
"app": "synapse.app.appservice",
"listener_resources": [],
"endpoint_patterns": [],
"shared_extra_conf": "notify_appservices: false"
},
"federation_sender": {
"app": "synapse.app.federation_sender",
"listener_resources": [],
"endpoint_patterns": [],
"shared_extra_conf": "send_federation: false"
},
"synchrotron": {
"app": "synapse.app.generic_worker",
"listener_resources": DEFAULT_LISTENER_RESOURCES,
"endpoint_patterns": [
"^/_matrix/client/(v2_alpha|r0)/sync$",
"^/_matrix/client/(api/v1|v2_alpha|r0)/events$",
"^/_matrix/client/(api/v1|r0)/initialSync$",
"^/_matrix/client/(api/v1|r0)/rooms/[^/]+/initialSync$",
],
"shared_extra_conf": ""
},
"federation_reader": {
"app": "synapse.app.generic_worker",
"listener_resources": DEFAULT_LISTENER_RESOURCES,
"endpoint_patterns": [
"^/_matrix/federation/(v1|v2)/event/",
"^/_matrix/federation/(v1|v2)/state/",
"^/_matrix/federation/(v1|v2)/state_ids/",
"^/_matrix/federation/(v1|v2)/backfill/",
"^/_matrix/federation/(v1|v2)/get_missing_events/",
"^/_matrix/federation/(v1|v2)/publicRooms",
"^/_matrix/federation/(v1|v2)/query/",
"^/_matrix/federation/(v1|v2)/make_join/",
"^/_matrix/federation/(v1|v2)/make_leave/",
"^/_matrix/federation/(v1|v2)/send_join/",
"^/_matrix/federation/(v1|v2)/send_leave/",
"^/_matrix/federation/(v1|v2)/invite/",
"^/_matrix/federation/(v1|v2)/query_auth/",
"^/_matrix/federation/(v1|v2)/event_auth/",
"^/_matrix/federation/(v1|v2)/exchange_third_party_invite/",
"^/_matrix/federation/(v1|v2)/user/devices/",
"^/_matrix/federation/(v1|v2)/get_groups_publicised$",
"^/_matrix/key/v2/query",
],
"shared_extra_conf": ""
},
"federation_inbound": {
"app": "synapse.app.generic_worker",
"listener_resources": DEFAULT_LISTENER_RESOURCES,
"endpoint_patterns": [
"/_matrix/federation/(v1|v2)/send/",
],
"shared_extra_conf": ""
},
}
# Utility functions
def log(txt):
@@ -44,6 +131,7 @@ def convert(src, dst, environ):
with open(src) as infile:
template = infile.read()
rendered = jinja2.Template(template, autoescape=True).render(**environ)
print(rendered)
with open(dst, "w") as outfile:
outfile.write(rendered)
@@ -56,7 +144,7 @@ def generate_base_homeserver_config():
"""
# start.py already does this for us, so just call that.
# note that this script is copied in in the official, monolith dockerfile
subprocess.check_output(["/usr/local/bin/python", "/start.py", "generate"])
subprocess.check_output(["/usr/local/bin/python", "/start.py", "migrate_config"])
def generate_worker_files(environ, config_path: str, data_dir: str):
@@ -74,14 +162,33 @@ def generate_worker_files(environ, config_path: str, data_dir: str):
# The contents of a Synapse config file that will be added alongside the generated
# config when running the main Synapse process.
# It is intended mainly for disabling functionality when certain workers are spun up.
homeserver_config = """
# It is intended mainly for disabling functionality when certain workers are spun up,
# and add the replication listener
# first read the original config file to take listeners config and add the replication one
listeners = [{
"port": 9093,
"bind_address": "127.0.0.1",
"type": "http",
"resources":[{
"names": ["replication"]
}]
}]
with open(config_path) as file_stream:
original_config = yaml.safe_load(file_stream)
original_listeners = original_config.get("listeners")
if original_listeners:
listeners += original_listeners
homeserver_config = yaml.dump({"listeners": listeners})
homeserver_config += """
redis:
enabled: true
# TODO: remove before prod
suppress_key_server_warning: true
"""
"""
# The supervisord config
supervisord_config = """
@@ -90,7 +197,7 @@ nodaemon=true
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
priority=900
priority=500
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
@@ -102,7 +209,7 @@ autorestart=true
command=/usr/local/bin/python -m synapse.app.homeserver \
--config-path="%s" \
--config-path=/conf/workers/shared.yaml
priority=1
# Log startup failures to supervisord's stdout/err
# Regular synapse logs will still go in the configured data directory
stdout_logfile=/dev/stdout
@@ -112,176 +219,95 @@ stderr_logfile_maxbytes=0
autorestart=unexpected
exitcodes=0
""" % (config_path,)
""" % (config_path,)
# An nginx site config. Will live in /etc/nginx/conf.d
nginx_config_template_header = """
server {
# Listen on Synapse's default HTTP port number
listen 8008;
listen [::]:8008;
listen 8080;
listen [::]:8080;
server_name localhost;
# Nginx by default only allows file uploads up to 1M in size
# Increase client_max_body_size to match max_upload_size defined in homeserver.yaml
client_max_body_size 100M;
"""
nginx_config_body = "" # to modify below
nginx_config_template_end = """
# Send all other traffic to the main process
location ~* ^(\/_matrix|\/_synapse) {
proxy_pass http://localhost:18008;
proxy_pass http://localhost:8008;
proxy_set_header X-Forwarded-For $remote_addr;
# TODO: Can we move this to the default nginx.conf so all locations are
# affected?
#
# Nginx by default only allows file uploads up to 1M in size
# Increase client_max_body_size to match max_upload_size defined in homeserver.yaml
client_max_body_size 50M;
}
}
"""
"""
# Read desired worker configuration from environment
if "SYNAPSE_WORKERS" not in environ:
error("Environment variable 'SYNAPSE_WORKERS' is mandatory.")
worker_types = []
else:
worker_types = environ.get("SYNAPSE_WORKERS")
worker_types = worker_types.split(",")
worker_types = environ.get("SYNAPSE_WORKERS")
worker_types = worker_types.split(",")
os.mkdir("/conf/workers")
worker_port = 18009
for worker_type in worker_types:
worker_type = worker_type.strip()
if worker_type == "pusher":
# Disable push handling from the main process
homeserver_config += """
start_pushers: false
"""
worker_config = WORKERS_CONFIG.get(worker_type)
if worker_config:
worker_config = worker_config.copy()
else:
log(worker_type + " is a wrong worker type ! It will be ignored")
continue
# this is not hardcoded bc we want to be able to have several workers
# of each type ultimately (not supported for now)
worker_name = worker_type
worker_config.update({"name": worker_name})
worker_config.update({"port": worker_port})
worker_config.update({"config_path": config_path})
homeserver_config += worker_config['shared_extra_conf'] + "\n"
# Enable the pusher worker in supervisord
supervisord_config += """
[program:synapse_pusher]
command=/usr/local/bin/python -m synapse.app.pusher \
--config-path="%s" \
supervisord_config += """
[program:synapse_{name}]
command=/usr/local/bin/python -m {app} \
--config-path="{config_path}" \
--config-path=/conf/workers/shared.yaml \
--config-path=/conf/workers/pusher.yaml
--config-path=/conf/workers/{name}.yaml
autorestart=unexpected
priority=500
exitcodes=0
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
""" % (config_path,)
stderr_logfile_maxbytes=0""".format_map(worker_config)
# This worker does not handle any REST endpoints
elif worker_type == "appservice":
# Disable appservice traffic sending from the main process
homeserver_config += """
notify_appservices: false
"""
# Enable the pusher worker in supervisord
supervisord_config += """
[program:synapse_appservice]
command=/usr/local/bin/python -m synapse.app.appservice \
--config-path="%s" \
--config-path=/conf/workers/shared.yaml \
--config-path=/conf/workers/appservice.yaml
autorestart=unexpected
exitcodes=0
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
""" % (config_path,)
# This worker does not handle any REST endpoints
elif worker_type == "user_dir":
# Disable user directory updates on the main process
homeserver_config += """
update_user_directory: false
"""
# Enable the user directory worker in supervisord
supervisord_config += """
[program:synapse_user_dir]
command=/usr/local/bin/python -m synapse.app.user_dir \
--config-path="%s" \
--config-path=/conf/workers/shared.yaml \
--config-path=/conf/workers/user_dir.yaml
autorestart=unexpected
exitcodes=0
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
""" % (config_path,)
# Route user directory requests to this worker
for pattern in worker_config['endpoint_patterns']:
nginx_config_body += """
location ~* ^/_matrix/client/(api/v1|r0|unstable)/user_directory/search$ {
proxy_pass http://localhost:8010;
location ~* %s {
proxy_pass http://localhost:%s;
proxy_set_header X-Forwarded-For $remote_addr;
}
"""
""" % (pattern, worker_port)
elif worker_type == "federation_sender":
# Disable user directory updates on the main process
homeserver_config += """
send_federation: False
"""
convert("/conf/worker.yaml.j2", "/conf/workers/{name}.yaml".format(name=worker_name), worker_config)
# Enable the user directory worker in supervisord
supervisord_config += """
[program:synapse_user_dir]
command=/usr/local/bin/python -m synapse.app.user_dir \
--config-path="%s" \
--config-path=/conf/workers/shared.yaml \
--config-path=/conf/workers/user_dir.yaml
autorestart=unexpected
exitcodes=0
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
""" % (config_path,)
worker_port += 1
# This worker does not handle any REST endpoints
elif worker_type == "media_repository":
# Disable user directory updates on the main process
homeserver_config += """
update_user_directory: false
"""
# Enable the user directory worker in supervisord
supervisord_config += """
[program:synapse_user_dir]
command=/usr/local/bin/python -m synapse.app.user_dir \
--config-path="%s" \
--config-path=/conf/workers/shared.yaml \
--config-path=/conf/workers/user_dir.yaml
autorestart=unexpected
exitcodes=0
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
""" % (config_path,)
# Route user directory requests to this worker
nginx_config_body += """
location ~* (^/_matrix/media/.*$|^/_synapse/admin/v1/(purge_media_cache$|(room|user)/.*/media.*$|media/.*$|quarantine_media/.*$) {
proxy_pass http://localhost:8010;
proxy_set_header X-Forwarded-For $remote_addr;
}
"""
# Write out the config files
# Write out the config files. We use append mode for each in case the
# files may have already been written to by others.
# Shared homeserver config
print(homeserver_config)
with open("/conf/workers/shared.yaml", "w") as f:
with open("/conf/workers/shared.yaml", "a") as f:
f.write(homeserver_config)
# Nginx config
@@ -289,7 +315,7 @@ stderr_logfile_maxbytes=0
print(nginx_config_template_header)
print(nginx_config_body)
print(nginx_config_template_end)
with open("/etc/nginx/conf.d/matrix-synapse.conf", "w") as f:
with open("/etc/nginx/conf.d/matrix-synapse.conf", "a") as f:
f.write(nginx_config_template_header)
f.write(nginx_config_body)
f.write(nginx_config_template_end)
@@ -297,20 +323,9 @@ stderr_logfile_maxbytes=0
# Supervisord config
print()
print(supervisord_config)
with open("/etc/supervisor/conf.d/supervisord.conf", "w") as f:
with open("/etc/supervisor/conf.d/supervisord.conf", "a") as f:
f.write(supervisord_config)
# Generate worker log config files from the templates.
# The templates are mainly there so that we can inject some environment variable
# values into them.
log_config_template_dir = "/conf/workers/log_config_templates/"
log_config_dir = "/conf/workers/"
for log_config_filename in os.listdir(log_config_template_dir):
template_path = log_config_template_dir + log_config_filename
out_path = log_config_dir + log_config_filename
convert(template_path, out_path, environ)
# Ensure the logging directory exists
log_dir = data_dir + "/logs"
if not os.path.exists(log_dir):
@@ -330,6 +345,10 @@ def main(args, environ):
config_path = environ.get("SYNAPSE_CONFIG_PATH", config_dir + "/homeserver.yaml")
data_dir = environ.get("SYNAPSE_DATA_DIR", "/data")
# override SYNAPSE_NO_TLS, we don't support TLS in worker mode,
# this needs to be handled by a frontend proxy
environ["SYNAPSE_NO_TLS"] = "yes"
# Generate the base homeserver config if one does not yet exist
if not os.path.exists(config_path):
log("Generating base homeserver config")

View File

@@ -134,6 +134,7 @@ def run_generate_config(environ, ownership):
Never returns.
"""
print("running generate config")
for v in ("SYNAPSE_SERVER_NAME", "SYNAPSE_REPORT_STATS"):
if v not in environ:
error("Environment variable '%s' is mandatory in `generate` mode." % (v,))
@@ -149,6 +150,8 @@ def run_generate_config(environ, ownership):
log("Creating log config %s" % (log_config_file,))
convert("/conf/log.config", log_config_file, environ)
print("Generating config at", config_path, "Config dir:", config_dir)
args = [
"python",
"-m",
@@ -178,6 +181,7 @@ def run_generate_config(environ, ownership):
os.execv("/usr/local/bin/python", args)
def main(args, environ):
print("bla")
mode = args[1] if len(args) > 1 else "run"
desired_uid = int(environ.get("UID", "991"))
desired_gid = int(environ.get("GID", "991"))

15
docker/worker.yaml.j2 Normal file
View File

@@ -0,0 +1,15 @@
worker_app: "{{ app }}"
worker_name: "{{ name }}"
# The replication listener on the main synapse process.
worker_replication_host: 127.0.0.1
worker_replication_http_port: 9093
worker_listeners:
- type: http
port: {{ port }}
resources:
- names:
{%- for resource in listener_resources %}
- {{ resource }}
{%- endfor %}

View File

@@ -1,30 +0,0 @@
version: 1
formatters:
precise:
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
handlers:
file:
class: logging.handlers.TimedRotatingFileHandler
formatter: precise
filename: "{{ SYNAPSE_DATA_DIR or '/data' }}/logs/pusher1.log"
when: midnight
backupCount: 3 # Does not include the current log file.
encoding: utf8
console:
class: logging.StreamHandler
formatter: precise
loggers:
synapse.storage.SQL:
# beware: increasing this to DEBUG will make synapse log sensitive
# information such as access tokens.
level: INFO
root:
level: {{ SYNAPSE_LOG_LEVEL or "INFO" }}
handlers: [console]
disable_existing_loggers: false

View File

@@ -1,30 +0,0 @@
version: 1
formatters:
precise:
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
handlers:
file:
class: logging.handlers.TimedRotatingFileHandler
formatter: precise
filename: "{{ SYNAPSE_DATA_DIR or '/data' }}/logs/pusher1.log"
when: midnight
backupCount: 3 # Does not include the current log file.
encoding: utf8
console:
class: logging.StreamHandler
formatter: precise
loggers:
synapse.storage.SQL:
# beware: increasing this to DEBUG will make synapse log sensitive
# information such as access tokens.
level: INFO
root:
level: {{ SYNAPSE_LOG_LEVEL or "INFO" }}
handlers: [console]
disable_existing_loggers: false

View File

@@ -1,13 +0,0 @@
worker_app: synapse.app.pusher
worker_name: pusher
# The replication listener on the main synapse process.
worker_replication_host: 127.0.0.1
worker_replication_http_port: 9093
worker_listeners:
- type: http
port: 8083
resources: []
worker_log_config: /conf/workers/pusher_log.yaml

View File

@@ -1,13 +0,0 @@
worker_app: synapse.app.user_dir
worker_name: user_dir
# The replication listener on the main synapse process.
worker_replication_host: 127.0.0.1
worker_replication_http_port: 9093
worker_listeners:
- type: http
port: 8084
resources: []
worker_log_config: /conf/workers/user_dir_log.yaml

View File

@@ -1111,8 +1111,15 @@ url_preview_accept_language:
#turn_allow_guests: true
## Account Validity ##
## Registration ##
#
# Registration can be rate-limited using the parameters in the "Ratelimiting"
# section of this file.
# Enable registration for new users.
#
#enable_registration: false
# Optional account validity configuration. This allows for accounts to be denied
# any request after a given period.
#
@@ -1124,67 +1131,57 @@ url_preview_accept_language:
# after that the validity period changes and Synapse is restarted, the users'
# expiration dates won't be updated unless their account is manually renewed. This
# date will be randomly selected within a range [now + period - d ; now + period],
# where d is equal to 10%% of the validity period.
# where d is equal to 10% of the validity period.
#
account_validity:
# The account validity feature is disabled by default. Uncomment the
# following line to enable it.
#
#enabled: true
# The account validity feature is disabled by default. Uncomment the
# following line to enable it.
#
#enabled: true
# The period after which an account is valid after its registration. When
# renewing the account, its validity period will be extended by this amount
# of time. This parameter is required when using the account validity
# feature.
#
#period: 6w
# The period after which an account is valid after its registration. When
# renewing the account, its validity period will be extended by this amount
# of time. This parameter is required when using the account validity
# feature.
#
#period: 6w
# The amount of time before an account's expiry date at which Synapse will
# send an email to the account's email address with a renewal link. By
# default, no such emails are sent.
#
# If you enable this setting, you will also need to fill out the 'email' and
# 'public_baseurl' configuration sections.
#
#renew_at: 1w
# The amount of time before an account's expiry date at which Synapse will
# send an email to the account's email address with a renewal link. By
# default, no such emails are sent.
#
# If you enable this setting, you will also need to fill out the 'email' and
# 'public_baseurl' configuration sections.
#
#renew_at: 1w
# The subject of the email sent out with the renewal link. '%%(app)s' can be
# used as a placeholder for the 'app_name' parameter from the 'email'
# section.
#
# Note that the placeholder must be written '%%(app)s', including the
# trailing 's'.
#
# If this is not set, a default value is used.
#
#renew_email_subject: "Renew your %%(app)s account"
# The subject of the email sent out with the renewal link. '%(app)s' can be
# used as a placeholder for the 'app_name' parameter from the 'email'
# section.
#
# Note that the placeholder must be written '%(app)s', including the
# trailing 's'.
#
# If this is not set, a default value is used.
#
#renew_email_subject: "Renew your %(app)s account"
# Directory in which Synapse will try to find templates for the HTML files to
# serve to the user when trying to renew an account. If not set, default
# templates from within the Synapse package will be used.
#
#template_dir: "res/templates"
# Directory in which Synapse will try to find templates for the HTML files to
# serve to the user when trying to renew an account. If not set, default
# templates from within the Synapse package will be used.
#
#template_dir: "res/templates"
# File within 'template_dir' giving the HTML to be displayed to the user after
# they successfully renewed their account. If not set, default text is used.
#
#account_renewed_html_path: "account_renewed.html"
# File within 'template_dir' giving the HTML to be displayed to the user after
# they successfully renewed their account. If not set, default text is used.
#
#account_renewed_html_path: "account_renewed.html"
# File within 'template_dir' giving the HTML to be displayed when the user
# tries to renew an account with an invalid renewal token. If not set,
# default text is used.
#
#invalid_token_html_path: "invalid_token.html"
## Registration ##
#
# Registration can be rate-limited using the parameters in the "Ratelimiting"
# section of this file.
# Enable registration for new users.
#
#enable_registration: false
# File within 'template_dir' giving the HTML to be displayed when the user
# tries to renew an account with an invalid renewal token. If not set,
# default text is used.
#
#invalid_token_html_path: "invalid_token.html"
# Time that a user's session remains valid for, after they log in.
#

View File

@@ -0,0 +1,71 @@
# Log configuration for Synapse.
#
# This is a YAML file containing a standard Python logging configuration
# dictionary. See [1] for details on the valid settings.
#
# Synapse also supports structured logging for machine readable logs which can
# be ingested by ELK stacks. See [2] for details.
#
# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema
# [2]: https://github.com/matrix-org/synapse/blob/master/docs/structured_logging.md
version: 1
formatters:
precise:
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
handlers:
file:
class: logging.handlers.TimedRotatingFileHandler
formatter: precise
filename: /var/log/matrix-synapse/homeserver.log
when: midnight
backupCount: 3 # Does not include the current log file.
encoding: utf8
# Default to buffering writes to log file for efficiency. This means that
# will be a delay for INFO/DEBUG logs to get written, but WARNING/ERROR
# logs will still be flushed immediately.
buffer:
class: logging.handlers.MemoryHandler
target: file
# The capacity is the number of log lines that are buffered before
# being written to disk. Increasing this will lead to better
# performance, at the expensive of it taking longer for log lines to
# be written to disk.
capacity: 10
flushLevel: 30 # Flush for WARNING logs as well
# A handler that writes logs to stderr. Unused by default, but can be used
# instead of "buffer" and "file" in the logger handlers.
console:
class: logging.StreamHandler
formatter: precise
loggers:
synapse.storage.SQL:
# beware: increasing this to DEBUG will make synapse log sensitive
# information such as access tokens.
level: INFO
twisted:
# We send the twisted logging directly to the file handler,
# to work around https://github.com/matrix-org/synapse/issues/3471
# when using "buffer" logger. Use "console" to log to stderr instead.
handlers: [file]
propagate: false
root:
level: INFO
# Write logs to the `buffer` handler, which will buffer them together in memory,
# then write them to a file.
#
# Replace "buffer" with "console" to log to stderr instead. (Note that you'll
# also need to update the configuration for the `twisted` logger above, in
# this case.)
#
handlers: [buffer]
disable_existing_loggers: false

View File

@@ -0,0 +1,30 @@
#! /bin/bash -eu
# This script is designed for developers who want to test their code
# against Complement.
#
# It creates a Complement-ready worker-enabled Synapse docker image from
# the local checkout and runs Complement tests against it.
#
# This script assumes that it is located in the scripts-dev folder of a
# Synapse checkout, and that Complement exists at ../../complement
# In my case, I have /home/user/code/complement and /home/user/code/synapse.
COMPLEMENT_DIR="/home/user/code/complement"
cd "$(dirname $0)/.."
# Build the Synapse image from the local checkout
docker build -t matrixdotorg/synapse:latest -f docker/Dockerfile .
# Build the base Synapse worker image
docker build -t matrixdotorg/synapse:workers -f docker/Dockerfile-workers .
cd "$COMPLEMENT_DIR"
# Build the Complement Synapse worker image
docker build -t matrixdotorg/complement-synapse:workers -f dockerfiles/SynapseWorkers.Dockerfile dockerfiles
# Run the tests on the resulting image!
COMPLEMENT_VERSION_CHECK_ITERATIONS=300 COMPLEMENT_DEBUG=1 COMPLEMENT_BASE_IMAGE=matrixdotorg/complement-synapse:workers go test -v -count=1 -tags="synapse_blacklist" -failfast ./tests
#COMPLEMENT_VERSION_CHECK_ITERATIONS=100 COMPLEMENT_DEBUG=1 COMPLEMENT_BASE_IMAGE=complement-synapse go test -v -count=1 -parallel=1 ./tests/
#COMPLEMENT_VERSION_CHECK_ITERATIONS=100 COMPLEMENT_BASE_IMAGE=complement-synapse go test ./tests

View File

@@ -76,7 +76,7 @@ class Auth:
self._auth_blocking = AuthBlocking(self.hs)
self._account_validity_enabled = hs.config.account_validity_enabled
self._account_validity = hs.config.account_validity
self._track_appservice_user_ips = hs.config.track_appservice_user_ips
self._macaroon_secret_key = hs.config.macaroon_secret_key
@@ -219,7 +219,7 @@ class Auth:
shadow_banned = user_info.shadow_banned
# Deny the request if the user account has expired.
if self._account_validity_enabled and not allow_expired:
if self._account_validity.enabled and not allow_expired:
if await self.store.is_account_expired(
user_info.user_id, self.clock.time_msec()
):

View File

@@ -1,7 +1,6 @@
from typing import Any, Iterable, List, Optional
from synapse.config import (
account_validity,
api,
appservice,
captcha,
@@ -56,7 +55,6 @@ class RootConfig:
media: repository.ContentRepositoryConfig
captcha: captcha.CaptchaConfig
voip: voip.VoipConfig
accountvalidity: account_validity.AccountValidityConfig
registration: registration.RegistrationConfig
metrics: metrics.MetricsConfig
api: api.ApiConfig

View File

@@ -1,171 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2020 The Matrix.org Foundation C.I.C.
#
# 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 os
import pkg_resources
from synapse.config._base import Config, ConfigError
class AccountValidityConfig(Config):
section = "accountvalidity"
def read_config(self, config, **kwargs):
account_validity_config = config.get("account_validity", {})
self.account_validity_enabled = account_validity_config.get("enabled", False)
self.account_validity_renew_by_email_enabled = (
"renew_at" in account_validity_config
)
if self.account_validity_enabled:
if "period" in config:
self.account_validity_period = self.parse_duration(config["period"])
else:
raise ConfigError("'period' is required when using account validity")
if "renew_at" in config:
self.account_validity_renew_at = self.parse_duration(config["renew_at"])
if "renew_email_subject" in config:
self.account_validity_renew_email_subject = config[
"renew_email_subject"
]
else:
self.account_validity_renew_email_subject = "Renew your %(app)s account"
self.account_validity_startup_job_max_delta = self.period * 10.0 / 100.0
if self.account_validity_renew_by_email_enabled:
if not self.public_baseurl:
raise ConfigError("Can't send renewal emails without 'public_baseurl'")
template_dir = config.get("template_dir")
if not template_dir:
template_dir = pkg_resources.resource_filename("synapse", "res/templates")
if "account_renewed_html_path" in config:
file_path = os.path.join(template_dir, config["account_renewed_html_path"])
self.account_validity_account_renewed_html_content = self.read_file(
file_path, "account_validity.account_renewed_html_path"
)
else:
self.account_validity_account_renewed_html_content = (
"<html><body>Your account has been successfully renewed.</body><html>"
)
if "invalid_token_html_path" in config:
file_path = os.path.join(template_dir, config["invalid_token_html_path"])
self.account_validity_invalid_token_html_content = self.read_file(
file_path, "account_validity.invalid_token_html_path"
)
else:
self.account_validity_invalid_token_html_content = (
"<html><body>Invalid renewal token.</body><html>"
)
# Load account validity templates.
# We do this here instead of in AccountValidityConfig as read_templates
# relies on state that hasn't been initialised in AccountValidityConfig
account_renewed_template_filename = config.get(
"account_renewed_html_path", "account_renewed.html"
)
account_previously_renewed_template_filename = config.get(
"account_previously_renewed_html_path", "account_previously_renewed.html"
)
invalid_token_template_filename = config.get(
"invalid_token_html_path", "invalid_token.html"
)
(
self.account_validity.account_renewed_template,
self.account_validity.account_previously_renewed_template,
self.account_validity.invalid_token_template,
) = self.read_templates(
[
account_renewed_template_filename,
account_previously_renewed_template_filename,
invalid_token_template_filename,
]
)
def generate_config_section(self, **kwargs):
return """\
## Account Validity ##
#
# Optional account validity configuration. This allows for accounts to be denied
# any request after a given period.
#
# Once this feature is enabled, Synapse will look for registered users without an
# expiration date at startup and will add one to every account it found using the
# current settings at that time.
# This means that, if a validity period is set, and Synapse is restarted (it will
# then derive an expiration date from the current validity period), and some time
# after that the validity period changes and Synapse is restarted, the users'
# expiration dates won't be updated unless their account is manually renewed. This
# date will be randomly selected within a range [now + period - d ; now + period],
# where d is equal to 10%% of the validity period.
#
account_validity:
# The account validity feature is disabled by default. Uncomment the
# following line to enable it.
#
#enabled: true
# The period after which an account is valid after its registration. When
# renewing the account, its validity period will be extended by this amount
# of time. This parameter is required when using the account validity
# feature.
#
#period: 6w
# The amount of time before an account's expiry date at which Synapse will
# send an email to the account's email address with a renewal link. By
# default, no such emails are sent.
#
# If you enable this setting, you will also need to fill out the 'email' and
# 'public_baseurl' configuration sections.
#
#renew_at: 1w
# The subject of the email sent out with the renewal link. '%%(app)s' can be
# used as a placeholder for the 'app_name' parameter from the 'email'
# section.
#
# Note that the placeholder must be written '%%(app)s', including the
# trailing 's'.
#
# If this is not set, a default value is used.
#
#renew_email_subject: "Renew your %%(app)s account"
# Directory in which Synapse will try to find templates for the HTML files to
# serve to the user when trying to renew an account. If not set, default
# templates from within the Synapse package will be used.
#
#template_dir: "res/templates"
# File within 'template_dir' giving the HTML to be displayed to the user after
# they successfully renewed their account. If not set, default text is used.
#
#account_renewed_html_path: "account_renewed.html"
# File within 'template_dir' giving the HTML to be displayed when the user
# tries to renew an account with an invalid renewal token. If not set,
# default text is used.
#
#invalid_token_html_path: "invalid_token.html"
"""

View File

@@ -13,8 +13,8 @@
# 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 RootConfig
from .account_validity import AccountValidityConfig
from .api import ApiConfig
from .appservice import AppServiceConfig
from .cache import CacheConfig
@@ -66,7 +66,6 @@ class HomeServerConfig(RootConfig):
ContentRepositoryConfig,
CaptchaConfig,
VoipConfig,
AccountValidityConfig,
RegistrationConfig,
MetricsConfig,
ApiConfig,

View File

@@ -13,14 +13,75 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from distutils.util import strtobool
import pkg_resources
from synapse.api.constants import RoomCreationPreset
from synapse.config._base import Config, ConfigError
from synapse.types import RoomAlias, UserID
from synapse.util.stringutils import random_string_with_symbols
class AccountValidityConfig(Config):
section = "accountvalidity"
def __init__(self, config, synapse_config):
if config is None:
return
super().__init__()
self.enabled = config.get("enabled", False)
self.renew_by_email_enabled = "renew_at" in config
if self.enabled:
if "period" in config:
self.period = self.parse_duration(config["period"])
else:
raise ConfigError("'period' is required when using account validity")
if "renew_at" in config:
self.renew_at = self.parse_duration(config["renew_at"])
if "renew_email_subject" in config:
self.renew_email_subject = config["renew_email_subject"]
else:
self.renew_email_subject = "Renew your %(app)s account"
self.startup_job_max_delta = self.period * 10.0 / 100.0
if self.renew_by_email_enabled:
if "public_baseurl" not in synapse_config:
raise ConfigError("Can't send renewal emails without 'public_baseurl'")
template_dir = config.get("template_dir")
if not template_dir:
template_dir = pkg_resources.resource_filename("synapse", "res/templates")
if "account_renewed_html_path" in config:
file_path = os.path.join(template_dir, config["account_renewed_html_path"])
self.account_renewed_html_content = self.read_file(
file_path, "account_validity.account_renewed_html_path"
)
else:
self.account_renewed_html_content = (
"<html><body>Your account has been successfully renewed.</body><html>"
)
if "invalid_token_html_path" in config:
file_path = os.path.join(template_dir, config["invalid_token_html_path"])
self.invalid_token_html_content = self.read_file(
file_path, "account_validity.invalid_token_html_path"
)
else:
self.invalid_token_html_content = (
"<html><body>Invalid renewal token.</body><html>"
)
class RegistrationConfig(Config):
section = "registration"
@@ -33,6 +94,10 @@ class RegistrationConfig(Config):
strtobool(str(config["disable_registration"]))
)
self.account_validity = AccountValidityConfig(
config.get("account_validity") or {}, config
)
self.registrations_require_3pid = config.get("registrations_require_3pid", [])
self.allowed_local_3pids = config.get("allowed_local_3pids", [])
self.enable_3pid_lookup = config.get("enable_3pid_lookup", True)
@@ -146,6 +211,69 @@ class RegistrationConfig(Config):
#
#enable_registration: false
# Optional account validity configuration. This allows for accounts to be denied
# any request after a given period.
#
# Once this feature is enabled, Synapse will look for registered users without an
# expiration date at startup and will add one to every account it found using the
# current settings at that time.
# This means that, if a validity period is set, and Synapse is restarted (it will
# then derive an expiration date from the current validity period), and some time
# after that the validity period changes and Synapse is restarted, the users'
# expiration dates won't be updated unless their account is manually renewed. This
# date will be randomly selected within a range [now + period - d ; now + period],
# where d is equal to 10%% of the validity period.
#
account_validity:
# The account validity feature is disabled by default. Uncomment the
# following line to enable it.
#
#enabled: true
# The period after which an account is valid after its registration. When
# renewing the account, its validity period will be extended by this amount
# of time. This parameter is required when using the account validity
# feature.
#
#period: 6w
# The amount of time before an account's expiry date at which Synapse will
# send an email to the account's email address with a renewal link. By
# default, no such emails are sent.
#
# If you enable this setting, you will also need to fill out the 'email' and
# 'public_baseurl' configuration sections.
#
#renew_at: 1w
# The subject of the email sent out with the renewal link. '%%(app)s' can be
# used as a placeholder for the 'app_name' parameter from the 'email'
# section.
#
# Note that the placeholder must be written '%%(app)s', including the
# trailing 's'.
#
# If this is not set, a default value is used.
#
#renew_email_subject: "Renew your %%(app)s account"
# Directory in which Synapse will try to find templates for the HTML files to
# serve to the user when trying to renew an account. If not set, default
# templates from within the Synapse package will be used.
#
#template_dir: "res/templates"
# File within 'template_dir' giving the HTML to be displayed to the user after
# they successfully renewed their account. If not set, default text is used.
#
#account_renewed_html_path: "account_renewed.html"
# File within 'template_dir' giving the HTML to be displayed when the user
# tries to renew an account with an invalid renewal token. If not set,
# default text is used.
#
#invalid_token_html_path: "invalid_token.html"
# Time that a user's session remains valid for, after they log in.
#
# Note that this is not currently compatible with guest logins.

View File

@@ -40,18 +40,11 @@ class AccountValidityHandler:
self.sendmail = self.hs.get_sendmail()
self.clock = self.hs.get_clock()
self._account_validity_period = self.hs.config.account_validity_period
self._account_validity_enabled = self.hs.config.account_validity_enabled
self._account_validity_renew_email_subject = (
self.hs.config.account_validity_renew_email_subject
)
self._account_validity_renew_by_email_enabled = (
self.hs.config.account_validity_renew_by_email_enabled
)
self._account_validity = self.hs.config.account_validity
if (
self._account_validity_enabled
and self._account_validity_renew_by_email_enabled
self._account_validity.enabled
and self._account_validity.renew_by_email_enabled
):
# Don't do email-specific configuration if renewal by email is disabled.
self._template_html = self.config.account_validity_template_html
@@ -60,14 +53,14 @@ class AccountValidityHandler:
try:
app_name = self.hs.config.email_app_name
self._subject = self._account_validity_renew_email_subject % {
self._subject = self._account_validity.renew_email_subject % {
"app": app_name
}
self._from_string = self.hs.config.email_notif_from % {"app": app_name}
except Exception:
# If substitution failed, fall back to the bare strings.
self._subject = self._account_validity_renew_email_subject
self._subject = self._account_validity.renew_email_subject
self._from_string = self.hs.config.email_notif_from
self._raw_from = email.utils.parseaddr(self._from_string)[1]
@@ -265,7 +258,7 @@ class AccountValidityHandler:
milliseconds since epoch.
"""
if expiration_ts is None:
expiration_ts = self.clock.time_msec() + self._account_validity_period
expiration_ts = self.clock.time_msec() + self._account_validity.period
await self.store.set_account_validity_for_user(
user_id=user_id, expiration_ts=expiration_ts, email_sent=email_sent

View File

@@ -49,7 +49,7 @@ class DeactivateAccountHandler(BaseHandler):
if hs.config.run_background_tasks:
hs.get_reactor().callWhenRunning(self._start_user_parting)
self._account_validity_enabled = hs.config.account_validity_enabled
self._account_validity_enabled = hs.config.account_validity.enabled
async def deactivate_account(
self, user_id: str, erase_data: bool, id_server: Optional[str] = None

View File

@@ -62,7 +62,7 @@ class PusherPool:
self.store = self.hs.get_datastore()
self.clock = self.hs.get_clock()
self._account_validity_enabled = hs.config.account_validity_enabled
self._account_validity = hs.config.account_validity
# We shard the handling of push notifications by user ID.
self._pusher_shard_config = hs.config.push.pusher_shard_config
@@ -223,7 +223,7 @@ class PusherPool:
for u in users_affected:
# Don't push if the user account has expired
if self._account_validity_enabled:
if self._account_validity.enabled:
expired = await self.store.is_account_expired(
u, self.clock.time_msec()
)
@@ -251,7 +251,7 @@ class PusherPool:
for u in users_affected:
# Don't push if the user account has expired
if self._account_validity_enabled:
if self._account_validity.enabled:
expired = await self.store.is_account_expired(
u, self.clock.time_msec()
)

View File

@@ -37,8 +37,8 @@ class AccountValidityRenewServlet(RestServlet):
self.hs = hs
self.account_activity_handler = hs.get_account_validity_handler()
self.auth = hs.get_auth()
self.success_html = hs.config.account_validity_account_renewed_html_content
self.failure_html = hs.config.account_validity_invalid_token_html_content
self.success_html = hs.config.account_validity.account_renewed_html_content
self.failure_html = hs.config.account_validity.invalid_token_html_content
async def on_GET(self, request):
if b"token" not in request.args:

View File

@@ -82,13 +82,8 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
database.engine, find_max_generated_user_id_localpart, "user_id_seq",
)
self._account_validity_enabled = hs.config.account_validity_enabled
self._account_validity_period = hs.config.account_validity_period
self._account_validity_renew_at = hs.config.account_validity_renew_at
self._account_validity_startup_job_max_delta = (
hs.config.account_validity_startup_job_max_delta
)
if hs.config.run_background_tasks and self._account_validity_enabled:
self._account_validity = hs.config.account_validity
if hs.config.run_background_tasks and self._account_validity.enabled:
self._clock.call_later(
0.0, self._set_expiration_date_when_missing,
)
@@ -296,7 +291,7 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
"get_users_expiring_soon",
select_users_txn,
self._clock.time_msec(),
self.config.account_validity_renew_at,
self.config.account_validity.renew_at,
)
async def set_renewal_mail_status(self, user_id: str, email_sent: bool) -> None:
@@ -907,11 +902,11 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
delta equal to 10% of the validity period.
"""
now_ms = self._clock.time_msec()
expiration_ts = now_ms + self._account_validity_period
expiration_ts = now_ms + self._account_validity.period
if use_delta:
expiration_ts = self.rand.randrange(
expiration_ts - self._account_validity_startup_job_max_delta,
expiration_ts - self._account_validity.startup_job_max_delta,
expiration_ts,
)
@@ -1311,7 +1306,7 @@ class RegistrationStore(StatsStore, RegistrationBackgroundUpdateStore):
except self.database_engine.module.IntegrityError:
raise StoreError(400, "User ID already taken.", errcode=Codes.USER_IN_USE)
if self._account_validity_enabled:
if self._account_validity.enabled:
self.set_expiration_date_for_user_txn(txn, user_id)
if create_profile_with_displayname: