Merge branch 'develop' into cross-signing_sig_upload

This commit is contained in:
Hubert Chathi 2019-09-07 13:14:45 -04:00
commit 0d61d1d735
61 changed files with 1486 additions and 583 deletions

View file

@ -49,6 +49,56 @@ returned by the Client-Server API:
# configured on port 443. # configured on port 443.
curl -kv https://<host.name>/_matrix/client/versions 2>&1 | grep "Server:" curl -kv https://<host.name>/_matrix/client/versions 2>&1 | grep "Server:"
Upgrading to v1.4.0
===================
Config options
--------------
**Note: Registration by email address or phone number will not work in this release unless
some config options are changed from their defaults.**
This is due to Synapse v1.4.0 now defaulting to sending registration and password reset tokens
itself. This is for security reasons as well as putting less reliance on identity servers.
However, currently Synapse only supports sending emails, and does not have support for
phone-based password reset or account registration. If Synapse is configured to handle these on
its own, phone-based password resets and registration will be disabled. For Synapse to send
emails, the ``email`` block of the config must be filled out. If not, then password resets and
registration via email will be disabled entirely.
This release also deprecates the ``email.trust_identity_server_for_password_resets`` option and
replaces it with the ``account_threepid_delegates`` dictionary. This option defines whether the
homeserver should delegate an external server (typically an `identity server
<https://matrix.org/docs/spec/identity_service/r0.2.1>`_) to handle sending password reset or
registration messages via email and SMS.
If ``email.trust_identity_server_for_password_resets`` is set to ``true``, and
``account_threepid_delegates.email`` is not set, then the first entry in
``trusted_third_party_id_servers`` will be used as the account threepid delegate for email.
This is to ensure compatibility with existing Synapse installs that set up external server
handling for these tasks before v1.4.0. If ``email.trust_identity_server_for_password_resets``
is ``true`` and no trusted identity server domains are configured, Synapse will throw an error.
If ``email.trust_identity_server_for_password_resets`` is ``false`` or absent and a threepid
type in ``account_threepid_delegates`` is not set to a domain, then Synapse will attempt to
send password reset and registration messages for that type.
Email templates
---------------
If you have configured a custom template directory with the ``email.template_dir`` option, be
aware that there are new templates regarding registration. ``registration.html`` and
``registration.txt`` have been added and contain the content that is sent to a client upon
registering via an email address.
``registration_success.html`` and ``registration_failure.html`` are also new HTML templates
that will be shown to the user when they click the link in their registration emai , either
showing them a success or failure page (assuming a redirect URL is not configured).
Synapse will expect these files to exist inside the configured template directory. To view the
default templates, see `synapse/res/templates
<https://github.com/matrix-org/synapse/tree/master/synapse/res/templates>`_.
Upgrading to v1.2.0 Upgrading to v1.2.0
=================== ===================
@ -132,6 +182,19 @@ server for password resets, set ``trust_identity_server_for_password_resets`` to
See the `sample configuration file <docs/sample_config.yaml>`_ See the `sample configuration file <docs/sample_config.yaml>`_
for more details on these settings. for more details on these settings.
New email templates
---------------
Some new templates have been added to the default template directory for the purpose of the
homeserver sending its own password reset emails. If you have configured a custom
``template_dir`` in your Synapse config, these files will need to be added.
``password_reset.html`` and ``password_reset.txt`` are HTML and plain text templates
respectively that contain the contents of what will be emailed to the user upon attempting to
reset their password via email. ``password_reset_success.html`` and
``password_reset_failure.html`` are HTML files that the content of which (assuming no redirect
URL is set) will be shown to the user after they attempt to click the link in the email sent
to them.
Upgrading to v0.99.0 Upgrading to v0.99.0
==================== ====================

1
changelog.d/5835.feature Normal file
View file

@ -0,0 +1 @@
Add the ability to send registration emails from the homeserver rather than delegating to an identity server.

1
changelog.d/5868.feature Normal file
View file

@ -0,0 +1 @@
Add `m.require_identity_server` key to `/versions`'s `unstable_features` section.

1
changelog.d/5875.misc Normal file
View file

@ -0,0 +1 @@
Deprecate the `trusted_third_party_id_servers` option.

1
changelog.d/5876.feature Normal file
View file

@ -0,0 +1 @@
Replace `trust_identity_server_for_password_resets` config option with `account_threepid_delegates`.

1
changelog.d/5892.misc Normal file
View file

@ -0,0 +1 @@
Compatibility with v2 Identity Service APIs other than /lookup.

1
changelog.d/5940.feature Normal file
View file

@ -0,0 +1 @@
Add the ability to send registration emails from the homeserver rather than delegating to an identity server.

1
changelog.d/5969.feature Normal file
View file

@ -0,0 +1 @@
Replace `trust_identity_server_for_password_resets` config option with `account_threepid_delegates`.

1
changelog.d/5980.feature Normal file
View file

@ -0,0 +1 @@
Add POST /_matrix/client/r0/account/3pid/unbind endpoint from MSC2140 for unbinding a 3PID from an identity server without removing it from the homeserver user account.

1
changelog.d/5981.feature Normal file
View file

@ -0,0 +1 @@
Setting metrics_flags.known_servers to True in the configuration will publish the synapse_federation_known_servers metric over Prometheus. This represents the total number of servers your server knows about (i.e. is in rooms with), including itself.

1
changelog.d/5982.bugfix Normal file
View file

@ -0,0 +1 @@
Include missing opentracing contexts in outbout replication requests.

1
changelog.d/5983.feature Normal file
View file

@ -0,0 +1 @@
Add minimum opentracing for client servlets.

1
changelog.d/5984.bugfix Normal file
View file

@ -0,0 +1 @@
Fix sending of EDUs when opentracing is enabled with an empty whitelist.

1
changelog.d/5986.feature Normal file
View file

@ -0,0 +1 @@
Trace replication send times.

1
changelog.d/5988.bugfix Normal file
View file

@ -0,0 +1 @@
Fix invalid references to None while opentracing if the log context slips.

1
changelog.d/5991.bugfix Normal file
View file

@ -0,0 +1 @@
Fix invalid references to None while opentracing if the log context slips.

1
changelog.d/5993.feature Normal file
View file

@ -0,0 +1 @@
Add the ability to send registration emails from the homeserver rather than delegating to an identity server.

1
changelog.d/5994.feature Normal file
View file

@ -0,0 +1 @@
Add the ability to send registration emails from the homeserver rather than delegating to an identity server.

1
changelog.d/5995.bugfix Normal file
View file

@ -0,0 +1 @@
Return a M_MISSING_PARAM if `sid` is not provided to `/account/3pid`.

1
changelog.d/5998.bugfix Normal file
View file

@ -0,0 +1 @@
Fix room and user stats tracking.

View file

