forked from MirrorHub/synapse
Merge branch 'develop' into uhoreg/e2e_cross-signing_merged
This commit is contained in:
commit
8e86f5b65c
61 changed files with 1486 additions and 583 deletions
63
UPGRADE.rst
63
UPGRADE.rst
|
@ -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
1
changelog.d/5835.feature
Normal 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
1
changelog.d/5868.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add `m.require_identity_server` key to `/versions`'s `unstable_features` section.
|
1
changelog.d/5875.misc
Normal file
1
changelog.d/5875.misc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Deprecate the `trusted_third_party_id_servers` option.
|
1
changelog.d/5876.feature
Normal file
1
changelog.d/5876.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Replace `trust_identity_server_for_password_resets` config option with `account_threepid_delegates`.
|
1
changelog.d/5892.misc
Normal file
1
changelog.d/5892.misc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Compatibility with v2 Identity Service APIs other than /lookup.
|
1
changelog.d/5940.feature
Normal file
1
changelog.d/5940.feature
Normal 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
1
changelog.d/5969.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Replace `trust_identity_server_for_password_resets` config option with `account_threepid_delegates`.
|
1
changelog.d/5980.feature
Normal file
1
changelog.d/5980.feature
Normal 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
1
changelog.d/5981.feature
Normal 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
1
changelog.d/5982.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Include missing opentracing contexts in outbout replication requests.
|
1
changelog.d/5983.feature
Normal file
1
changelog.d/5983.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add minimum opentracing for client servlets.
|
1
changelog.d/5984.bugfix
Normal file
1
changelog.d/5984.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fix sending of EDUs when opentracing is enabled with an empty whitelist.
|
1
changelog.d/5986.feature
Normal file
1
changelog.d/5986.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Trace replication send times.
|
1
changelog.d/5988.bugfix
Normal file
1
changelog.d/5988.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fix invalid references to None while opentracing if the log context slips.
|
1
changelog.d/5991.bugfix
Normal file
1
changelog.d/5991.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fix invalid references to None while opentracing if the log context slips.
|
1
changelog.d/5993.feature
Normal file
1
changelog.d/5993.feature
Normal 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
1
changelog.d/5994.feature
Normal 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
1
changelog.d/5995.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Return a M_MISSING_PARAM if `sid` is not provided to `/account/3pid`.
|
1
changelog.d/5998.bugfix
Normal file
1
changelog.d/5998.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fix room and user stats tracking.
|
|
@ -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"
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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})
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
#
|
#
|
||||||
|
|
|
@ -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):
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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={
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 "",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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>
|
||||||
|
|
11
synapse/res/templates/registration.html
Normal file
11
synapse/res/templates/registration.html
Normal 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>
|
10
synapse/res/templates/registration.txt
Normal file
10
synapse/res/templates/registration.txt
Normal 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.
|
6
synapse/res/templates/registration_failure.html
Normal file
6
synapse/res/templates/registration_failure.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<html>
|
||||||
|
<head></head>
|
||||||
|
<body>
|
||||||
|
<p>Validation failed for the following reason: {{ failure_reason }}.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
6
synapse/res/templates/registration_success.html
Normal file
6
synapse/res/templates/registration_success.html
Normal 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>
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"]),
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in a new issue