@ -37,6 +37,8 @@ from signedjson.sign import verify_signed_json, SignatureVerifyException
CONFIG_JSON = "cmdclient_config.json" CONFIG_JSON = "cmdclient_config.json"
# TODO: The concept of trusted identity servers has been deprecated. This option and checks
# should be removed
TRUSTED_ID_SERVERS = ["localhost:8001"] TRUSTED_ID_SERVERS = ["localhost:8001"]
@ -268,6 +270,7 @@ class SynapseCmd(cmd.Cmd):
@defer.inlineCallbacks @defer.inlineCallbacks
def _do_emailrequest(self, args): def _do_emailrequest(self, args):
# TODO: Update to use v2 Identity Service API endpoint
url = ( url = (
self._identityServerUrl() self._identityServerUrl()
+ "/_matrix/identity/api/v1/validate/email/requestToken" + "/_matrix/identity/api/v1/validate/email/requestToken"
@ -302,6 +305,7 @@ class SynapseCmd(cmd.Cmd):
@defer.inlineCallbacks @defer.inlineCallbacks
def _do_emailvalidate(self, args): def _do_emailvalidate(self, args):
# TODO: Update to use v2 Identity Service API endpoint
url = ( url = (
self._identityServerUrl() self._identityServerUrl()
+ "/_matrix/identity/api/v1/validate/email/submitToken" + "/_matrix/identity/api/v1/validate/email/submitToken"
@ -330,6 +334,7 @@ class SynapseCmd(cmd.Cmd):
@defer.inlineCallbacks @defer.inlineCallbacks
def _do_3pidbind(self, args): def _do_3pidbind(self, args):
# TODO: Update to use v2 Identity Service API endpoint
url = self._identityServerUrl() + "/_matrix/identity/api/v1/3pid/bind" url = self._identityServerUrl() + "/_matrix/identity/api/v1/3pid/bind"
json_res = yield self.http_client.do_request( json_res = yield self.http_client.do_request(
@ -398,6 +403,7 @@ class SynapseCmd(cmd.Cmd):
@defer.inlineCallbacks @defer.inlineCallbacks
def _do_invite(self, roomid, userstring): def _do_invite(self, roomid, userstring):
if not userstring.startswith("@") and self._is_on("complete_usernames"): if not userstring.startswith("@") and self._is_on("complete_usernames"):
# TODO: Update to use v2 Identity Service API endpoint
url = self._identityServerUrl() + "/_matrix/identity/api/v1/lookup" url = self._identityServerUrl() + "/_matrix/identity/api/v1/lookup"
json_res = yield self.http_client.do_request( json_res = yield self.http_client.do_request(
@ -407,6 +413,7 @@ class SynapseCmd(cmd.Cmd):
mxid = None mxid = None
if "mxid" in json_res and "signatures" in json_res: if "mxid" in json_res and "signatures" in json_res:
# TODO: Update to use v2 Identity Service API endpoint
url = ( url = (
self._identityServerUrl() self._identityServerUrl()
+ "/_matrix/identity/api/v1/pubkey/ed25519" + "/_matrix/identity/api/v1/pubkey/ed25519"

View file

@ -891,10 +891,42 @@ uploads_path: "DATADIR/uploads"
# Also defines the ID server which will be called when an account is # Also defines the ID server which will be called when an account is
# deactivated (one will be picked arbitrarily). # deactivated (one will be picked arbitrarily).
# #
# Note: This option is deprecated. Since v0.99.4, Synapse has tracked which identity
# server a 3PID has been bound to. For 3PIDs bound before then, Synapse runs a
# background migration script, informing itself that the identity server all of its
# 3PIDs have been bound to is likely one of the below.
#
# As of Synapse v1.4.0, all other functionality of this option has been deprecated, and
# it is now solely used for the purposes of the background migration script, and can be
# removed once it has run.
#trusted_third_party_id_servers: #trusted_third_party_id_servers:
# - matrix.org # - matrix.org
# - vector.im # - vector.im
# Handle threepid (email/phone etc) registration and password resets through a set of
# *trusted* identity servers. Note that this allows the configured identity server to
# reset passwords for accounts!
#
# Be aware that if `email` is not set, and SMTP options have not been
# configured in the email config block, registration and user password resets via
# email will be globally disabled.
#
# Additionally, if `msisdn` is not set, registration and password resets via msisdn
# will be disabled regardless. This is due to Synapse currently not supporting any
# method of sending SMS messages on its own.
#
# To enable using an identity server for operations regarding a particular third-party
# identifier type, set the value to the URL of that identity server as shown in the
# examples below.
#
# Servers handling the these requests must answer the `/requestToken` endpoints defined
# by the Matrix Identity Service API specification:
# https://matrix.org/docs/spec/identity_service/latest
#
account_threepid_delegates:
#email: https://example.com # Delegate email sending to matrix.org
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process
# Users who register on this homeserver will automatically be joined # Users who register on this homeserver will automatically be joined
# to these rooms # to these rooms
# #
@ -926,6 +958,16 @@ uploads_path: "DATADIR/uploads"
#sentry: #sentry:
# dsn: "..." # dsn: "..."
# Flags to enable Prometheus metrics which are not suitable to be
# enabled by default, either for performance reasons or limited use.
#
metrics_flags:
# Publish synapse_federation_known_servers, a g auge of the number of
# servers this homeserver knows about, including itself. May cause
# performance problems on large homeservers.
#
#known_servers: true
# Whether or not to report anonymized homeserver usage statistics. # Whether or not to report anonymized homeserver usage statistics.
# report_stats: true|false # report_stats: true|false
@ -1164,19 +1206,6 @@ password_config:
# # # #
# riot_base_url: "http://localhost/riot" # riot_base_url: "http://localhost/riot"
# #
# # Enable sending password reset emails via the configured, trusted
# # identity servers
# #
# # IMPORTANT! This will give a malicious or overtaken identity server
# # the ability to reset passwords for your users! Make absolutely sure
# # that you want to do this! It is strongly recommended that password
# # reset emails be sent by the homeserver instead
# #
# # If this option is set to false and SMTP options have not been
# # configured, resetting user passwords via email will be disabled
# #
# #trust_identity_server_for_password_resets: false
#
# # Configure the time that a validation email or text message code # # Configure the time that a validation email or text message code
# # will expire after sending # # will expire after sending
# # # #
@ -1208,11 +1237,22 @@ password_config:
# #password_reset_template_html: password_reset.html # #password_reset_template_html: password_reset.html
# #password_reset_template_text: password_reset.txt # #password_reset_template_text: password_reset.txt
# #
# # Templates for registration emails sent by the homeserver
# #
# #registration_template_html: registration.html
# #registration_template_text: registration.txt
#
# # Templates for password reset success and failure pages that a user # # Templates for password reset success and failure pages that a user
# # will see after attempting to reset their password # # will see after attempting to reset their password
# # # #
# #password_reset_template_success_html: password_reset_success.html # #password_reset_template_success_html: password_reset_success.html
# #password_reset_template_failure_html: password_reset_failure.html # #password_reset_template_failure_html: password_reset_failure.html
#
# # Templates for registration success and failure pages that a user
# # will see after attempting to register using an email or phone
# #
# #registration_template_success_html: registration_success.html
# #registration_template_failure_html: registration_failure.html
#password_providers: #password_providers:

View file

@ -119,7 +119,7 @@ class ClientReaderServer(HomeServer):
KeyChangesServlet(self).register(resource) KeyChangesServlet(self).register(resource)
VoipRestServlet(self).register(resource) VoipRestServlet(self).register(resource)
PushRuleRestServlet(self).register(resource) PushRuleRestServlet(self).register(resource)
VersionsRestServlet().register(resource) VersionsRestServlet(self).register(resource)
resources.update({"/_matrix/client": resource}) resources.update({"/_matrix/client": resource})

View file

@ -20,6 +20,7 @@ from __future__ import print_function
# This file can't be called email.py because if it is, we cannot: # This file can't be called email.py because if it is, we cannot:
import email.utils import email.utils
import os import os
from enum import Enum
import pkg_resources import pkg_resources
@ -74,19 +75,48 @@ class EmailConfig(Config):
"renew_at" "renew_at"
) )
email_trust_identity_server_for_password_resets = email_config.get( self.threepid_behaviour_email = (
"trust_identity_server_for_password_resets", False # Have Synapse handle the email sending if account_threepid_delegates.email
# is not defined
# msisdn is currently always remote while Synapse does not support any method of
# sending SMS messages
ThreepidBehaviour.REMOTE
if self.account_threepid_delegate_email
else ThreepidBehaviour.LOCAL
) )
self.email_password_reset_behaviour = ( # Prior to Synapse v1.4.0, there was another option that defined whether Synapse would
"remote" if email_trust_identity_server_for_password_resets else "local" # use an identity server to password reset tokens on its behalf. We now warn the user
) # if they have this set and tell them to use the updated option, while using a default
self.password_resets_were_disabled_due_to_email_config = False # identity server in the process.
if self.email_password_reset_behaviour == "local" and email_config == {}: self.using_identity_server_from_trusted_list = False
if (
not self.account_threepid_delegate_email
and config.get("trust_identity_server_for_password_resets", False) is True
):
# Use the first entry in self.trusted_third_party_id_servers instead
if self.trusted_third_party_id_servers:
# XXX: It's a little confusing that account_threepid_delegate_email is modified
# both in RegistrationConfig and here. We should factor this bit out
self.account_threepid_delegate_email = self.trusted_third_party_id_servers[
0
]
self.using_identity_server_from_trusted_list = True
else:
raise ConfigError(
"Attempted to use an identity server from"
'"trusted_third_party_id_servers" but it is empty.'
)
self.local_threepid_handling_disabled_due_to_email_config = False
if (
self.threepid_behaviour_email == ThreepidBehaviour.LOCAL
and email_config == {}
):
# We cannot warn the user this has happened here # We cannot warn the user this has happened here
# Instead do so when a user attempts to reset their password # Instead do so when a user attempts to reset their password
self.password_resets_were_disabled_due_to_email_config = True self.local_threepid_handling_disabled_due_to_email_config = True
self.email_password_reset_behaviour = "off" self.threepid_behaviour_email = ThreepidBehaviour.OFF
# Get lifetime of a validation token in milliseconds # Get lifetime of a validation token in milliseconds
self.email_validation_token_lifetime = self.parse_duration( self.email_validation_token_lifetime = self.parse_duration(
@ -96,7 +126,7 @@ class EmailConfig(Config):
if ( if (
self.email_enable_notifs self.email_enable_notifs
or account_validity_renewal_enabled or account_validity_renewal_enabled
or self.email_password_reset_behaviour == "local" or self.threepid_behaviour_email == ThreepidBehaviour.LOCAL
): ):
# make sure we can import the required deps # make sure we can import the required deps
import jinja2 import jinja2
@ -106,7 +136,7 @@ class EmailConfig(Config):
jinja2 jinja2
bleach bleach
if self.email_password_reset_behaviour == "local": if self.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
required = ["smtp_host", "smtp_port", "notif_from"] required = ["smtp_host", "smtp_port", "notif_from"]
missing = [] missing = []
@ -125,28 +155,45 @@ class EmailConfig(Config):
% (", ".join(missing),) % (", ".join(missing),)
) )
# Templates for password reset emails # These email templates have placeholders in them, and thus must be
# parsed using a templating engine during a request
self.email_password_reset_template_html = email_config.get( self.email_password_reset_template_html = email_config.get(
"password_reset_template_html", "password_reset.html" "password_reset_template_html", "password_reset.html"
) )
self.email_password_reset_template_text = email_config.get( self.email_password_reset_template_text = email_config.get(
"password_reset_template_text", "password_reset.txt" "password_reset_template_text", "password_reset.txt"
) )
self.email_registration_template_html = email_config.get(
"registration_template_html", "registration.html"
)
self.email_registration_template_text = email_config.get(
"registration_template_text", "registration.txt"
)
self.email_password_reset_template_failure_html = email_config.get( self.email_password_reset_template_failure_html = email_config.get(
"password_reset_template_failure_html", "password_reset_failure.html" "password_reset_template_failure_html", "password_reset_failure.html"
) )
# This template does not support any replaceable variables, so we will self.email_registration_template_failure_html = email_config.get(
# read it from the disk once during setup "registration_template_failure_html", "registration_failure.html"
)
# These templates do not support any placeholder variables, so we
# will read them from disk once during setup
email_password_reset_template_success_html = email_config.get( email_password_reset_template_success_html = email_config.get(
"password_reset_template_success_html", "password_reset_success.html" "password_reset_template_success_html", "password_reset_success.html"
) )
email_registration_template_success_html = email_config.get(
"registration_template_success_html", "registration_success.html"
)
# Check templates exist # Check templates exist
for f in [ for f in [
self.email_password_reset_template_html, self.email_password_reset_template_html,
self.email_password_reset_template_text, self.email_password_reset_template_text,
self.email_registration_template_html,
self.email_registration_template_text,
self.email_password_reset_template_failure_html, self.email_password_reset_template_failure_html,
email_password_reset_template_success_html, email_password_reset_template_success_html,
email_registration_template_success_html,
]: ]:
p = os.path.join(self.email_template_dir, f) p = os.path.join(self.email_template_dir, f)
if not os.path.isfile(p): if not os.path.isfile(p):
@ -156,9 +203,15 @@ class EmailConfig(Config):
filepath = os.path.join( filepath = os.path.join(
self.email_template_dir, email_password_reset_template_success_html self.email_template_dir, email_password_reset_template_success_html
) )
self.email_password_reset_template_success_html_content = self.read_file( self.email_password_reset_template_success_html = self.read_file(
filepath, "email.password_reset_template_success_html" filepath, "email.password_reset_template_success_html"
) )
filepath = os.path.join(
self.email_template_dir, email_registration_template_success_html
)
self.email_registration_template_success_html_content = self.read_file(
filepath, "email.registration_template_success_html"
)
if self.email_enable_notifs: if self.email_enable_notifs:
required = [ required = [
@ -239,19 +292,6 @@ class EmailConfig(Config):
# # # #
# riot_base_url: "http://localhost/riot" # riot_base_url: "http://localhost/riot"
# #
# # Enable sending password reset emails via the configured, trusted
# # identity servers
# #
# # IMPORTANT! This will give a malicious or overtaken identity server
# # the ability to reset passwords for your users! Make absolutely sure
# # that you want to do this! It is strongly recommended that password
# # reset emails be sent by the homeserver instead
# #
# # If this option is set to false and SMTP options have not been
# # configured, resetting user passwords via email will be disabled
# #
# #trust_identity_server_for_password_resets: false
#
# # Configure the time that a validation email or text message code # # Configure the time that a validation email or text message code
# # will expire after sending # # will expire after sending
# # # #
@ -283,9 +323,35 @@ class EmailConfig(Config):
# #password_reset_template_html: password_reset.html # #password_reset_template_html: password_reset.html
# #password_reset_template_text: password_reset.txt # #password_reset_template_text: password_reset.txt
# #
# # Templates for registration emails sent by the homeserver
# #
# #registration_template_html: registration.html
# #registration_template_text: registration.txt
#
# # Templates for password reset success and failure pages that a user # # Templates for password reset success and failure pages that a user
# # will see after attempting to reset their password # # will see after attempting to reset their password
# # # #
# #password_reset_template_success_html: password_reset_success.html # #password_reset_template_success_html: password_reset_success.html
# #password_reset_template_failure_html: password_reset_failure.html # #password_reset_template_failure_html: password_reset_failure.html
#
# # Templates for registration success and failure pages that a user
# # will see after attempting to register using an email or phone
# #
# #registration_template_success_html: registration_success.html
# #registration_template_failure_html: registration_failure.html
""" """
class ThreepidBehaviour(Enum):
"""
Enum to define the behaviour of Synapse with regards to when it contacts an identity
server for 3pid registration and password resets
REMOTE = use an external server to send tokens
LOCAL = send tokens ourselves
OFF = disable registration via 3pid and password resets
"""
REMOTE = "remote"
LOCAL = "local"
OFF = "off"

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2015, 2016 OpenMarket Ltd # Copyright 2015, 2016 OpenMarket Ltd
# Copyright 2019 The Matrix.org Foundation C.I.C.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -13,6 +14,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import attr
from ._base import Config, ConfigError from ._base import Config, ConfigError
MISSING_SENTRY = """Missing sentry-sdk library. This is required to enable sentry MISSING_SENTRY = """Missing sentry-sdk library. This is required to enable sentry
@ -20,6 +23,18 @@ MISSING_SENTRY = """Missing sentry-sdk library. This is required to enable sentr
""" """
@attr.s
class MetricsFlags(object):
known_servers = attr.ib(default=False, validator=attr.validators.instance_of(bool))
@classmethod
def all_off(cls):
"""
Instantiate the flags with all options set to off.
"""
return cls(**{x.name: False for x in attr.fields(cls)})
class MetricsConfig(Config): class MetricsConfig(Config):
def read_config(self, config, **kwargs): def read_config(self, config, **kwargs):
self.enable_metrics = config.get("enable_metrics", False) self.enable_metrics = config.get("enable_metrics", False)
@ -27,6 +42,12 @@ class MetricsConfig(Config):
self.metrics_port = config.get("metrics_port") self.metrics_port = config.get("metrics_port")
self.metrics_bind_host = config.get("metrics_bind_host", "127.0.0.1") self.metrics_bind_host = config.get("metrics_bind_host", "127.0.0.1")
if self.enable_metrics:
_metrics_config = config.get("metrics_flags") or {}
self.metrics_flags = MetricsFlags(**_metrics_config)
else:
self.metrics_flags = MetricsFlags.all_off()
self.sentry_enabled = "sentry" in config self.sentry_enabled = "sentry" in config
if self.sentry_enabled: if self.sentry_enabled:
try: try:
@ -58,6 +79,16 @@ class MetricsConfig(Config):
#sentry: #sentry:
# dsn: "..." # dsn: "..."
# Flags to enable Prometheus metrics which are not suitable to be
# enabled by default, either for performance reasons or limited use.
#
metrics_flags:
# Publish synapse_federation_known_servers, a g auge of the number of
# servers this homeserver knows about, including itself. May cause
# performance problems on large homeservers.
#
#known_servers: true
# Whether or not to report anonymized homeserver usage statistics. # Whether or not to report anonymized homeserver usage statistics.
""" """

View file

@ -99,6 +99,10 @@ class RegistrationConfig(Config):
self.trusted_third_party_id_servers = config.get( self.trusted_third_party_id_servers = config.get(
"trusted_third_party_id_servers", ["matrix.org", "vector.im"] "trusted_third_party_id_servers", ["matrix.org", "vector.im"]
) )
account_threepid_delegates = config.get("account_threepid_delegates") or {}
self.account_threepid_delegate_email = account_threepid_delegates.get("email")
self.account_threepid_delegate_msisdn = account_threepid_delegates.get("msisdn")
self.default_identity_server = config.get("default_identity_server") self.default_identity_server = config.get("default_identity_server")
self.allow_guest_access = config.get("allow_guest_access", False) self.allow_guest_access = config.get("allow_guest_access", False)
@ -257,10 +261,42 @@ class RegistrationConfig(Config):
# Also defines the ID server which will be called when an account is # Also defines the ID server which will be called when an account is
# deactivated (one will be picked arbitrarily). # deactivated (one will be picked arbitrarily).
# #
# Note: This option is deprecated. Since v0.99.4, Synapse has tracked which identity
# server a 3PID has been bound to. For 3PIDs bound before then, Synapse runs a
# background migration script, informing itself that the identity server all of its
# 3PIDs have been bound to is likely one of the below.
#
# As of Synapse v1.4.0, all other functionality of this option has been deprecated, and
# it is now solely used for the purposes of the background migration script, and can be
# removed once it has run.
#trusted_third_party_id_servers: #trusted_third_party_id_servers:
# - matrix.org # - matrix.org
# - vector.im # - vector.im
# Handle threepid (email/phone etc) registration and password resets through a set of
# *trusted* identity servers. Note that this allows the configured identity server to
# reset passwords for accounts!
#
# Be aware that if `email` is not set, and SMTP options have not been
# configured in the email config block, registration and user password resets via
# email will be globally disabled.
#
# Additionally, if `msisdn` is not set, registration and password resets via msisdn
# will be disabled regardless. This is due to Synapse currently not supporting any
# method of sending SMS messages on its own.
#
# To enable using an identity server for operations regarding a particular third-party
# identifier type, set the value to the URL of that identity server as shown in the
# examples below.
#
# Servers handling the these requests must answer the `/requestToken` endpoints defined
# by the Matrix Identity Service API specification:
# https://matrix.org/docs/spec/identity_service/latest
#
account_threepid_delegates:
#email: https://example.com # Delegate email sending to matrix.org
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process
# Users who register on this homeserver will automatically be joined # Users who register on this homeserver will automatically be joined
# to these rooms # to these rooms
# #

View file

@ -26,6 +26,7 @@ from synapse.logging.opentracing import (
set_tag, set_tag,
start_active_span_follows_from, start_active_span_follows_from,
tags, tags,
whitelisted_homeserver,
) )
from synapse.util.metrics import measure_func from synapse.util.metrics import measure_func
@ -59,9 +60,15 @@ class TransactionManager(object):
# The span_contexts is a generator so that it won't be evaluated if # The span_contexts is a generator so that it won't be evaluated if
# opentracing is disabled. (Yay speed!) # opentracing is disabled. (Yay speed!)
span_contexts = ( span_contexts = []
extract_text_map(json.loads(edu.get_context())) for edu in pending_edus keep_destination = whitelisted_homeserver(destination)
)
for edu in pending_edus:
context = edu.get_context()
if context:
span_contexts.append(extract_text_map(json.loads(context)))
if keep_destination:
edu.strip_context()
with start_active_span_follows_from("send_transaction", span_contexts): with start_active_span_follows_from("send_transaction", span_contexts):

View file

@ -342,7 +342,11 @@ class BaseFederationServlet(object):
continue continue
server.register_paths( server.register_paths(
method, (pattern,), self._wrap(code), self.__class__.__name__ method,
(pattern,),
self._wrap(code),
self.__class__.__name__,
trace=False,
) )

View file

@ -41,6 +41,9 @@ class Edu(JsonEncodedObject):
def get_context(self): def get_context(self):
return getattr(self, "content", {}).get("org.matrix.opentracing_context", "{}") return getattr(self, "content", {}).get("org.matrix.opentracing_context", "{}")
def strip_context(self):
getattr(self, "content", {})["org.matrix.opentracing_context"] = "{}"
class Transaction(JsonEncodedObject): class Transaction(JsonEncodedObject):
""" A transaction is a list of Pdus and Edus to be sent to a remote home """ A transaction is a list of Pdus and Edus to be sent to a remote home

View file

@ -38,6 +38,7 @@ logger = logging.getLogger(__name__)
class AccountValidityHandler(object): class AccountValidityHandler(object):
def __init__(self, hs): def __init__(self, hs):
self.hs = hs self.hs = hs
self.config = hs.config
self.store = self.hs.get_datastore() self.store = self.hs.get_datastore()
self.sendmail = self.hs.get_sendmail() self.sendmail = self.hs.get_sendmail()
self.clock = self.hs.get_clock() self.clock = self.hs.get_clock()
@ -62,9 +63,14 @@ class AccountValidityHandler(object):
self._raw_from = email.utils.parseaddr(self._from_string)[1] self._raw_from = email.utils.parseaddr(self._from_string)[1]
self._template_html, self._template_text = load_jinja2_templates( self._template_html, self._template_text = load_jinja2_templates(
config=self.hs.config, self.config.email_template_dir,
template_html_name=self.hs.config.email_expiry_template_html, [
template_text_name=self.hs.config.email_expiry_template_text, self.config.email_expiry_template_html,
self.config.email_expiry_template_text,
],
apply_format_ts_filter=True,
apply_mxc_to_http_filter=True,
public_baseurl=self.config.public_baseurl,
) )
# Check the renewal emails to send and send them every 30min. # Check the renewal emails to send and send them every 30min.

View file

@ -38,6 +38,7 @@ from synapse.api.errors import (
UserDeactivatedError, UserDeactivatedError,
) )
from synapse.api.ratelimiting import Ratelimiter from synapse.api.ratelimiting import Ratelimiter
from synapse.config.emailconfig import ThreepidBehaviour
from synapse.logging.context import defer_to_thread from synapse.logging.context import defer_to_thread
from synapse.module_api import ModuleApi from synapse.module_api import ModuleApi
from synapse.types import UserID from synapse.types import UserID
@ -158,7 +159,7 @@ class AuthHandler(BaseHandler):
return params return params
@defer.inlineCallbacks @defer.inlineCallbacks
def check_auth(self, flows, clientdict, clientip, password_servlet=False): def check_auth(self, flows, clientdict, clientip):
""" """
Takes a dictionary sent by the client in the login / registration Takes a dictionary sent by the client in the login / registration
protocol and handles the User-Interactive Auth flow. protocol and handles the User-Interactive Auth flow.
@ -182,16 +183,6 @@ class AuthHandler(BaseHandler):
clientip (str): The IP address of the client. clientip (str): The IP address of the client.
password_servlet (bool): Whether the request originated from
PasswordRestServlet.
XXX: This is a temporary hack to distinguish between checking
for threepid validations locally (in the case of password
resets) and using the identity server (in the case of binding
a 3PID during registration). Once we start using the
homeserver for both tasks, this distinction will no longer be
necessary.
Returns: Returns:
defer.Deferred[dict, dict, str]: a deferred tuple of defer.Deferred[dict, dict, str]: a deferred tuple of
(creds, params, session_id). (creds, params, session_id).
@ -247,9 +238,7 @@ class AuthHandler(BaseHandler):
if "type" in authdict: if "type" in authdict:
login_type = authdict["type"] login_type = authdict["type"]
try: try:
result = yield self._check_auth_dict( result = yield self._check_auth_dict(authdict, clientip)
authdict, clientip, password_servlet=password_servlet
)
if result: if result:
creds[login_type] = result creds[login_type] = result
self._save_session(session) self._save_session(session)
@ -356,7 +345,7 @@ class AuthHandler(BaseHandler):
return sess.setdefault("serverdict", {}).get(key, default) return sess.setdefault("serverdict", {}).get(key, default)
@defer.inlineCallbacks @defer.inlineCallbacks
def _check_auth_dict(self, authdict, clientip, password_servlet=False): def _check_auth_dict(self, authdict, clientip):
"""Attempt to validate the auth dict provided by a client """Attempt to validate the auth dict provided by a client
Args: Args:
@ -374,11 +363,7 @@ class AuthHandler(BaseHandler):
login_type = authdict["type"] login_type = authdict["type"]
checker = self.checkers.get(login_type) checker = self.checkers.get(login_type)
if checker is not None: if checker is not None:
# XXX: Temporary workaround for having Synapse handle password resets res = yield checker(authdict, clientip=clientip)
# See AuthHandler.check_auth for further details
res = yield checker(
authdict, clientip=clientip, password_servlet=password_servlet
)
return res return res
# build a v1-login-style dict out of the authdict and fall back to the # build a v1-login-style dict out of the authdict and fall back to the
@ -449,7 +434,7 @@ class AuthHandler(BaseHandler):
return defer.succeed(True) return defer.succeed(True)
@defer.inlineCallbacks @defer.inlineCallbacks
def _check_threepid(self, medium, authdict, password_servlet=False, **kwargs): def _check_threepid(self, medium, authdict, **kwargs):
if "threepid_creds" not in authdict: if "threepid_creds" not in authdict:
raise LoginError(400, "Missing threepid_creds", Codes.MISSING_PARAM) raise LoginError(400, "Missing threepid_creds", Codes.MISSING_PARAM)
@ -458,12 +443,9 @@ class AuthHandler(BaseHandler):
identity_handler = self.hs.get_handlers().identity_handler identity_handler = self.hs.get_handlers().identity_handler
logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,)) logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,))
if ( if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
not password_servlet
or self.hs.config.email_password_reset_behaviour == "remote"
):
threepid = yield identity_handler.threepid_from_creds(threepid_creds) threepid = yield identity_handler.threepid_from_creds(threepid_creds)
elif self.hs.config.email_password_reset_behaviour == "local": elif self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
row = yield self.store.get_threepid_validation_session( row = yield self.store.get_threepid_validation_session(
medium, medium,
threepid_creds["client_secret"], threepid_creds["client_secret"],

View file

@ -25,7 +25,6 @@ from synapse.logging.opentracing import (
log_kv, log_kv,
set_tag, set_tag,
start_active_span, start_active_span,
whitelisted_homeserver,
) )
from synapse.types import UserID, get_domain_from_id from synapse.types import UserID, get_domain_from_id
from synapse.util.stringutils import random_string from synapse.util.stringutils import random_string
@ -121,9 +120,7 @@ class DeviceMessageHandler(object):
"sender": sender_user_id, "sender": sender_user_id,
"type": message_type, "type": message_type,
"message_id": message_id, "message_id": message_id,
"org.matrix.opentracing_context": json.dumps(context) "org.matrix.opentracing_context": json.dumps(context),
if whitelisted_homeserver(destination)
else None,
} }
log_kv({"local_messages": local_messages}) log_kv({"local_messages": local_messages})

View file

@ -29,6 +29,7 @@ from synapse.api.errors import (
HttpResponseException, HttpResponseException,
SynapseError, SynapseError,
) )
from synapse.util.stringutils import random_string
from ._base import BaseHandler from ._base import BaseHandler
@ -41,87 +42,135 @@ class IdentityHandler(BaseHandler):
self.http_client = hs.get_simple_http_client() self.http_client = hs.get_simple_http_client()
self.federation_http_client = hs.get_http_client() self.federation_http_client = hs.get_http_client()
self.hs = hs
self.trusted_id_servers = set(hs.config.trusted_third_party_id_servers) def _extract_items_from_creds_dict(self, creds):
self.trust_any_id_server_just_for_testing_do_not_use = ( """
hs.config.use_insecure_ssl_client_just_for_testing_do_not_use Retrieve entries from a "credentials" dictionary
Args:
creds (dict[str, str]): Dictionary of credentials that contain the following keys:
* client_secret|clientSecret: A unique secret str provided by the client
* id_server|idServer: the domain of the identity server to query
* id_access_token: The access token to authenticate to the identity
server with.
Returns:
tuple(str, str, str|None): A tuple containing the client_secret, the id_server,
and the id_access_token value if available.
"""
client_secret = creds.get("client_secret") or creds.get("clientSecret")
if not client_secret:
raise SynapseError(
400, "No client_secret in creds", errcode=Codes.MISSING_PARAM
)
id_server = creds.get("id_server") or creds.get("idServer")
if not id_server:
raise SynapseError(
400, "No id_server in creds", errcode=Codes.MISSING_PARAM
)
id_access_token = creds.get("id_access_token")
return client_secret, id_server, id_access_token
@defer.inlineCallbacks
def threepid_from_creds(self, creds, use_v2=True):
"""
Retrieve and validate a threepid identitier from a "credentials" dictionary
Args:
creds (dict[str, str]): Dictionary of credentials that contain the following keys:
* client_secret|clientSecret: A unique secret str provided by the client
* id_server|idServer: the domain of the identity server to query
* id_access_token: The access token to authenticate to the identity
server with. Required if use_v2 is true
use_v2 (bool): Whether to use v2 Identity Service API endpoints
Returns:
Deferred[dict[str,str|int]|None]: A dictionary consisting of response params to
the /getValidated3pid endpoint of the Identity Service API, or None if the
threepid was not found
"""
client_secret, id_server, id_access_token = self._extract_items_from_creds_dict(
creds
) )
def _should_trust_id_server(self, id_server): # If an id_access_token is not supplied, force usage of v1
if id_server not in self.trusted_id_servers: if id_access_token is None:
if self.trust_any_id_server_just_for_testing_do_not_use: use_v2 = False
logger.warn(
"Trusting untrustworthy ID server %r even though it isn't"
" in the trusted id list for testing because"
" 'use_insecure_ssl_client_just_for_testing_do_not_use'"
" is set in the config",
id_server,
)
else:
return False
return True
@defer.inlineCallbacks query_params = {"sid": creds["sid"], "client_secret": client_secret}
def threepid_from_creds(self, creds):
if "id_server" in creds:
id_server = creds["id_server"]
elif "idServer" in creds:
id_server = creds["idServer"]
else:
raise SynapseError(400, "No id_server in creds")
if "client_secret" in creds: # Decide which API endpoint URLs and query parameters to use
client_secret = creds["client_secret"] if use_v2:
elif "clientSecret" in creds: url = "https://%s%s" % (
client_secret = creds["clientSecret"]
else:
raise SynapseError(400, "No client_secret in creds")
if not self._should_trust_id_server(id_server):
logger.warn(
"%s is not a trusted ID server: rejecting 3pid " + "credentials",
id_server, id_server,
"/_matrix/identity/v2/3pid/getValidated3pid",
)
query_params["id_access_token"] = id_access_token
else:
url = "https://%s%s" % (
id_server,
"/_matrix/identity/api/v1/3pid/getValidated3pid",
) )
return None
try: try:
data = yield self.http_client.get_json( data = yield self.http_client.get_json(url, query_params)
"https://%s%s" return data if "medium" in data else None
% (id_server, "/_matrix/identity/api/v1/3pid/getValidated3pid"),
{"sid": creds["sid"], "client_secret": client_secret},
)
except HttpResponseException as e: except HttpResponseException as e:
logger.info("getValidated3pid failed with Matrix error: %r", e) if e.code != 404 or not use_v2:
raise e.to_synapse_error() # Generic failure
logger.info("getValidated3pid failed with Matrix error: %r", e)
raise e.to_synapse_error()
if "medium" in data: # This identity server is too old to understand Identity Service API v2
return data # Attempt v1 endpoint
return None logger.info("Got 404 when POSTing JSON %s, falling back to v1 URL", url)
return (yield self.threepid_from_creds(creds, use_v2=False))
@defer.inlineCallbacks @defer.inlineCallbacks
def bind_threepid(self, creds, mxid): def bind_threepid(self, creds, mxid, use_v2=True):
"""Bind a 3PID to an identity server
Args:
creds (dict[str, str]): Dictionary of credentials that contain the following keys:
* client_secret|clientSecret: A unique secret str provided by the client
* id_server|idServer: the domain of the identity server to query
* id_access_token: The access token to authenticate to the identity
server with. Required if use_v2 is true
mxid (str): The MXID to bind the 3PID to
use_v2 (bool): Whether to use v2 Identity Service API endpoints
Returns:
Deferred[dict]: The response from the identity server
"""
logger.debug("binding threepid %r to %s", creds, mxid) logger.debug("binding threepid %r to %s", creds, mxid)
data = None
if "id_server" in creds: client_secret, id_server, id_access_token = self._extract_items_from_creds_dict(
id_server = creds["id_server"] creds
elif "idServer" in creds: )
id_server = creds["idServer"]
else:
raise SynapseError(400, "No id_server in creds")
if "client_secret" in creds: sid = creds.get("sid")
client_secret = creds["client_secret"] if not sid:
elif "clientSecret" in creds: raise SynapseError(
client_secret = creds["clientSecret"] 400, "No sid in three_pid_creds", errcode=Codes.MISSING_PARAM
)
# If an id_access_token is not supplied, force usage of v1
if id_access_token is None:
use_v2 = False
# Decide which API endpoint URLs to use
bind_data = {"sid": sid, "client_secret": client_secret, "mxid": mxid}
if use_v2:
bind_url = "https://%s/_matrix/identity/v2/3pid/bind" % (id_server,)
bind_data["id_access_token"] = id_access_token
else: else:
raise SynapseError(400, "No client_secret in creds") bind_url = "https://%s/_matrix/identity/api/v1/3pid/bind" % (id_server,)
try: try:
data = yield self.http_client.post_json_get_json( data = yield self.http_client.post_json_get_json(bind_url, bind_data)
"https://%s%s" % (id_server, "/_matrix/identity/api/v1/3pid/bind"),
{"sid": creds["sid"], "client_secret": client_secret, "mxid": mxid},
)
logger.debug("bound threepid %r to %s", creds, mxid) logger.debug("bound threepid %r to %s", creds, mxid)
# Remember where we bound the threepid # Remember where we bound the threepid
@ -131,13 +180,23 @@ class IdentityHandler(BaseHandler):
address=data["address"], address=data["address"],
id_server=id_server, id_server=id_server,
) )
return data
except HttpResponseException as e:
if e.code != 404 or not use_v2:
logger.error("3PID bind failed with Matrix error: %r", e)
raise e.to_synapse_error()
except CodeMessageException as e: except CodeMessageException as e:
data = json.loads(e.msg) # XXX WAT? data = json.loads(e.msg) # XXX WAT?
return data return data
logger.info("Got 404 when POSTing JSON %s, falling back to v1 URL", bind_url)
return (yield self.bind_threepid(creds, mxid, use_v2=False))
@defer.inlineCallbacks @defer.inlineCallbacks
def try_unbind_threepid(self, mxid, threepid): def try_unbind_threepid(self, mxid, threepid):
"""Removes a binding from an identity server """Attempt to remove a 3PID from an identity server, or if one is not provided, all
identity servers we're aware the binding is present on
Args: Args:
mxid (str): Matrix user ID of binding to be removed mxid (str): Matrix user ID of binding to be removed
@ -188,6 +247,8 @@ class IdentityHandler(BaseHandler):
server doesn't support unbinding server doesn't support unbinding
""" """
url = "https://%s/_matrix/identity/api/v1/3pid/unbind" % (id_server,) url = "https://%s/_matrix/identity/api/v1/3pid/unbind" % (id_server,)
url_bytes = "/_matrix/identity/api/v1/3pid/unbind".encode("ascii")
content = { content = {
"mxid": mxid, "mxid": mxid,
"threepid": {"medium": threepid["medium"], "address": threepid["address"]}, "threepid": {"medium": threepid["medium"], "address": threepid["address"]},
@ -199,7 +260,7 @@ class IdentityHandler(BaseHandler):
auth_headers = self.federation_http_client.build_auth_headers( auth_headers = self.federation_http_client.build_auth_headers(
destination=None, destination=None,
method="POST", method="POST",
url_bytes="/_matrix/identity/api/v1/3pid/unbind".encode("ascii"), url_bytes=url_bytes,
content=content, content=content,
destination_is=id_server, destination_is=id_server,
) )
@ -226,28 +287,122 @@ class IdentityHandler(BaseHandler):
return changed return changed
@defer.inlineCallbacks
def send_threepid_validation(
self,
email_address,
client_secret,
send_attempt,
send_email_func,
next_link=None,
):
"""Send a threepid validation email for password reset or
registration purposes
Args:
email_address (str): The user's email address
client_secret (str): The provided client secret
send_attempt (int): Which send attempt this is
send_email_func (func): A function that takes an email address, token,
client_secret and session_id, sends an email
and returns a Deferred.
next_link (str|None): The URL to redirect the user to after validation
Returns:
The new session_id upon success
Raises:
SynapseError is an error occurred when sending the email
"""
# Check that this email/client_secret/send_attempt combo is new or
# greater than what we've seen previously
session = yield self.store.get_threepid_validation_session(
"email", client_secret, address=email_address, validated=False
)
# Check to see if a session already exists and that it is not yet
# marked as validated
if session and session.get("validated_at") is None:
session_id = session["session_id"]
last_send_attempt = session["last_send_attempt"]
# Check that the send_attempt is higher than previous attempts
if send_attempt <= last_send_attempt:
# If not, just return a success without sending an email
return session_id
else:
# An non-validated session does not exist yet.
# Generate a session id
session_id = random_string(16)
# Generate a new validation token
token = random_string(32)
# Send the mail with the link containing the token, client_secret
# and session_id
try:
yield send_email_func(email_address, token, client_secret, session_id)
except Exception:
logger.exception(
"Error sending threepid validation email to %s", email_address
)
raise SynapseError(500, "An error was encountered when sending the email")
token_expires = (
self.hs.clock.time_msec() + self.hs.config.email_validation_token_lifetime
)
yield self.store.start_or_continue_validation_session(
"email",
email_address,
session_id,
client_secret,
send_attempt,
next_link,
token,
token_expires,
)
return session_id
@defer.inlineCallbacks @defer.inlineCallbacks
def requestEmailToken( def requestEmailToken(
self, id_server, email, client_secret, send_attempt, next_link=None self, id_server, email, client_secret, send_attempt, next_link=None
): ):
if not self._should_trust_id_server(id_server): """
raise SynapseError( Request an external server send an email on our behalf for the purposes of threepid
400, "Untrusted ID server '%s'" % id_server, Codes.SERVER_NOT_TRUSTED validation.
)
Args:
id_server (str): The identity server to proxy to
email (str): The email to send the message to
client_secret (str): The unique client_secret sends by the user
send_attempt (int): Which attempt this is
next_link: A link to redirect the user to once they submit the token
Returns:
The json response body from the server
"""
params = { params = {
"email": email, "email": email,
"client_secret": client_secret, "client_secret": client_secret,
"send_attempt": send_attempt, "send_attempt": send_attempt,
} }
if next_link: if next_link:
params.update({"next_link": next_link}) params["next_link"] = next_link
if self.hs.config.using_identity_server_from_trusted_list:
# Warn that a deprecated config option is in use
logger.warn(
'The config option "trust_identity_server_for_password_resets" '
'has been replaced by "account_threepid_delegate". '
"Please consult the sample config at docs/sample_config.yaml for "
"details and update your config file."
)
try: try:
data = yield self.http_client.post_json_get_json( data = yield self.http_client.post_json_get_json(
"https://%s%s" id_server + "/_matrix/identity/api/v1/validate/email/requestToken",
% (id_server, "/_matrix/identity/api/v1/validate/email/requestToken"),
params, params,
) )
return data return data
@ -257,25 +412,49 @@ class IdentityHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def requestMsisdnToken( def requestMsisdnToken(
self, id_server, country, phone_number, client_secret, send_attempt, **kwargs self,
id_server,
country,
phone_number,
client_secret,
send_attempt,
next_link=None,
): ):
if not self._should_trust_id_server(id_server): """
raise SynapseError( Request an external server send an SMS message on our behalf for the purposes of
400, "Untrusted ID server '%s'" % id_server, Codes.SERVER_NOT_TRUSTED threepid validation.
) Args:
id_server (str): The identity server to proxy to
country (str): The country code of the phone number
phone_number (str): The number to send the message to
client_secret (str): The unique client_secret sends by the user
send_attempt (int): Which attempt this is
next_link: A link to redirect the user to once they submit the token
Returns:
The json response body from the server
"""
params = { params = {
"country": country, "country": country,
"phone_number": phone_number, "phone_number": phone_number,
"client_secret": client_secret, "client_secret": client_secret,
"send_attempt": send_attempt, "send_attempt": send_attempt,
} }
params.update(kwargs) if next_link:
params["next_link"] = next_link
if self.hs.config.using_identity_server_from_trusted_list:
# Warn that a deprecated config option is in use
logger.warn(
'The config option "trust_identity_server_for_password_resets" '
'has been replaced by "account_threepid_delegate". '
"Please consult the sample config at docs/sample_config.yaml for "
"details and update your config file."
)
try: try:
data = yield self.http_client.post_json_get_json( data = yield self.http_client.post_json_get_json(
"https://%s%s" id_server + "/_matrix/identity/api/v1/validate/msisdn/requestToken",
% (id_server, "/_matrix/identity/api/v1/validate/msisdn/requestToken"),
params, params,
) )
return data return data

View file

@ -260,7 +260,9 @@ class StatsHandler(StateDeltasHandler):
room_stats_delta["local_users_in_room"] += delta room_stats_delta["local_users_in_room"] += delta
elif typ == EventTypes.Create: elif typ == EventTypes.Create:
room_state["is_federatable"] = event_content.get("m.federate", True) room_state["is_federatable"] = (
event_content.get("m.federate", True) is True
)
if sender and self.is_mine_id(sender): if sender and self.is_mine_id(sender):
user_to_stats_deltas.setdefault(sender, Counter())[ user_to_stats_deltas.setdefault(sender, Counter())[
"rooms_created" "rooms_created"

View file

@ -46,6 +46,7 @@ from synapse.http import (
redact_uri, redact_uri,
) )
from synapse.logging.context import make_deferred_yieldable from synapse.logging.context import make_deferred_yieldable
from synapse.logging.opentracing import set_tag, start_active_span, tags
from synapse.util.async_helpers import timeout_deferred from synapse.util.async_helpers import timeout_deferred
from synapse.util.caches import CACHE_SIZE_FACTOR from synapse.util.caches import CACHE_SIZE_FACTOR
@ -269,42 +270,56 @@ class SimpleHttpClient(object):
# log request but strip `access_token` (AS requests for example include this) # log request but strip `access_token` (AS requests for example include this)
logger.info("Sending request %s %s", method, redact_uri(uri)) logger.info("Sending request %s %s", method, redact_uri(uri))
try: with start_active_span(
body_producer = None "outgoing-client-request",
if data is not None: tags={
body_producer = QuieterFileBodyProducer(BytesIO(data)) tags.SPAN_KIND: tags.SPAN_KIND_RPC_CLIENT,
tags.HTTP_METHOD: method,
tags.HTTP_URL: uri,
},
finish_on_close=True,
):
try:
body_producer = None
if data is not None:
body_producer = QuieterFileBodyProducer(BytesIO(data))
request_deferred = treq.request( request_deferred = treq.request(
method, method,
uri, uri,
agent=self.agent, agent=self.agent,
data=body_producer, data=body_producer,
headers=headers, headers=headers,
**self._extra_treq_args **self._extra_treq_args
) )
request_deferred = timeout_deferred( request_deferred = timeout_deferred(
request_deferred, request_deferred,
60, 60,
self.hs.get_reactor(), self.hs.get_reactor(),
cancelled_to_request_timed_out_error, cancelled_to_request_timed_out_error,
) )
response = yield make_deferred_yieldable(request_deferred) response = yield make_deferred_yieldable(request_deferred)
incoming_responses_counter.labels(method, response.code).inc() incoming_responses_counter.labels(method, response.code).inc()
logger.info( logger.info(
"Received response to %s %s: %s", method, redact_uri(uri), response.code "Received response to %s %s: %s",
) method,
return response redact_uri(uri),
except Exception as e: response.code,
incoming_responses_counter.labels(method, "ERR").inc() )
logger.info( return response
"Error sending request to %s %s: %s %s", except Exception as e:
method, incoming_responses_counter.labels(method, "ERR").inc()
redact_uri(uri), logger.info(
type(e).__name__, "Error sending request to %s %s: %s %s",
e.args[0], method,
) redact_uri(uri),
raise type(e).__name__,
e.args[0],
)
set_tag(tags.ERROR, True)
set_tag("error_reason", e.args[0])
raise
@defer.inlineCallbacks @defer.inlineCallbacks
def post_urlencoded_get_json(self, uri, args={}, headers=None): def post_urlencoded_get_json(self, uri, args={}, headers=None):

View file

@ -345,7 +345,6 @@ class MatrixFederationHttpClient(object):
else: else:
query_bytes = b"" query_bytes = b""
# Retreive current span
scope = start_active_span( scope = start_active_span(
"outgoing-federation-request", "outgoing-federation-request",
tags={ tags={

View file

@ -40,6 +40,7 @@ from synapse.api.errors import (
UnrecognizedRequestError, UnrecognizedRequestError,
) )
from synapse.logging.context import preserve_fn from synapse.logging.context import preserve_fn
from synapse.logging.opentracing import trace_servlet
from synapse.util.caches import intern_dict from synapse.util.caches import intern_dict
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -257,7 +258,9 @@ class JsonResource(HttpServer, resource.Resource):
self.path_regexs = {} self.path_regexs = {}
self.hs = hs self.hs = hs
def register_paths(self, method, path_patterns, callback, servlet_classname): def register_paths(
self, method, path_patterns, callback, servlet_classname, trace=True
):
""" """
Registers a request handler against a regular expression. Later request URLs are Registers a request handler against a regular expression. Later request URLs are
checked against these regular expressions in order to identify an appropriate checked against these regular expressions in order to identify an appropriate
@ -273,8 +276,16 @@ class JsonResource(HttpServer, resource.Resource):
servlet_classname (str): The name of the handler to be used in prometheus servlet_classname (str): The name of the handler to be used in prometheus
and opentracing logs. and opentracing logs.
trace (bool): Whether we should start a span to trace the servlet.
""" """
method = method.encode("utf-8") # method is bytes on py3 method = method.encode("utf-8") # method is bytes on py3
if trace:
# We don't extract the context from the servlet because we can't
# trust the sender
callback = trace_servlet(servlet_classname)(callback)
for path_pattern in path_patterns: for path_pattern in path_patterns:
logger.debug("Registering for %s %s", method, path_pattern.pattern) logger.debug("Registering for %s %s", method, path_pattern.pattern)
self.path_regexs.setdefault(method, []).append( self.path_regexs.setdefault(method, []).append(

View file

@ -20,7 +20,6 @@ import logging
from canonicaljson import json from canonicaljson import json
from synapse.api.errors import Codes, SynapseError from synapse.api.errors import Codes, SynapseError
from synapse.logging.opentracing import trace_servlet
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -298,10 +297,7 @@ class RestServlet(object):
servlet_classname = self.__class__.__name__ servlet_classname = self.__class__.__name__
method_handler = getattr(self, "on_%s" % (method,)) method_handler = getattr(self, "on_%s" % (method,))
http_server.register_paths( http_server.register_paths(
method, method, patterns, method_handler, servlet_classname
patterns,
trace_servlet(servlet_classname)(method_handler),
servlet_classname,
) )
else: else:

View file

@ -239,8 +239,7 @@ _homeserver_whitelist = None
def only_if_tracing(func): def only_if_tracing(func):
"""Executes the function only if we're tracing. Otherwise return. """Executes the function only if we're tracing. Otherwise returns None."""
Assumes the function wrapped may return None"""
@wraps(func) @wraps(func)
def _only_if_tracing_inner(*args, **kwargs): def _only_if_tracing_inner(*args, **kwargs):
@ -252,6 +251,41 @@ def only_if_tracing(func):
return _only_if_tracing_inner return _only_if_tracing_inner
def ensure_active_span(message, ret=None):
"""Executes the operation only if opentracing is enabled and there is an active span.
If there is no active span it logs message at the error level.
Args:
message (str): Message which fills in "There was no active span when trying to %s"
in the error log if there is no active span and opentracing is enabled.
ret (object): return value if opentracing is None or there is no active span.
Returns (object): The result of the func or ret if opentracing is disabled or there
was no active span.
"""
def ensure_active_span_inner_1(func):
@wraps(func)
def ensure_active_span_inner_2(*args, **kwargs):
if not opentracing:
return ret
if not opentracing.tracer.active_span:
logger.error(
"There was no active span when trying to %s."
" Did you forget to start one or did a context slip?",
message,
)
return ret
return func(*args, **kwargs)
return ensure_active_span_inner_2
return ensure_active_span_inner_1
@contextlib.contextmanager @contextlib.contextmanager
def _noop_context_manager(*args, **kwargs): def _noop_context_manager(*args, **kwargs):
"""Does exactly what it says on the tin""" """Does exactly what it says on the tin"""
@ -319,7 +353,7 @@ def whitelisted_homeserver(destination):
Args: Args:
destination (str) destination (str)
""" """
_homeserver_whitelist
if _homeserver_whitelist: if _homeserver_whitelist:
return _homeserver_whitelist.match(destination) return _homeserver_whitelist.match(destination)
return False return False
@ -349,26 +383,24 @@ def start_active_span(
if opentracing is None: if opentracing is None:
return _noop_context_manager() return _noop_context_manager()
else: return opentracing.tracer.start_active_span(
# We need to enter the scope here for the logcontext to become active operation_name,
return opentracing.tracer.start_active_span( child_of=child_of,
operation_name, references=references,
child_of=child_of, tags=tags,
references=references, start_time=start_time,
tags=tags, ignore_active_span=ignore_active_span,
start_time=start_time, finish_on_close=finish_on_close,
ignore_active_span=ignore_active_span, )
finish_on_close=finish_on_close,
)
def start_active_span_follows_from(operation_name, contexts): def start_active_span_follows_from(operation_name, contexts):
if opentracing is None: if opentracing is None:
return _noop_context_manager() return _noop_context_manager()
else:
references = [opentracing.follows_from(context) for context in contexts] references = [opentracing.follows_from(context) for context in contexts]
scope = start_active_span(operation_name, references=references) scope = start_active_span(operation_name, references=references)
return scope return scope
def start_active_span_from_request( def start_active_span_from_request(
@ -465,19 +497,19 @@ def start_active_span_from_edu(
# Opentracing setters for tags, logs, etc # Opentracing setters for tags, logs, etc
@only_if_tracing @ensure_active_span("set a tag")
def set_tag(key, value): def set_tag(key, value):
"""Sets a tag on the active span""" """Sets a tag on the active span"""
opentracing.tracer.active_span.set_tag(key, value) opentracing.tracer.active_span.set_tag(key, value)
@only_if_tracing @ensure_active_span("log")
def log_kv(key_values, timestamp=None): def log_kv(key_values, timestamp=None):
"""Log to the active span""" """Log to the active span"""
opentracing.tracer.active_span.log_kv(key_values, timestamp) opentracing.tracer.active_span.log_kv(key_values, timestamp)
@only_if_tracing @ensure_active_span("set the traces operation name")
def set_operation_name(operation_name): def set_operation_name(operation_name):
"""Sets the operation name of the active span""" """Sets the operation name of the active span"""
opentracing.tracer.active_span.set_operation_name(operation_name) opentracing.tracer.active_span.set_operation_name(operation_name)
@ -486,13 +518,18 @@ def set_operation_name(operation_name):
# Injection and extraction # Injection and extraction
@only_if_tracing @ensure_active_span("inject the span into a header")
def inject_active_span_twisted_headers(headers, destination, check_destination=True): def inject_active_span_twisted_headers(headers, destination, check_destination=True):
""" """
Injects a span context into twisted headers in-place Injects a span context into twisted headers in-place
Args: Args:
headers (twisted.web.http_headers.Headers) headers (twisted.web.http_headers.Headers)
destination (str): address of entity receiving the span context. If check_destination
is true the context will only be injected if the destination matches the
opentracing whitelist
check_destination (bool): If false, destination will be ignored and the context
will always be injected.
span (opentracing.Span) span (opentracing.Span)
Returns: Returns:
@ -517,7 +554,7 @@ def inject_active_span_twisted_headers(headers, destination, check_destination=T
headers.addRawHeaders(key, value) headers.addRawHeaders(key, value)
@only_if_tracing @ensure_active_span("inject the span into a byte dict")
def inject_active_span_byte_dict(headers, destination, check_destination=True): def inject_active_span_byte_dict(headers, destination, check_destination=True):
""" """
Injects a span context into a dict where the headers are encoded as byte Injects a span context into a dict where the headers are encoded as byte
@ -525,6 +562,11 @@ def inject_active_span_byte_dict(headers, destination, check_destination=True):
Args: Args:
headers (dict) headers (dict)
destination (str): address of entity receiving the span context. If check_destination
is true the context will only be injected if the destination matches the
opentracing whitelist
check_destination (bool): If false, destination will be ignored and the context
will always be injected.
span (opentracing.Span) span (opentracing.Span)
Returns: Returns:
@ -537,7 +579,7 @@ def inject_active_span_byte_dict(headers, destination, check_destination=True):
here: here:
https://github.com/jaegertracing/jaeger-client-python/blob/master/jaeger_client/constants.py https://github.com/jaegertracing/jaeger-client-python/blob/master/jaeger_client/constants.py
""" """
if not whitelisted_homeserver(destination): if check_destination and not whitelisted_homeserver(destination):
return return
span = opentracing.tracer.active_span span = opentracing.tracer.active_span
@ -549,16 +591,18 @@ def inject_active_span_byte_dict(headers, destination, check_destination=True):
headers[key.encode()] = [value.encode()] headers[key.encode()] = [value.encode()]
@only_if_tracing @ensure_active_span("inject the span into a text map")
def inject_active_span_text_map(carrier, destination, check_destination=True): def inject_active_span_text_map(carrier, destination, check_destination=True):
""" """
Injects a span context into a dict Injects a span context into a dict
Args: Args:
carrier (dict) carrier (dict)
destination (str): the name of the remote server. The span context destination (str): address of entity receiving the span context. If check_destination
will only be injected if the destination matches the homeserver_whitelist is true the context will only be injected if the destination matches the
or destination is None. opentracing whitelist
check_destination (bool): If false, destination will be ignored and the context
will always be injected.
Returns: Returns:
In-place modification of carrier In-place modification of carrier
@ -579,6 +623,7 @@ def inject_active_span_text_map(carrier, destination, check_destination=True):
) )
@ensure_active_span("get the active span context as a dict", ret={})
def get_active_span_text_map(destination=None): def get_active_span_text_map(destination=None):
""" """
Gets a span context as a dict. This can be used instead of manually Gets a span context as a dict. This can be used instead of manually
@ -591,7 +636,7 @@ def get_active_span_text_map(destination=None):
dict: the active span's context if opentracing is enabled, otherwise empty. dict: the active span's context if opentracing is enabled, otherwise empty.
""" """
if not opentracing or (destination and not whitelisted_homeserver(destination)): if destination and not whitelisted_homeserver(destination):
return {} return {}
carrier = {} carrier = {}
@ -602,6 +647,7 @@ def get_active_span_text_map(destination=None):
return carrier return carrier
@ensure_active_span("get the span context as a string.", ret={})
def active_span_context_as_string(): def active_span_context_as_string():
""" """
Returns: Returns:
@ -656,15 +702,15 @@ def trace(func=None, opname=None):
_opname = opname if opname else func.__name__ _opname = opname if opname else func.__name__
@wraps(func) @wraps(func)
def _trace_inner(self, *args, **kwargs): def _trace_inner(*args, **kwargs):
if opentracing is None: if opentracing is None:
return func(self, *args, **kwargs) return func(*args, **kwargs)
scope = start_active_span(_opname) scope = start_active_span(_opname)
scope.__enter__() scope.__enter__()
try: try:
result = func(self, *args, **kwargs) result = func(*args, **kwargs)
if isinstance(result, defer.Deferred): if isinstance(result, defer.Deferred):
def call_back(result): def call_back(result):
@ -704,13 +750,13 @@ def tag_args(func):
return func return func
@wraps(func) @wraps(func)
def _tag_args_inner(self, *args, **kwargs): def _tag_args_inner(*args, **kwargs):
argspec = inspect.getargspec(func) argspec = inspect.getargspec(func)
for i, arg in enumerate(argspec.args[1:]): for i, arg in enumerate(argspec.args[1:]):
set_tag("ARG_" + arg, args[i]) set_tag("ARG_" + arg, args[i])
set_tag("args", args[len(argspec.args) :]) set_tag("args", args[len(argspec.args) :])
set_tag("kwargs", kwargs) set_tag("kwargs", kwargs)
return func(self, *args, **kwargs) return func(*args, **kwargs)
return _tag_args_inner return _tag_args_inner

View file

@ -131,14 +131,11 @@ class Mailer(object):
email_address (str): Email address we're sending the password email_address (str): Email address we're sending the password
reset to reset to
token (str): Unique token generated by the server to verify token (str): Unique token generated by the server to verify
password reset email was received the email was received
client_secret (str): Unique token generated by the client to client_secret (str): Unique token generated by the client to
group together multiple email sending attempts group together multiple email sending attempts
sid (str): The generated session ID sid (str): The generated session ID
""" """
if email.utils.parseaddr(email_address)[1] == "":
raise RuntimeError("Invalid 'to' email address")
link = ( link = (
self.hs.config.public_baseurl self.hs.config.public_baseurl
+ "_matrix/client/unstable/password_reset/email/submit_token" + "_matrix/client/unstable/password_reset/email/submit_token"
@ -149,7 +146,34 @@ class Mailer(object):
yield self.send_email( yield self.send_email(
email_address, email_address,
"[%s] Password Reset Email" % self.hs.config.server_name, "[%s] Password Reset" % self.hs.config.server_name,
template_vars,
)
@defer.inlineCallbacks
def send_registration_mail(self, email_address, token, client_secret, sid):
"""Send an email with a registration confirmation link to a user
Args:
email_address (str): Email address we're sending the registration
link to
token (str): Unique token generated by the server to verify
the email was received
client_secret (str): Unique token generated by the client to
group together multiple email sending attempts
sid (str): The generated session ID
"""
link = (
self.hs.config.public_baseurl
+ "_matrix/client/unstable/registration/email/submit_token"
"?token=%s&client_secret=%s&sid=%s" % (token, client_secret, sid)
)
template_vars = {"link": link}
yield self.send_email(
email_address,
"[%s] Register your Email Address" % self.hs.config.server_name,
template_vars, template_vars,
) )
@ -605,25 +629,50 @@ def format_ts_filter(value, format):
return time.strftime(format, time.localtime(value / 1000)) return time.strftime(format, time.localtime(value / 1000))
def load_jinja2_templates(config, template_html_name, template_text_name): def load_jinja2_templates(
"""Load the jinja2 email templates from disk template_dir,
template_filenames,
apply_format_ts_filter=False,
apply_mxc_to_http_filter=False,
public_baseurl=None,
):
"""Loads and returns one or more jinja2 templates and applies optional filters
Args:
template_dir (str): The directory where templates are stored
template_filenames (list[str]): A list of template filenames
apply_format_ts_filter (bool): Whether to apply a template filter that formats
timestamps
apply_mxc_to_http_filter (bool): Whether to apply a template filter that converts
mxc urls to http urls
public_baseurl (str|None): The public baseurl of the server. Required for
apply_mxc_to_http_filter to be enabled
Returns: Returns:
(template_html, template_text) A list of jinja2 templates corresponding to the given list of filenames,
with order preserved
""" """
logger.info("loading email templates from '%s'", config.email_template_dir) logger.info(
loader = jinja2.FileSystemLoader(config.email_template_dir) "loading email templates %s from '%s'", template_filenames, template_dir
)
loader = jinja2.FileSystemLoader(template_dir)
env = jinja2.Environment(loader=loader) env = jinja2.Environment(loader=loader)
env.filters["format_ts"] = format_ts_filter
env.filters["mxc_to_http"] = _create_mxc_to_http_filter(config)
template_html = env.get_template(template_html_name) if apply_format_ts_filter:
template_text = env.get_template(template_text_name) env.filters["format_ts"] = format_ts_filter
return template_html, template_text if apply_mxc_to_http_filter and public_baseurl:
env.filters["mxc_to_http"] = _create_mxc_to_http_filter(public_baseurl)
templates = []
for template_filename in template_filenames:
template = env.get_template(template_filename)
templates.append(template)
return templates
def _create_mxc_to_http_filter(config): def _create_mxc_to_http_filter(public_baseurl):
def mxc_to_http_filter(value, width, height, resize_method="crop"): def mxc_to_http_filter(value, width, height, resize_method="crop"):
if value[0:6] != "mxc://": if value[0:6] != "mxc://":
return "" return ""
@ -636,7 +685,7 @@ def _create_mxc_to_http_filter(config):
params = {"width": width, "height": height, "method": resize_method} params = {"width": width, "height": height, "method": resize_method}
return "%s_matrix/media/v1/thumbnail/%s?%s%s" % ( return "%s_matrix/media/v1/thumbnail/%s?%s%s" % (
config.public_baseurl, public_baseurl,
serverAndMediaId, serverAndMediaId,
urllib.parse.urlencode(params), urllib.parse.urlencode(params),
fragment or "", fragment or "",

View file

@ -35,6 +35,7 @@ except Exception:
class PusherFactory(object): class PusherFactory(object):
def __init__(self, hs): def __init__(self, hs):
self.hs = hs self.hs = hs
self.config = hs.config
self.pusher_types = {"http": HttpPusher} self.pusher_types = {"http": HttpPusher}
@ -42,12 +43,16 @@ class PusherFactory(object):
if hs.config.email_enable_notifs: if hs.config.email_enable_notifs:
self.mailers = {} # app_name -> Mailer self.mailers = {} # app_name -> Mailer
templates = load_jinja2_templates( self.notif_template_html, self.notif_template_text = load_jinja2_templates(
config=hs.config, self.config.email_template_dir,
template_html_name=hs.config.email_notif_template_html, [
template_text_name=hs.config.email_notif_template_text, self.config.email_notif_template_html,
self.config.email_notif_template_text,
],
apply_format_ts_filter=True,
apply_mxc_to_http_filter=True,
public_baseurl=self.config.public_baseurl,
) )
self.notif_template_html, self.notif_template_text = templates
self.pusher_types["email"] = self._create_email_pusher self.pusher_types["email"] = self._create_email_pusher
@ -78,6 +83,6 @@ class PusherFactory(object):
if "data" in pusherdict and "brand" in pusherdict["data"]: if "data" in pusherdict and "brand" in pusherdict["data"]:
app_name = pusherdict["data"]["brand"] app_name = pusherdict["data"]["brand"]
else: else:
app_name = self.hs.config.email_app_name app_name = self.config.email_app_name
return app_name return app_name

View file

@ -22,13 +22,17 @@ from six.moves import urllib
from twisted.internet import defer from twisted.internet import defer
import synapse.logging.opentracing as opentracing
from synapse.api.errors import ( from synapse.api.errors import (
CodeMessageException, CodeMessageException,
HttpResponseException, HttpResponseException,
RequestSendFailed, RequestSendFailed,
SynapseError, SynapseError,
) )
from synapse.logging.opentracing import (
inject_active_span_byte_dict,
trace,
trace_servlet,
)
from synapse.util.caches.response_cache import ResponseCache from synapse.util.caches.response_cache import ResponseCache
from synapse.util.stringutils import random_string from synapse.util.stringutils import random_string
@ -129,6 +133,7 @@ class ReplicationEndpoint(object):
client = hs.get_simple_http_client() client = hs.get_simple_http_client()
@trace(opname="outgoing_replication_request")
@defer.inlineCallbacks @defer.inlineCallbacks
def send_request(**kwargs): def send_request(**kwargs):
data = yield cls._serialize_payload(**kwargs) data = yield cls._serialize_payload(**kwargs)
@ -167,9 +172,7 @@ class ReplicationEndpoint(object):
# the master, and so whether we should clean up or not. # the master, and so whether we should clean up or not.
while True: while True:
headers = {} headers = {}
opentracing.inject_active_span_byte_dict( inject_active_span_byte_dict(headers, None, check_destination=False)
headers, None, check_destination=False
)
try: try:
result = yield request_func(uri, data, headers=headers) result = yield request_func(uri, data, headers=headers)
break break
@ -210,13 +213,11 @@ class ReplicationEndpoint(object):
args = "/".join("(?P<%s>[^/]+)" % (arg,) for arg in url_args) args = "/".join("(?P<%s>[^/]+)" % (arg,) for arg in url_args)
pattern = re.compile("^/_synapse/replication/%s/%s$" % (self.NAME, args)) pattern = re.compile("^/_synapse/replication/%s/%s$" % (self.NAME, args))
handler = trace_servlet(self.__class__.__name__, extract_context=True)(handler)
# We don't let register paths trace this servlet using the default tracing
# options because we wish to extract the context explicitly.
http_server.register_paths( http_server.register_paths(
method, method, [pattern], handler, self.__class__.__name__, trace=False
[pattern],
opentracing.trace_servlet(self.__class__.__name__, extract_context=True)(
handler
),
self.__class__.__name__,
) )
def _cached_handler(self, request, txn_id, **kwargs): def _cached_handler(self, request, txn_id, **kwargs):

View file

@ -4,6 +4,6 @@
<a href="{{ link }}">{{ link }}</a> <a href="{{ link }}">{{ link }}</a>
<p>If this was not you, please disregard this email and contact your server administrator. Thank you.</p> <p>If this was not you, <strong>do not</strong> click the link above and instead contact your server administrator. Thank you.</p>
</body> </body>
</html> </html>

View file

@ -3,5 +3,5 @@ was you, please click the link below to confirm resetting your password:
{{ link }} {{ link }}
If this was not you, please disregard this email and contact your server If this was not you, DO NOT click the link above and instead contact your
administrator. Thank you. server administrator. Thank you.

View file

@ -1,6 +1,8 @@
<html> <html>
<head></head> <head></head>
<body> <body>
<p>{{ failure_reason }}. Your password has not been reset.</p> <p>The request failed for the following reason: {{ failure_reason }}.</p>
<p>Your password has not been reset.</p>
</body> </body>
</html> </html>

View file

@ -0,0 +1,11 @@
<html>
<body>
<p>You have asked us to register this email with a new Matrix account. If this was you, please click the link below to confirm your email address:</p>
<a href="{{ link }}">Verify Your Email Address</a>
<p>If this was not you, you can safely disregard this email.</p>
<p>Thank you.</p>
</body>
</html>

View file

@ -0,0 +1,10 @@
Hello there,
You have asked us to register this email with a new Matrix account. If this
was you, please click the link below to confirm your email address:
{{ link }}
If this was not you, you can safely disregard this email.
Thank you.

View file

@ -0,0 +1,6 @@
<html>
<head></head>
<body>
<p>Validation failed for the following reason: {{ failure_reason }}.</p>
</body>
</html>

View file

@ -0,0 +1,6 @@
<html>
<head></head>
<body>
<p>Your email has now been validated, please return to your client. You may now close this window.</p>
</body>
</html>

View file

@ -73,7 +73,7 @@ class ClientRestResource(JsonResource):
@staticmethod @staticmethod
def register_servlets(client_resource, hs): def register_servlets(client_resource, hs):
versions.register_servlets(client_resource) versions.register_servlets(hs, client_resource)
# Deprecated in r0 # Deprecated in r0
initial_sync.register_servlets(hs, client_resource) initial_sync.register_servlets(hs, client_resource)

View file

@ -37,6 +37,7 @@ def client_patterns(path_regex, releases=(0,), unstable=True, v1=False):
SRE_Pattern SRE_Pattern
""" """
patterns = [] patterns = []
if unstable: if unstable:
unstable_prefix = CLIENT_API_PREFIX + "/unstable" unstable_prefix = CLIENT_API_PREFIX + "/unstable"
patterns.append(re.compile("^" + unstable_prefix + path_regex)) patterns.append(re.compile("^" + unstable_prefix + path_regex))
@ -46,6 +47,7 @@ def client_patterns(path_regex, releases=(0,), unstable=True, v1=False):
for release in releases: for release in releases:
new_prefix = CLIENT_API_PREFIX + "/r%d" % (release,) new_prefix = CLIENT_API_PREFIX + "/r%d" % (release,)
patterns.append(re.compile("^" + new_prefix + path_regex)) patterns.append(re.compile("^" + new_prefix + path_regex))
return patterns return patterns

View file

@ -18,12 +18,11 @@ import logging
from six.moves import http_client from six.moves import http_client
import jinja2
from twisted.internet import defer from twisted.internet import defer
from synapse.api.constants import LoginType from synapse.api.constants import LoginType
from synapse.api.errors import Codes, SynapseError, ThreepidValidationError from synapse.api.errors import Codes, SynapseError, ThreepidValidationError
from synapse.config.emailconfig import ThreepidBehaviour
from synapse.http.server import finish_request from synapse.http.server import finish_request
from synapse.http.servlet import ( from synapse.http.servlet import (
RestServlet, RestServlet,
@ -31,8 +30,8 @@ from synapse.http.servlet import (
parse_json_object_from_request, parse_json_object_from_request,
parse_string, parse_string,
) )
from synapse.push.mailer import Mailer, load_jinja2_templates
from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.msisdn import phone_number_to_msisdn
from synapse.util.stringutils import random_string
from synapse.util.threepids import check_3pid_allowed from synapse.util.threepids import check_3pid_allowed
from ._base import client_patterns, interactive_auth_handler from ._base import client_patterns, interactive_auth_handler
@ -50,25 +49,28 @@ class EmailPasswordRequestTokenRestServlet(RestServlet):
self.config = hs.config self.config = hs.config
self.identity_handler = hs.get_handlers().identity_handler self.identity_handler = hs.get_handlers().identity_handler
if self.config.email_password_reset_behaviour == "local": if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
from synapse.push.mailer import Mailer, load_jinja2_templates template_html, template_text = load_jinja2_templates(
self.config.email_template_dir,
templates = load_jinja2_templates( [
config=hs.config, self.config.email_password_reset_template_html,
template_html_name=hs.config.email_password_reset_template_html, self.config.email_password_reset_template_text,
template_text_name=hs.config.email_password_reset_template_text, ],
apply_format_ts_filter=True,
apply_mxc_to_http_filter=True,
public_baseurl=self.config.public_baseurl,
) )
self.mailer = Mailer( self.mailer = Mailer(
hs=self.hs, hs=self.hs,
app_name=self.config.email_app_name, app_name=self.config.email_app_name,
template_html=templates[0], template_html=template_html,
template_text=templates[1], template_text=template_text,
) )
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request): def on_POST(self, request):
if self.config.email_password_reset_behaviour == "off": if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF:
if self.config.password_resets_were_disabled_due_to_email_config: if self.config.local_threepid_handling_disabled_due_to_email_config:
logger.warn( logger.warn(
"User password resets have been disabled due to lack of email config" "User password resets have been disabled due to lack of email config"
) )
@ -93,25 +95,39 @@ class EmailPasswordRequestTokenRestServlet(RestServlet):
Codes.THREEPID_DENIED, Codes.THREEPID_DENIED,
) )
existingUid = yield self.hs.get_datastore().get_user_id_by_threepid( existing_user_id = yield self.hs.get_datastore().get_user_id_by_threepid(
"email", email "email", email
) )
if existingUid is None: if existing_user_id is None:
raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND) raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND)
if self.config.email_password_reset_behaviour == "remote": if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
if "id_server" not in body: # Have the configured identity server handle the request
raise SynapseError(400, "Missing 'id_server' param in body") if not self.hs.config.account_threepid_delegate_email:
logger.warn(
"No upstream email account_threepid_delegate configured on the server to "
"handle this request"
)
raise SynapseError(
400, "Password reset by email is not supported on this homeserver"
)
# Have the identity server handle the password reset flow
ret = yield self.identity_handler.requestEmailToken( ret = yield self.identity_handler.requestEmailToken(
body["id_server"], email, client_secret, send_attempt, next_link self.hs.config.account_threepid_delegate_email,
email,
client_secret,
send_attempt,
next_link,
) )
else: else:
# Send password reset emails from Synapse # Send password reset emails from Synapse
sid = yield self.send_password_reset( sid = yield self.identity_handler.send_threepid_validation(
email, client_secret, send_attempt, next_link email,
client_secret,
send_attempt,
self.mailer.send_password_reset_mail,
next_link,
) )
# Wrap the session id in a JSON object # Wrap the session id in a JSON object
@ -119,74 +135,6 @@ class EmailPasswordRequestTokenRestServlet(RestServlet):
return 200, ret return 200, ret
@defer.inlineCallbacks
def send_password_reset(self, email, client_secret, send_attempt, next_link=None):
"""Send a password reset email
Args:
email (str): The user's email address
client_secret (str): The provided client secret
send_attempt (int): Which send attempt this is
Returns:
The new session_id upon success
Raises:
SynapseError is an error occurred when sending the email
"""
# Check that this email/client_secret/send_attempt combo is new or
# greater than what we've seen previously
session = yield self.datastore.get_threepid_validation_session(
"email", client_secret, address=email, validated=False
)
# Check to see if a session already exists and that it is not yet
# marked as validated
if session and session.get("validated_at") is None:
session_id = session["session_id"]
last_send_attempt = session["last_send_attempt"]
# Check that the send_attempt is higher than previous attempts
if send_attempt <= last_send_attempt:
# If not, just return a success without sending an email
return session_id
else:
# An non-validated session does not exist yet.
# Generate a session id
session_id = random_string(16)
# Generate a new validation token
token = random_string(32)
# Send the mail with the link containing the token, client_secret
# and session_id
try:
yield self.mailer.send_password_reset_mail(
email, token, client_secret, session_id
)
except Exception:
logger.exception("Error sending a password reset email to %s", email)
raise SynapseError(
500, "An error was encountered when sending the password reset email"
)
token_expires = (
self.hs.clock.time_msec() + self.config.email_validation_token_lifetime
)
yield self.datastore.start_or_continue_validation_session(
"email",
email,
session_id,
client_secret,
send_attempt,
next_link,
token,
token_expires,
)
return session_id
class MsisdnPasswordRequestTokenRestServlet(RestServlet): class MsisdnPasswordRequestTokenRestServlet(RestServlet):
PATTERNS = client_patterns("/account/password/msisdn/requestToken$") PATTERNS = client_patterns("/account/password/msisdn/requestToken$")
@ -202,11 +150,15 @@ class MsisdnPasswordRequestTokenRestServlet(RestServlet):
body = parse_json_object_from_request(request) body = parse_json_object_from_request(request)
assert_params_in_dict( assert_params_in_dict(
body, body, ["client_secret", "country", "phone_number", "send_attempt"]
["id_server", "client_secret", "country", "phone_number", "send_attempt"],
) )
client_secret = body["client_secret"]
country = body["country"]
phone_number = body["phone_number"]
send_attempt = body["send_attempt"]
next_link = body.get("next_link") # Optional param
msisdn = phone_number_to_msisdn(body["country"], body["phone_number"]) msisdn = phone_number_to_msisdn(country, phone_number)
if not check_3pid_allowed(self.hs, "msisdn", msisdn): if not check_3pid_allowed(self.hs, "msisdn", msisdn):
raise SynapseError( raise SynapseError(
@ -215,12 +167,32 @@ class MsisdnPasswordRequestTokenRestServlet(RestServlet):
Codes.THREEPID_DENIED, Codes.THREEPID_DENIED,
) )
existingUid = yield self.datastore.get_user_id_by_threepid("msisdn", msisdn) existing_user_id = yield self.datastore.get_user_id_by_threepid(
"msisdn", msisdn
)
if existingUid is None: if existing_user_id is None:
raise SynapseError(400, "MSISDN not found", Codes.THREEPID_NOT_FOUND) raise SynapseError(400, "MSISDN not found", Codes.THREEPID_NOT_FOUND)
ret = yield self.identity_handler.requestMsisdnToken(**body) if not self.hs.config.account_threepid_delegate_msisdn:
logger.warn(
"No upstream msisdn account_threepid_delegate configured on the server to "
"handle this request"
)
raise SynapseError(
400,
"Password reset by phone number is not supported on this homeserver",
)
ret = yield self.identity_handler.requestMsisdnToken(
self.hs.config.account_threepid_delegate_msisdn,
country,
phone_number,
client_secret,
send_attempt,
next_link,
)
return 200, ret return 200, ret
@ -241,31 +213,32 @@ class PasswordResetSubmitTokenServlet(RestServlet):
self.auth = hs.get_auth() self.auth = hs.get_auth()
self.config = hs.config self.config = hs.config
self.clock = hs.get_clock() self.clock = hs.get_clock()
self.datastore = hs.get_datastore() self.store = hs.get_datastore()
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, medium): def on_GET(self, request, medium):
# We currently only handle threepid token submissions for email
if medium != "email": if medium != "email":
raise SynapseError( raise SynapseError(
400, "This medium is currently not supported for password resets" 400, "This medium is currently not supported for password resets"
) )
if self.config.email_password_reset_behaviour == "off": if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF:
if self.config.password_resets_were_disabled_due_to_email_config: if self.config.local_threepid_handling_disabled_due_to_email_config:
logger.warn( logger.warn(
"User password resets have been disabled due to lack of email config" "Password reset emails have been disabled due to lack of an email config"
) )
raise SynapseError( raise SynapseError(
400, "Email-based password resets have been disabled on this server" 400, "Email-based password resets are disabled on this server"
) )
sid = parse_string(request, "sid") sid = parse_string(request, "sid", required=True)
client_secret = parse_string(request, "client_secret") client_secret = parse_string(request, "client_secret", required=True)
token = parse_string(request, "token") token = parse_string(request, "token", required=True)
# Attempt to validate a 3PID sesssion # Attempt to validate a 3PID session
try: try:
# Mark the session as valid # Mark the session as valid
next_link = yield self.datastore.validate_threepid_session( next_link = yield self.store.validate_threepid_session(
sid, client_secret, token, self.clock.time_msec() sid, client_secret, token, self.clock.time_msec()
) )
@ -282,38 +255,22 @@ class PasswordResetSubmitTokenServlet(RestServlet):
return None return None
# Otherwise show the success template # Otherwise show the success template
html = self.config.email_password_reset_template_success_html_content html = self.config.email_password_reset_template_success_html
request.setResponseCode(200) request.setResponseCode(200)
except ThreepidValidationError as e: except ThreepidValidationError as e:
# Show a failure page with a reason
html = self.load_jinja2_template(
self.config.email_template_dir,
self.config.email_password_reset_template_failure_html,
template_vars={"failure_reason": e.msg},
)
request.setResponseCode(e.code) request.setResponseCode(e.code)
# Show a failure page with a reason
html_template, = load_jinja2_templates(
self.config.email_template_dir,
[self.config.email_password_reset_template_failure_html],
)
template_vars = {"failure_reason": e.msg}
html = html_template.render(**template_vars)
request.write(html.encode("utf-8")) request.write(html.encode("utf-8"))
finish_request(request) finish_request(request)
return None
def load_jinja2_template(self, template_dir, template_filename, template_vars):
"""Loads a jinja2 template with variables to insert
Args:
template_dir (str): The directory where templates are stored
template_filename (str): The name of the template in the template_dir
template_vars (Dict): Dictionary of keys in the template
alongside their values to insert
Returns:
str containing the contents of the rendered template
"""
loader = jinja2.FileSystemLoader(template_dir)
env = jinja2.Environment(loader=loader)
template = env.get_template(template_filename)
return template.render(**template_vars)
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request, medium): def on_POST(self, request, medium):
@ -325,7 +282,7 @@ class PasswordResetSubmitTokenServlet(RestServlet):
body = parse_json_object_from_request(request) body = parse_json_object_from_request(request)
assert_params_in_dict(body, ["sid", "client_secret", "token"]) assert_params_in_dict(body, ["sid", "client_secret", "token"])
valid, _ = yield self.datastore.validate_threepid_validation_token( valid, _ = yield self.store.validate_threepid_session(
body["sid"], body["client_secret"], body["token"], self.clock.time_msec() body["sid"], body["client_secret"], body["token"], self.clock.time_msec()
) )
response_code = 200 if valid else 400 response_code = 200 if valid else 400
@ -371,7 +328,6 @@ class PasswordRestServlet(RestServlet):
[[LoginType.EMAIL_IDENTITY], [LoginType.MSISDN]], [[LoginType.EMAIL_IDENTITY], [LoginType.MSISDN]],
body, body,
self.hs.get_ip_from_request(request), self.hs.get_ip_from_request(request),
password_servlet=True,
) )
if LoginType.EMAIL_IDENTITY in result: if LoginType.EMAIL_IDENTITY in result:
@ -454,10 +410,11 @@ class EmailThreepidRequestTokenRestServlet(RestServlet):
PATTERNS = client_patterns("/account/3pid/email/requestToken$") PATTERNS = client_patterns("/account/3pid/email/requestToken$")
def __init__(self, hs): def __init__(self, hs):
self.hs = hs
super(EmailThreepidRequestTokenRestServlet, self).__init__() super(EmailThreepidRequestTokenRestServlet, self).__init__()
self.hs = hs
self.config = hs.config
self.identity_handler = hs.get_handlers().identity_handler self.identity_handler = hs.get_handlers().identity_handler
self.datastore = self.hs.get_datastore() self.store = self.hs.get_datastore()
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request): def on_POST(self, request):
@ -465,22 +422,29 @@ class EmailThreepidRequestTokenRestServlet(RestServlet):
assert_params_in_dict( assert_params_in_dict(
body, ["id_server", "client_secret", "email", "send_attempt"] body, ["id_server", "client_secret", "email", "send_attempt"]
) )
id_server = "https://" + body["id_server"] # Assume https
client_secret = body["client_secret"]
email = body["email"]
send_attempt = body["send_attempt"]
next_link = body.get("next_link") # Optional param
if not check_3pid_allowed(self.hs, "email", body["email"]): if not check_3pid_allowed(self.hs, "email", email):
raise SynapseError( raise SynapseError(
403, 403,
"Your email domain is not authorized on this server", "Your email domain is not authorized on this server",
Codes.THREEPID_DENIED, Codes.THREEPID_DENIED,
) )
existingUid = yield self.datastore.get_user_id_by_threepid( existing_user_id = yield self.store.get_user_id_by_threepid(
"email", body["email"] "email", body["email"]
) )
if existingUid is not None: if existing_user_id is not None:
raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE) raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE)
ret = yield self.identity_handler.requestEmailToken(**body) ret = yield self.identity_handler.requestEmailToken(
id_server, email, client_secret, send_attempt, next_link
)
return 200, ret return 200, ret
@ -490,8 +454,8 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet):
def __init__(self, hs): def __init__(self, hs):
self.hs = hs self.hs = hs
super(MsisdnThreepidRequestTokenRestServlet, self).__init__() super(MsisdnThreepidRequestTokenRestServlet, self).__init__()
self.store = self.hs.get_datastore()
self.identity_handler = hs.get_handlers().identity_handler self.identity_handler = hs.get_handlers().identity_handler
self.datastore = self.hs.get_datastore()
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request): def on_POST(self, request):
@ -500,8 +464,14 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet):
body, body,
["id_server", "client_secret", "country", "phone_number", "send_attempt"], ["id_server", "client_secret", "country", "phone_number", "send_attempt"],
) )
id_server = "https://" + body["id_server"] # Assume https
client_secret = body["client_secret"]
country = body["country"]
phone_number = body["phone_number"]
send_attempt = body["send_attempt"]
next_link = body.get("next_link") # Optional param
msisdn = phone_number_to_msisdn(body["country"], body["phone_number"]) msisdn = phone_number_to_msisdn(country, phone_number)
if not check_3pid_allowed(self.hs, "msisdn", msisdn): if not check_3pid_allowed(self.hs, "msisdn", msisdn):
raise SynapseError( raise SynapseError(
@ -510,12 +480,14 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet):
Codes.THREEPID_DENIED, Codes.THREEPID_DENIED,
) )
existingUid = yield self.datastore.get_user_id_by_threepid("msisdn", msisdn) existing_user_id = yield self.store.get_user_id_by_threepid("msisdn", msisdn)
if existingUid is not None: if existing_user_id is not None:
raise SynapseError(400, "MSISDN is already in use", Codes.THREEPID_IN_USE) raise SynapseError(400, "MSISDN is already in use", Codes.THREEPID_IN_USE)
ret = yield self.identity_handler.requestMsisdnToken(**body) ret = yield self.identity_handler.requestMsisdnToken(
id_server, country, phone_number, client_secret, send_attempt, next_link
)
return 200, ret return 200, ret
@ -542,15 +514,16 @@ class ThreepidRestServlet(RestServlet):
def on_POST(self, request): def on_POST(self, request):
body = parse_json_object_from_request(request) body = parse_json_object_from_request(request)
threePidCreds = body.get("threePidCreds") threepid_creds = body.get("threePidCreds") or body.get("three_pid_creds")
threePidCreds = body.get("three_pid_creds", threePidCreds) if threepid_creds is None:
if threePidCreds is None: raise SynapseError(
raise SynapseError(400, "Missing param", Codes.MISSING_PARAM) 400, "Missing param three_pid_creds", Codes.MISSING_PARAM
)
requester = yield self.auth.get_user_by_req(request) requester = yield self.auth.get_user_by_req(request)
user_id = requester.user.to_string() user_id = requester.user.to_string()
threepid = yield self.identity_handler.threepid_from_creds(threePidCreds) threepid = yield self.identity_handler.threepid_from_creds(threepid_creds)
if not threepid: if not threepid:
raise SynapseError(400, "Failed to auth 3pid", Codes.THREEPID_AUTH_FAILED) raise SynapseError(400, "Failed to auth 3pid", Codes.THREEPID_AUTH_FAILED)
@ -566,11 +539,43 @@ class ThreepidRestServlet(RestServlet):
if "bind" in body and body["bind"]: if "bind" in body and body["bind"]:
logger.debug("Binding threepid %s to %s", threepid, user_id) logger.debug("Binding threepid %s to %s", threepid, user_id)
yield self.identity_handler.bind_threepid(threePidCreds, user_id) yield self.identity_handler.bind_threepid(threepid_creds, user_id)
return 200, {} return 200, {}
class ThreepidUnbindRestServlet(RestServlet):
PATTERNS = client_patterns("/account/3pid/unbind$")
def __init__(self, hs):
super(ThreepidUnbindRestServlet, self).__init__()
self.hs = hs
self.identity_handler = hs.get_handlers().identity_handler
self.auth = hs.get_auth()
self.datastore = self.hs.get_datastore()
@defer.inlineCallbacks
def on_POST(self, request):
"""Unbind the given 3pid from a specific identity server, or identity servers that are
known to have this 3pid bound
"""
requester = yield self.auth.get_user_by_req(request)
body = parse_json_object_from_request(request)
assert_params_in_dict(body, ["medium", "address"])
medium = body.get("medium")
address = body.get("address")
id_server = body.get("id_server")
# Attempt to unbind the threepid from an identity server. If id_server is None, try to
# unbind from all identity servers this threepid has been added to in the past
result = yield self.identity_handler.try_unbind_threepid(
requester.user.to_string(),
{"address": address, "medium": medium, "id_server": id_server},
)
return 200, {"id_server_unbind_result": "success" if result else "no-support"}
class ThreepidDeleteRestServlet(RestServlet): class ThreepidDeleteRestServlet(RestServlet):
PATTERNS = client_patterns("/account/3pid/delete$") PATTERNS = client_patterns("/account/3pid/delete$")
@ -629,5 +634,6 @@ def register_servlets(hs, http_server):
EmailThreepidRequestTokenRestServlet(hs).register(http_server) EmailThreepidRequestTokenRestServlet(hs).register(http_server)
MsisdnThreepidRequestTokenRestServlet(hs).register(http_server) MsisdnThreepidRequestTokenRestServlet(hs).register(http_server)
ThreepidRestServlet(hs).register(http_server) ThreepidRestServlet(hs).register(http_server)
ThreepidUnbindRestServlet(hs).register(http_server)
ThreepidDeleteRestServlet(hs).register(http_server) ThreepidDeleteRestServlet(hs).register(http_server)
WhoamiRestServlet(hs).register(http_server) WhoamiRestServlet(hs).register(http_server)

View file

@ -28,16 +28,20 @@ from synapse.api.errors import (
Codes, Codes,
LimitExceededError, LimitExceededError,
SynapseError, SynapseError,
ThreepidValidationError,
UnrecognizedRequestError, UnrecognizedRequestError,
) )
from synapse.config.emailconfig import ThreepidBehaviour
from synapse.config.ratelimiting import FederationRateLimitConfig from synapse.config.ratelimiting import FederationRateLimitConfig
from synapse.config.server import is_threepid_reserved from synapse.config.server import is_threepid_reserved
from synapse.http.server import finish_request
from synapse.http.servlet import ( from synapse.http.servlet import (
RestServlet, RestServlet,
assert_params_in_dict, assert_params_in_dict,
parse_json_object_from_request, parse_json_object_from_request,
parse_string, parse_string,
) )
from synapse.push.mailer import load_jinja2_templates
from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.msisdn import phone_number_to_msisdn
from synapse.util.ratelimitutils import FederationRateLimiter from synapse.util.ratelimitutils import FederationRateLimiter
from synapse.util.threepids import check_3pid_allowed from synapse.util.threepids import check_3pid_allowed
@ -70,30 +74,92 @@ class EmailRegisterRequestTokenRestServlet(RestServlet):
super(EmailRegisterRequestTokenRestServlet, self).__init__() super(EmailRegisterRequestTokenRestServlet, self).__init__()
self.hs = hs self.hs = hs
self.identity_handler = hs.get_handlers().identity_handler self.identity_handler = hs.get_handlers().identity_handler
self.config = hs.config
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
from synapse.push.mailer import Mailer, load_jinja2_templates
template_html, template_text = load_jinja2_templates(
self.config.email_template_dir,
[
self.config.email_registration_template_html,
self.config.email_registration_template_text,
],
apply_format_ts_filter=True,
apply_mxc_to_http_filter=True,
public_baseurl=self.config.public_baseurl,
)
self.mailer = Mailer(
hs=self.hs,
app_name=self.config.email_app_name,
template_html=template_html,
template_text=template_text,
)
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request): def on_POST(self, request):
if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.OFF:
if self.hs.config.local_threepid_handling_disabled_due_to_email_config:
logger.warn(
"Email registration has been disabled due to lack of email config"
)
raise SynapseError(
400, "Email-based registration has been disabled on this server"
)
body = parse_json_object_from_request(request) body = parse_json_object_from_request(request)
assert_params_in_dict( assert_params_in_dict(body, ["client_secret", "email", "send_attempt"])
body, ["id_server", "client_secret", "email", "send_attempt"]
)
if not check_3pid_allowed(self.hs, "email", body["email"]): # Extract params from body
client_secret = body["client_secret"]
email = body["email"]
send_attempt = body["send_attempt"]
next_link = body.get("next_link") # Optional param
if not check_3pid_allowed(self.hs, "email", email):
raise SynapseError( raise SynapseError(
403, 403,
"Your email domain is not authorized to register on this server", "Your email domain is not authorized to register on this server",
Codes.THREEPID_DENIED, Codes.THREEPID_DENIED,
) )
existingUid = yield self.hs.get_datastore().get_user_id_by_threepid( existing_user_id = yield self.hs.get_datastore().get_user_id_by_threepid(
"email", body["email"] "email", body["email"]
) )
if existingUid is not None: if existing_user_id is not None:
raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE) raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE)
ret = yield self.identity_handler.requestEmailToken(**body) if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
if not self.hs.config.account_threepid_delegate_email:
logger.warn(
"No upstream email account_threepid_delegate configured on the server to "
"handle this request"
)
raise SynapseError(
400, "Registration by email is not supported on this homeserver"
)
ret = yield self.identity_handler.requestEmailToken(
self.hs.config.account_threepid_delegate_email,
email,
client_secret,
send_attempt,
next_link,
)
else:
# Send registration emails from Synapse
sid = yield self.identity_handler.send_threepid_validation(
email,
client_secret,
send_attempt,
self.mailer.send_registration_mail,
next_link,
)
# Wrap the session id in a JSON object
ret = {"sid": sid}
return 200, ret return 200, ret
@ -114,11 +180,15 @@ class MsisdnRegisterRequestTokenRestServlet(RestServlet):
body = parse_json_object_from_request(request) body = parse_json_object_from_request(request)
assert_params_in_dict( assert_params_in_dict(
body, body, ["client_secret", "country", "phone_number", "send_attempt"]
["id_server", "client_secret", "country", "phone_number", "send_attempt"],
) )
client_secret = body["client_secret"]
country = body["country"]
phone_number = body["phone_number"]
send_attempt = body["send_attempt"]
next_link = body.get("next_link") # Optional param
msisdn = phone_number_to_msisdn(body["country"], body["phone_number"]) msisdn = phone_number_to_msisdn(country, phone_number)
if not check_3pid_allowed(self.hs, "msisdn", msisdn): if not check_3pid_allowed(self.hs, "msisdn", msisdn):
raise SynapseError( raise SynapseError(
@ -127,19 +197,114 @@ class MsisdnRegisterRequestTokenRestServlet(RestServlet):
Codes.THREEPID_DENIED, Codes.THREEPID_DENIED,
) )
existingUid = yield self.hs.get_datastore().get_user_id_by_threepid( existing_user_id = yield self.hs.get_datastore().get_user_id_by_threepid(
"msisdn", msisdn "msisdn", msisdn
) )
if existingUid is not None: if existing_user_id is not None:
raise SynapseError( raise SynapseError(
400, "Phone number is already in use", Codes.THREEPID_IN_USE 400, "Phone number is already in use", Codes.THREEPID_IN_USE
) )
ret = yield self.identity_handler.requestMsisdnToken(**body) if not self.hs.config.account_threepid_delegate_msisdn:
logger.warn(
"No upstream msisdn account_threepid_delegate configured on the server to "
"handle this request"
)
raise SynapseError(
400, "Registration by phone number is not supported on this homeserver"
)
ret = yield self.identity_handler.requestMsisdnToken(
self.hs.config.account_threepid_delegate_msisdn,
country,
phone_number,
client_secret,
send_attempt,
next_link,
)
return 200, ret return 200, ret
class RegistrationSubmitTokenServlet(RestServlet):
"""Handles registration 3PID validation token submission"""
PATTERNS = client_patterns(
"/registration/(?P<medium>[^/]*)/submit_token$", releases=(), unstable=True
)
def __init__(self, hs):
"""
Args:
hs (synapse.server.HomeServer): server
"""
super(RegistrationSubmitTokenServlet, self).__init__()
self.hs = hs
self.auth = hs.get_auth()
self.config = hs.config
self.clock = hs.get_clock()
self.store = hs.get_datastore()
@defer.inlineCallbacks
def on_GET(self, request, medium):
if medium != "email":
raise SynapseError(
400, "This medium is currently not supported for registration"
)
if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF:
if self.config.local_threepid_handling_disabled_due_to_email_config:
logger.warn(
"User registration via email has been disabled due to lack of email config"
)
raise SynapseError(
400, "Email-based registration is disabled on this server"
)
sid = parse_string(request, "sid", required=True)
client_secret = parse_string(request, "client_secret", required=True)
token = parse_string(request, "token", required=True)
# Attempt to validate a 3PID session
try:
# Mark the session as valid
next_link = yield self.store.validate_threepid_session(
sid, client_secret, token, self.clock.time_msec()
)
# Perform a 302 redirect if next_link is set
if next_link:
if next_link.startswith("file:///"):
logger.warn(
"Not redirecting to next_link as it is a local file: address"
)
else:
request.setResponseCode(302)
request.setHeader("Location", next_link)
finish_request(request)
return None
# Otherwise show the success template
html = self.config.email_registration_template_success_html_content
request.setResponseCode(200)
except ThreepidValidationError as e:
# Show a failure page with a reason
request.setResponseCode(e.code)
# Show a failure page with a reason
html_template, = load_jinja2_templates(
self.config.email_template_dir,
[self.config.email_registration_template_failure_html],
)
template_vars = {"failure_reason": e.msg}
html = html_template.render(**template_vars)
request.write(html.encode("utf-8"))
finish_request(request)
class UsernameAvailabilityRestServlet(RestServlet): class UsernameAvailabilityRestServlet(RestServlet):
PATTERNS = client_patterns("/register/available") PATTERNS = client_patterns("/register/available")
@ -438,11 +603,11 @@ class RegisterRestServlet(RestServlet):
medium = auth_result[login_type]["medium"] medium = auth_result[login_type]["medium"]
address = auth_result[login_type]["address"] address = auth_result[login_type]["address"]
existingUid = yield self.store.get_user_id_by_threepid( existing_user_id = yield self.store.get_user_id_by_threepid(
medium, address medium, address
) )
if existingUid is not None: if existing_user_id is not None:
raise SynapseError( raise SynapseError(
400, 400,
"%s is already in use" % medium, "%s is already in use" % medium,
@ -550,4 +715,5 @@ def register_servlets(hs, http_server):
EmailRegisterRequestTokenRestServlet(hs).register(http_server) EmailRegisterRequestTokenRestServlet(hs).register(http_server)
MsisdnRegisterRequestTokenRestServlet(hs).register(http_server) MsisdnRegisterRequestTokenRestServlet(hs).register(http_server)
UsernameAvailabilityRestServlet(hs).register(http_server) UsernameAvailabilityRestServlet(hs).register(http_server)
RegistrationSubmitTokenServlet(hs).register(http_server)
RegisterRestServlet(hs).register(http_server) RegisterRestServlet(hs).register(http_server)

View file

@ -24,6 +24,10 @@ logger = logging.getLogger(__name__)
class VersionsRestServlet(RestServlet): class VersionsRestServlet(RestServlet):
PATTERNS = [re.compile("^/_matrix/client/versions$")] PATTERNS = [re.compile("^/_matrix/client/versions$")]
def __init__(self, hs):
super(VersionsRestServlet, self).__init__()
self.config = hs.config
def on_GET(self, request): def on_GET(self, request):
return ( return (
200, 200,
@ -49,5 +53,5 @@ class VersionsRestServlet(RestServlet):
) )
def register_servlets(http_server): def register_servlets(hs, http_server):
VersionsRestServlet().register(http_server) VersionsRestServlet(hs).register(http_server)

View file

@ -933,7 +933,7 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore):
"ts": now, "ts": now,
"opentracing_context": json.dumps(context) "opentracing_context": json.dumps(context)
if whitelisted_homeserver(destination) if whitelisted_homeserver(destination)
else None, else "{}",
} }
for destination in hosts for destination in hosts
for device_id in device_ids for device_id in device_ids

View file

@ -614,6 +614,85 @@ class RegistrationWorkerStore(SQLBaseStore):
# Convert the integer into a boolean. # Convert the integer into a boolean.
return res == 1 return res == 1
def get_threepid_validation_session(
self, medium, client_secret, address=None, sid=None, validated=True
):
"""Gets a session_id and last_send_attempt (if available) for a
client_secret/medium/(address|session_id) combo
Args:
medium (str|None): The medium of the 3PID
address (str|None): The address of the 3PID
sid (str|None): The ID of the validation session
client_secret (str|None): A unique string provided by the client to
help identify this validation attempt
validated (bool|None): Whether sessions should be filtered by
whether they have been validated already or not. None to
perform no filtering
Returns:
deferred {str, int}|None: A dict containing the
latest session_id and send_attempt count for this 3PID.
Otherwise None if there hasn't been a previous attempt
"""
keyvalues = {"medium": medium, "client_secret": client_secret}
if address:
keyvalues["address"] = address
if sid:
keyvalues["session_id"] = sid
assert address or sid
def get_threepid_validation_session_txn(txn):
sql = """
SELECT address, session_id, medium, client_secret,
last_send_attempt, validated_at
FROM threepid_validation_session WHERE %s
""" % (
" AND ".join("%s = ?" % k for k in iterkeys(keyvalues)),
)
if validated is not None:
sql += " AND validated_at IS " + ("NOT NULL" if validated else "NULL")
sql += " LIMIT 1"
txn.execute(sql, list(keyvalues.values()))
rows = self.cursor_to_dict(txn)
if not rows:
return None
return rows[0]
return self.runInteraction(
"get_threepid_validation_session", get_threepid_validation_session_txn
)
def delete_threepid_session(self, session_id):
"""Removes a threepid validation session from the database. This can
be done after validation has been performed and whatever action was
waiting on it has been carried out
Args:
session_id (str): The ID of the session to delete
"""
def delete_threepid_session_txn(txn):
self._simple_delete_txn(
txn,
table="threepid_validation_token",
keyvalues={"session_id": session_id},
)
self._simple_delete_txn(
txn,
table="threepid_validation_session",
keyvalues={"session_id": session_id},
)
return self.runInteraction(
"delete_threepid_session", delete_threepid_session_txn
)
class RegistrationStore( class RegistrationStore(
RegistrationWorkerStore, background_updates.BackgroundUpdateStore RegistrationWorkerStore, background_updates.BackgroundUpdateStore
@ -1082,60 +1161,6 @@ class RegistrationStore(
return 1 return 1
def get_threepid_validation_session(
self, medium, client_secret, address=None, sid=None, validated=True
):
"""Gets a session_id and last_send_attempt (if available) for a
client_secret/medium/(address|session_id) combo
Args:
medium (str|None): The medium of the 3PID
address (str|None): The address of the 3PID
sid (str|None): The ID of the validation session
client_secret (str|None): A unique string provided by the client to
help identify this validation attempt
validated (bool|None): Whether sessions should be filtered by
whether they have been validated already or not. None to
perform no filtering
Returns:
deferred {str, int}|None: A dict containing the
latest session_id and send_attempt count for this 3PID.
Otherwise None if there hasn't been a previous attempt
"""
keyvalues = {"medium": medium, "client_secret": client_secret}
if address:
keyvalues["address"] = address
if sid:
keyvalues["session_id"] = sid
assert address or sid
def get_threepid_validation_session_txn(txn):
sql = """
SELECT address, session_id, medium, client_secret,
last_send_attempt, validated_at
FROM threepid_validation_session WHERE %s
""" % (
" AND ".join("%s = ?" % k for k in iterkeys(keyvalues)),
)
if validated is not None:
sql += " AND validated_at IS " + ("NOT NULL" if validated else "NULL")
sql += " LIMIT 1"
txn.execute(sql, list(keyvalues.values()))
rows = self.cursor_to_dict(txn)
if not rows:
return None
return rows[0]
return self.runInteraction(
"get_threepid_validation_session", get_threepid_validation_session_txn
)
def validate_threepid_session(self, session_id, client_secret, token, current_ts): def validate_threepid_session(self, session_id, client_secret, token, current_ts):
"""Attempt to validate a threepid session using a token """Attempt to validate a threepid session using a token
@ -1323,31 +1348,6 @@ class RegistrationStore(
self.clock.time_msec(), self.clock.time_msec(),
) )
def delete_threepid_session(self, session_id):
"""Removes a threepid validation session from the database. This can
be done after validation has been performed and whatever action was
waiting on it has been carried out
Args:
session_id (str): The ID of the session to delete
"""
def delete_threepid_session_txn(txn):
self._simple_delete_txn(
txn,
table="threepid_validation_token",
keyvalues={"session_id": session_id},
)
self._simple_delete_txn(
txn,
table="threepid_validation_session",
keyvalues={"session_id": session_id},
)
return self.runInteraction(
"delete_threepid_session", delete_threepid_session_txn
)
def set_user_deactivated_status_txn(self, txn, user_id, deactivated): def set_user_deactivated_status_txn(self, txn, user_id, deactivated):
self._simple_update_one_txn( self._simple_update_one_txn(
txn=txn, txn=txn,

View file

@ -24,8 +24,10 @@ from canonicaljson import json
from twisted.internet import defer from twisted.internet import defer
from synapse.api.constants import EventTypes, Membership from synapse.api.constants import EventTypes, Membership
from synapse.metrics import LaterGauge
from synapse.metrics.background_process_metrics import run_as_background_process from synapse.metrics.background_process_metrics import run_as_background_process
from synapse.storage._base import LoggingTransaction from synapse.storage._base import LoggingTransaction
from synapse.storage.engines import Sqlite3Engine
from synapse.storage.events_worker import EventsWorkerStore from synapse.storage.events_worker import EventsWorkerStore
from synapse.types import get_domain_from_id from synapse.types import get_domain_from_id
from synapse.util.async_helpers import Linearizer from synapse.util.async_helpers import Linearizer
@ -74,6 +76,63 @@ class RoomMemberWorkerStore(EventsWorkerStore):
self._check_safe_current_state_events_membership_updated_txn(txn) self._check_safe_current_state_events_membership_updated_txn(txn)
txn.close() txn.close()
if self.hs.config.metrics_flags.known_servers:
self._known_servers_count = 1
self.hs.get_clock().looping_call(
run_as_background_process,
60 * 1000,
"_count_known_servers",
self._count_known_servers,
)
self.hs.get_clock().call_later(
1000,
run_as_background_process,
"_count_known_servers",
self._count_known_servers,
)
LaterGauge(
"synapse_federation_known_servers",
"",
[],
lambda: self._known_servers_count,
)
@defer.inlineCallbacks
def _count_known_servers(self):
"""
Count the servers that this server knows about.
The statistic is stored on the class for the
`synapse_federation_known_servers` LaterGauge to collect.
"""
def _transact(txn):
if isinstance(self.database_engine, Sqlite3Engine):
query = """
SELECT COUNT(DISTINCT substr(out.user_id, pos+1))
FROM (
SELECT rm.user_id as user_id, instr(rm.user_id, ':')
AS pos FROM room_memberships as rm
INNER JOIN current_state_events as c ON rm.event_id = c.event_id
WHERE c.type = 'm.room.member'
) as out
"""
else:
query = """
SELECT COUNT(DISTINCT split_part(state_key, ':', 2))
FROM current_state_events
WHERE type = 'm.room.member' AND membership = 'join';
"""
txn.execute(query)
return list(txn)[0][0]
count = yield self.runInteraction("get_known_servers", _transact)
# We always know about ourselves, even if we have nothing in
# room_memberships (for example, the server is new).
self._known_servers_count = max([count, 1])
return self._known_servers_count
def _check_safe_current_state_events_membership_updated_txn(self, txn): def _check_safe_current_state_events_membership_updated_txn(self, txn):
"""Checks if it is safe to assume the new current_state_events """Checks if it is safe to assume the new current_state_events
membership column is up to date membership column is up to date

View file

@ -823,7 +823,9 @@ class StatsStore(StateDeltasStore):
elif event.type == EventTypes.CanonicalAlias: elif event.type == EventTypes.CanonicalAlias:
room_state["canonical_alias"] = event.content.get("alias") room_state["canonical_alias"] = event.content.get("alias")
elif event.type == EventTypes.Create: elif event.type == EventTypes.Create:
room_state["is_federatable"] = event.content.get("m.federate", True) room_state["is_federatable"] = (
event.content.get("m.federate", True) is True
)
yield self.update_room_state(room_id, room_state) yield self.update_room_state(room_id, room_state)

View file

@ -17,6 +17,8 @@ import os.path
import re import re
import shutil import shutil
import tempfile import tempfile
from contextlib import redirect_stdout
from io import StringIO
from synapse.config.homeserver import HomeServerConfig from synapse.config.homeserver import HomeServerConfig
@ -32,17 +34,18 @@ class ConfigGenerationTestCase(unittest.TestCase):
shutil.rmtree(self.dir) shutil.rmtree(self.dir)
def test_generate_config_generates_files(self): def test_generate_config_generates_files(self):
HomeServerConfig.load_or_generate_config( with redirect_stdout(StringIO()):
"", HomeServerConfig.load_or_generate_config(
[ "",
"--generate-config", [
"-c", "--generate-config",
self.file, "-c",
"--report-stats=yes", self.file,
"-H", "--report-stats=yes",
"lemurs.win", "-H",
], "lemurs.win",
) ],
)
self.assertSetEqual( self.assertSetEqual(
set(["homeserver.yaml", "lemurs.win.log.config", "lemurs.win.signing.key"]), set(["homeserver.yaml", "lemurs.win.log.config", "lemurs.win.signing.key"]),

View file

@ -15,6 +15,8 @@
import os.path import os.path
import shutil import shutil
import tempfile import tempfile
from contextlib import redirect_stdout
from io import StringIO
import yaml import yaml
@ -26,7 +28,6 @@ from tests import unittest
class ConfigLoadingTestCase(unittest.TestCase): class ConfigLoadingTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
self.dir = tempfile.mkdtemp() self.dir = tempfile.mkdtemp()
print(self.dir)
self.file = os.path.join(self.dir, "homeserver.yaml") self.file = os.path.join(self.dir, "homeserver.yaml")
def tearDown(self): def tearDown(self):
@ -94,18 +95,27 @@ class ConfigLoadingTestCase(unittest.TestCase):
) )
self.assertTrue(config.enable_registration) self.assertTrue(config.enable_registration)
def test_stats_enabled(self):
self.generate_config_and_remove_lines_containing("enable_metrics")
self.add_lines_to_config(["enable_metrics: true"])
# The default Metrics Flags are off by default.
config = HomeServerConfig.load_config("", ["-c", self.file])
self.assertFalse(config.metrics_flags.known_servers)
def generate_config(self): def generate_config(self):
HomeServerConfig.load_or_generate_config( with redirect_stdout(StringIO()):
"", HomeServerConfig.load_or_generate_config(
[ "",
"--generate-config", [
"-c", "--generate-config",
self.file, "-c",
"--report-stats=yes", self.file,
"-H", "--report-stats=yes",
"lemurs.win", "-H",
], "lemurs.win",
) ],
)
def generate_config_and_remove_lines_containing(self, needle): def generate_config_and_remove_lines_containing(self, needle):
self.generate_config() self.generate_config()

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2014-2016 OpenMarket Ltd # Copyright 2014-2016 OpenMarket Ltd
# Copyright 2019 The Matrix.org Foundation C.I.C.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -13,78 +14,129 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from unittest.mock import Mock
from mock import Mock
from twisted.internet import defer
from synapse.api.constants import EventTypes, Membership from synapse.api.constants import EventTypes, Membership
from synapse.api.room_versions import RoomVersions from synapse.api.room_versions import RoomVersions
from synapse.types import Requester, RoomID, UserID from synapse.rest.admin import register_servlets_for_client_rest_resource
from synapse.rest.client.v1 import login, room
from synapse.types import Requester, UserID
from tests import unittest from tests import unittest
from tests.utils import create_room, setup_test_homeserver
class RoomMemberStoreTestCase(unittest.TestCase): class RoomMemberStoreTestCase(unittest.HomeserverTestCase):
@defer.inlineCallbacks
def setUp(self): servlets = [
hs = yield setup_test_homeserver( login.register_servlets,
self.addCleanup, resource_for_federation=Mock(), http_client=None register_servlets_for_client_rest_resource,
room.register_servlets,
]
def make_homeserver(self, reactor, clock):
hs = self.setup_test_homeserver(
resource_for_federation=Mock(), http_client=None
) )
return hs
def prepare(self, reactor, clock, hs):
# We can't test the RoomMemberStore on its own without the other event # We can't test the RoomMemberStore on its own without the other event
# storage logic # storage logic
self.store = hs.get_datastore() self.store = hs.get_datastore()
self.event_builder_factory = hs.get_event_builder_factory() self.event_builder_factory = hs.get_event_builder_factory()
self.event_creation_handler = hs.get_event_creation_handler() self.event_creation_handler = hs.get_event_creation_handler()
self.u_alice = UserID.from_string("@alice:test") self.u_alice = self.register_user("alice", "pass")
self.u_bob = UserID.from_string("@bob:test") self.t_alice = self.login("alice", "pass")
self.u_bob = self.register_user("bob", "pass")
# User elsewhere on another host # User elsewhere on another host
self.u_charlie = UserID.from_string("@charlie:elsewhere") self.u_charlie = UserID.from_string("@charlie:elsewhere")
self.room = RoomID.from_string("!abc123:test")
yield create_room(hs, self.room.to_string(), self.u_alice.to_string())
@defer.inlineCallbacks
def inject_room_member(self, room, user, membership, replaces_state=None): def inject_room_member(self, room, user, membership, replaces_state=None):
builder = self.event_builder_factory.for_room_version( builder = self.event_builder_factory.for_room_version(
RoomVersions.V1, RoomVersions.V1,
{ {
"type": EventTypes.Member, "type": EventTypes.Member,
"sender": user.to_string(), "sender": user,
"state_key": user.to_string(), "state_key": user,
"room_id": room.to_string(), "room_id": room,
"content": {"membership": membership}, "content": {"membership": membership},
}, },
) )
event, context = yield self.event_creation_handler.create_new_client_event( event, context = self.get_success(
builder self.event_creation_handler.create_new_client_event(builder)
) )
yield self.store.persist_event(event, context) self.get_success(self.store.persist_event(event, context))
return event return event
@defer.inlineCallbacks
def test_one_member(self): def test_one_member(self):
yield self.inject_room_member(self.room, self.u_alice, Membership.JOIN)
self.assertEquals( # Alice creates the room, and is automatically joined
[self.room.to_string()], self.room = self.helper.create_room_as(self.u_alice, tok=self.t_alice)
[
m.room_id rooms_for_user = self.get_success(
for m in ( self.store.get_rooms_for_user_where_membership_is(
yield self.store.get_rooms_for_user_where_membership_is( self.u_alice, [Membership.JOIN]
self.u_alice.to_string(), [Membership.JOIN] )
)
)
],
) )
self.assertEquals([self.room], [m.room_id for m in rooms_for_user])
def test_count_known_servers(self):
"""
_count_known_servers will calculate how many servers are in a room.
"""
self.room = self.helper.create_room_as(self.u_alice, tok=self.t_alice)
self.inject_room_member(self.room, self.u_bob, Membership.JOIN)
self.inject_room_member(self.room, self.u_charlie.to_string(), Membership.JOIN)
servers = self.get_success(self.store._count_known_servers())
self.assertEqual(servers, 2)
def test_count_known_servers_stat_counter_disabled(self):
"""
If enabled, the metrics for how many servers are known will be counted.
"""
self.assertTrue("_known_servers_count" not in self.store.__dict__.keys())
self.room = self.helper.create_room_as(self.u_alice, tok=self.t_alice)
self.inject_room_member(self.room, self.u_bob, Membership.JOIN)
self.inject_room_member(self.room, self.u_charlie.to_string(), Membership.JOIN)
self.pump(20)
self.assertTrue("_known_servers_count" not in self.store.__dict__.keys())
@unittest.override_config(
{"enable_metrics": True, "metrics_flags": {"known_servers": True}}
)
def test_count_known_servers_stat_counter_enabled(self):
"""
If enabled, the metrics for how many servers are known will be counted.
"""
# Initialises to 1 -- itself
self.assertEqual(self.store._known_servers_count, 1)
self.pump(20)
# No rooms have been joined, so technically the SQL returns 0, but it
# will still say it knows about itself.
self.assertEqual(self.store._known_servers_count, 1)
self.room = self.helper.create_room_as(self.u_alice, tok=self.t_alice)
self.inject_room_member(self.room, self.u_bob, Membership.JOIN)
self.inject_room_member(self.room, self.u_charlie.to_string(), Membership.JOIN)
self.pump(20)
# It now knows about Charlie's server.
self.assertEqual(self.store._known_servers_count, 2)
class CurrentStateMembershipUpdateTestCase(unittest.HomeserverTestCase): class CurrentStateMembershipUpdateTestCase(unittest.HomeserverTestCase):
def prepare(self, reactor, clock, homeserver): def prepare(self, reactor, clock, homeserver):