Merge remote-tracking branch 'origin/release-v1.21.2' into bbz/info-mainline-1.21.2
This commit is contained in:
+157
-3
@@ -1,3 +1,155 @@
|
||||
Synapse 1.21.1 (2020-10-13)
|
||||
===========================
|
||||
|
||||
This release fixes a regression in v1.21.0 that prevented debian packages from being built.
|
||||
It is otherwise identical to v1.21.0.
|
||||
|
||||
Synapse 1.21.0 (2020-10-12)
|
||||
===========================
|
||||
|
||||
No significant changes since v1.21.0rc3.
|
||||
|
||||
As [noted in
|
||||
v1.20.0](https://github.com/matrix-org/synapse/blob/release-v1.21.0/CHANGES.md#synapse-1200-2020-09-22),
|
||||
a future release will drop support for accessing Synapse's
|
||||
[Admin API](https://github.com/matrix-org/synapse/tree/master/docs/admin_api) under the
|
||||
`/_matrix/client/*` endpoint prefixes. At that point, the Admin API will only
|
||||
be accessible under `/_synapse/admin`.
|
||||
|
||||
|
||||
Synapse 1.21.0rc3 (2020-10-08)
|
||||
==============================
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix duplication of events on high traffic servers, caused by PostgreSQL `could not serialize access due to concurrent update` errors. ([\#8456](https://github.com/matrix-org/synapse/issues/8456))
|
||||
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Add Groovy Gorilla to the list of distributions we build `.deb`s for. ([\#8475](https://github.com/matrix-org/synapse/issues/8475))
|
||||
|
||||
|
||||
Synapse 1.21.0rc2 (2020-10-02)
|
||||
==============================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Convert additional templates from inline HTML to Jinja2 templates. ([\#8444](https://github.com/matrix-org/synapse/issues/8444))
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix a regression in v1.21.0rc1 which broke thumbnails of remote media. ([\#8438](https://github.com/matrix-org/synapse/issues/8438))
|
||||
- Do not expose the experimental `uk.half-shot.msc2778.login.application_service` flow in the login API, which caused a compatibility problem with Element iOS. ([\#8440](https://github.com/matrix-org/synapse/issues/8440))
|
||||
- Fix malformed log line in new federation "catch up" logic. ([\#8442](https://github.com/matrix-org/synapse/issues/8442))
|
||||
- Fix DB query on startup for negative streams which caused long start up times. Introduced in [\#8374](https://github.com/matrix-org/synapse/issues/8374). ([\#8447](https://github.com/matrix-org/synapse/issues/8447))
|
||||
|
||||
|
||||
Synapse 1.21.0rc1 (2020-10-01)
|
||||
==============================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Require the user to confirm that their password should be reset after clicking the email confirmation link. ([\#8004](https://github.com/matrix-org/synapse/issues/8004))
|
||||
- Add an admin API `GET /_synapse/admin/v1/event_reports` to read entries of table `event_reports`. Contributed by @dklimpel. ([\#8217](https://github.com/matrix-org/synapse/issues/8217))
|
||||
- Consolidate the SSO error template across all configuration. ([\#8248](https://github.com/matrix-org/synapse/issues/8248), [\#8405](https://github.com/matrix-org/synapse/issues/8405))
|
||||
- Add a configuration option to specify a whitelist of domains that a user can be redirected to after validating their email or phone number. ([\#8275](https://github.com/matrix-org/synapse/issues/8275), [\#8417](https://github.com/matrix-org/synapse/issues/8417))
|
||||
- Add experimental support for sharding event persister. ([\#8294](https://github.com/matrix-org/synapse/issues/8294), [\#8387](https://github.com/matrix-org/synapse/issues/8387), [\#8396](https://github.com/matrix-org/synapse/issues/8396), [\#8419](https://github.com/matrix-org/synapse/issues/8419))
|
||||
- Add the room topic and avatar to the room details admin API. ([\#8305](https://github.com/matrix-org/synapse/issues/8305))
|
||||
- Add an admin API for querying rooms where a user is a member. Contributed by @dklimpel. ([\#8306](https://github.com/matrix-org/synapse/issues/8306))
|
||||
- Add `uk.half-shot.msc2778.login.application_service` login type to allow appservices to login. ([\#8320](https://github.com/matrix-org/synapse/issues/8320))
|
||||
- Add a configuration option that allows existing users to log in with OpenID Connect. Contributed by @BBBSnowball and @OmmyZhang. ([\#8345](https://github.com/matrix-org/synapse/issues/8345))
|
||||
- Add prometheus metrics for replication requests. ([\#8406](https://github.com/matrix-org/synapse/issues/8406))
|
||||
- Support passing additional single sign-on parameters to the client. ([\#8413](https://github.com/matrix-org/synapse/issues/8413))
|
||||
- Add experimental reporting of metrics on expensive rooms for state-resolution. ([\#8420](https://github.com/matrix-org/synapse/issues/8420))
|
||||
- Add experimental prometheus metric to track numbers of "large" rooms for state resolutiom. ([\#8425](https://github.com/matrix-org/synapse/issues/8425))
|
||||
- Add prometheus metrics to track federation delays. ([\#8430](https://github.com/matrix-org/synapse/issues/8430))
|
||||
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Fix a bug in the media repository where remote thumbnails with the same size but different crop methods would overwrite each other. Contributed by @deepbluev7. ([\#7124](https://github.com/matrix-org/synapse/issues/7124))
|
||||
- Fix inconsistent handling of non-existent push rules, and stop tracking the `enabled` state of removed push rules. ([\#7796](https://github.com/matrix-org/synapse/issues/7796))
|
||||
- Fix a longstanding bug when storing a media file with an empty `upload_name`. ([\#7905](https://github.com/matrix-org/synapse/issues/7905))
|
||||
- Fix messages not being sent over federation until an event is sent into the same room. ([\#8230](https://github.com/matrix-org/synapse/issues/8230), [\#8247](https://github.com/matrix-org/synapse/issues/8247), [\#8258](https://github.com/matrix-org/synapse/issues/8258), [\#8272](https://github.com/matrix-org/synapse/issues/8272), [\#8322](https://github.com/matrix-org/synapse/issues/8322))
|
||||
- Fix a longstanding bug where files that could not be thumbnailed would result in an Internal Server Error. ([\#8236](https://github.com/matrix-org/synapse/issues/8236), [\#8435](https://github.com/matrix-org/synapse/issues/8435))
|
||||
- Upgrade minimum version of `canonicaljson` to version 1.4.0, to fix an unicode encoding issue. ([\#8262](https://github.com/matrix-org/synapse/issues/8262))
|
||||
- Fix longstanding bug which could lead to incomplete database upgrades on SQLite. ([\#8265](https://github.com/matrix-org/synapse/issues/8265))
|
||||
- Fix stack overflow when stderr is redirected to the logging system, and the logging system encounters an error. ([\#8268](https://github.com/matrix-org/synapse/issues/8268))
|
||||
- Fix a bug which cause the logging system to report errors, if `DEBUG` was enabled and no `context` filter was applied. ([\#8278](https://github.com/matrix-org/synapse/issues/8278))
|
||||
- Fix edge case where push could get delayed for a user until a later event was pushed. ([\#8287](https://github.com/matrix-org/synapse/issues/8287))
|
||||
- Fix fetching malformed events from remote servers. ([\#8324](https://github.com/matrix-org/synapse/issues/8324))
|
||||
- Fix `UnboundLocalError` from occuring when appservices send a malformed register request. ([\#8329](https://github.com/matrix-org/synapse/issues/8329))
|
||||
- Don't send push notifications to expired user accounts. ([\#8353](https://github.com/matrix-org/synapse/issues/8353))
|
||||
- Fix a regression in v1.19.0 with reactivating users through the admin API. ([\#8362](https://github.com/matrix-org/synapse/issues/8362))
|
||||
- Fix a bug where during device registration the length of the device name wasn't limited. ([\#8364](https://github.com/matrix-org/synapse/issues/8364))
|
||||
- Include `guest_access` in the fields that are checked for null bytes when updating `room_stats_state`. Broke in v1.7.2. ([\#8373](https://github.com/matrix-org/synapse/issues/8373))
|
||||
- Fix theoretical race condition where events are not sent down `/sync` if the synchrotron worker is restarted without restarting other workers. ([\#8374](https://github.com/matrix-org/synapse/issues/8374))
|
||||
- Fix a bug which could cause errors in rooms with malformed membership events, on servers using sqlite. ([\#8385](https://github.com/matrix-org/synapse/issues/8385))
|
||||
- Fix "Re-starting finished log context" warning when receiving an event we already had over federation. ([\#8398](https://github.com/matrix-org/synapse/issues/8398))
|
||||
- Fix incorrect handling of timeouts on outgoing HTTP requests. ([\#8400](https://github.com/matrix-org/synapse/issues/8400))
|
||||
- Fix a regression in v1.20.0 in the `synapse_port_db` script regarding the `ui_auth_sessions_ips` table. ([\#8410](https://github.com/matrix-org/synapse/issues/8410))
|
||||
- Remove unnecessary 3PID registration check when resetting password via an email address. Bug introduced in v0.34.0rc2. ([\#8414](https://github.com/matrix-org/synapse/issues/8414))
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Add `/_synapse/client` to the reverse proxy documentation. ([\#8227](https://github.com/matrix-org/synapse/issues/8227))
|
||||
- Add note to the reverse proxy settings documentation about disabling Apache's mod_security2. Contributed by Julian Fietkau (@jfietkau). ([\#8375](https://github.com/matrix-org/synapse/issues/8375))
|
||||
- Improve description of `server_name` config option in `homserver.yaml`. ([\#8415](https://github.com/matrix-org/synapse/issues/8415))
|
||||
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- Drop support for `prometheus_client` older than 0.4.0. ([\#8426](https://github.com/matrix-org/synapse/issues/8426))
|
||||
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Fix tests on distros which disable TLSv1.0. Contributed by @danc86. ([\#8208](https://github.com/matrix-org/synapse/issues/8208))
|
||||
- Simplify the distributor code to avoid unnecessary work. ([\#8216](https://github.com/matrix-org/synapse/issues/8216))
|
||||
- Remove the `populate_stats_process_rooms_2` background job and restore functionality to `populate_stats_process_rooms`. ([\#8243](https://github.com/matrix-org/synapse/issues/8243))
|
||||
- Clean up type hints for `PaginationConfig`. ([\#8250](https://github.com/matrix-org/synapse/issues/8250), [\#8282](https://github.com/matrix-org/synapse/issues/8282))
|
||||
- Track the latest event for every destination and room for catch-up after federation outage. ([\#8256](https://github.com/matrix-org/synapse/issues/8256))
|
||||
- Fix non-user visible bug in implementation of `MultiWriterIdGenerator.get_current_token_for_writer`. ([\#8257](https://github.com/matrix-org/synapse/issues/8257))
|
||||
- Switch to the JSON implementation from the standard library. ([\#8259](https://github.com/matrix-org/synapse/issues/8259))
|
||||
- Add type hints to `synapse.util.async_helpers`. ([\#8260](https://github.com/matrix-org/synapse/issues/8260))
|
||||
- Simplify tests that mock asynchronous functions. ([\#8261](https://github.com/matrix-org/synapse/issues/8261))
|
||||
- Add type hints to `StreamToken` and `RoomStreamToken` classes. ([\#8279](https://github.com/matrix-org/synapse/issues/8279))
|
||||
- Change `StreamToken.room_key` to be a `RoomStreamToken` instance. ([\#8281](https://github.com/matrix-org/synapse/issues/8281))
|
||||
- Refactor notifier code to correctly use the max event stream position. ([\#8288](https://github.com/matrix-org/synapse/issues/8288))
|
||||
- Use slotted classes where possible. ([\#8296](https://github.com/matrix-org/synapse/issues/8296))
|
||||
- Support testing the local Synapse checkout against the [Complement homeserver test suite](https://github.com/matrix-org/complement/). ([\#8317](https://github.com/matrix-org/synapse/issues/8317))
|
||||
- Update outdated usages of `metaclass` to python 3 syntax. ([\#8326](https://github.com/matrix-org/synapse/issues/8326))
|
||||
- Move lint-related dependencies to package-extra field, update CONTRIBUTING.md to utilise this. ([\#8330](https://github.com/matrix-org/synapse/issues/8330), [\#8377](https://github.com/matrix-org/synapse/issues/8377))
|
||||
- Use the `admin_patterns` helper in additional locations. ([\#8331](https://github.com/matrix-org/synapse/issues/8331))
|
||||
- Fix test logging to allow braces in log output. ([\#8335](https://github.com/matrix-org/synapse/issues/8335))
|
||||
- Remove `__future__` imports related to Python 2 compatibility. ([\#8337](https://github.com/matrix-org/synapse/issues/8337))
|
||||
- Simplify `super()` calls to Python 3 syntax. ([\#8344](https://github.com/matrix-org/synapse/issues/8344))
|
||||
- Fix bad merge from `release-v1.20.0` branch to `develop`. ([\#8354](https://github.com/matrix-org/synapse/issues/8354))
|
||||
- Factor out a `_send_dummy_event_for_room` method. ([\#8370](https://github.com/matrix-org/synapse/issues/8370))
|
||||
- Improve logging of state resolution. ([\#8371](https://github.com/matrix-org/synapse/issues/8371))
|
||||
- Add type annotations to `SimpleHttpClient`. ([\#8372](https://github.com/matrix-org/synapse/issues/8372))
|
||||
- Refactor ID generators to use `async with` syntax. ([\#8383](https://github.com/matrix-org/synapse/issues/8383))
|
||||
- Add `EventStreamPosition` type. ([\#8388](https://github.com/matrix-org/synapse/issues/8388))
|
||||
- Create a mechanism for marking tests "logcontext clean". ([\#8399](https://github.com/matrix-org/synapse/issues/8399))
|
||||
- A pair of tiny cleanups in the federation request code. ([\#8401](https://github.com/matrix-org/synapse/issues/8401))
|
||||
- Add checks on startup that PostgreSQL sequences are consistent with their associated tables. ([\#8402](https://github.com/matrix-org/synapse/issues/8402))
|
||||
- Do not include appservice users when calculating the total MAU for a server. ([\#8404](https://github.com/matrix-org/synapse/issues/8404))
|
||||
- Typing fixes for `synapse.handlers.federation`. ([\#8422](https://github.com/matrix-org/synapse/issues/8422))
|
||||
- Various refactors to simplify stream token handling. ([\#8423](https://github.com/matrix-org/synapse/issues/8423))
|
||||
- Make stream token serializing/deserializing async. ([\#8427](https://github.com/matrix-org/synapse/issues/8427))
|
||||
|
||||
|
||||
Synapse 1.20.1 (2020-09-24)
|
||||
===========================
|
||||
|
||||
@@ -21,9 +173,11 @@ API](https://github.com/matrix-org/synapse/tree/master/docs) has been
|
||||
accessible under the `/_matrix/client/api/v1/admin`,
|
||||
`/_matrix/client/unstable/admin`, `/_matrix/client/r0/admin` and
|
||||
`/_synapse/admin` prefixes. In a future release, we will be dropping support
|
||||
for accessing Synapse's Admin API using the `/_matrix/client/*` prefixes. This
|
||||
makes it easier for homeserver admins to lock down external access to the Admin
|
||||
API endpoints.
|
||||
for accessing Synapse's Admin API using the `/_matrix/client/*` prefixes.
|
||||
|
||||
From that point, the Admin API will only be accessible under `/_synapse/admin`.
|
||||
This makes it easier for homeserver admins to lock down external access to the
|
||||
Admin API endpoints.
|
||||
|
||||
Synapse 1.20.0rc5 (2020-09-18)
|
||||
==============================
|
||||
|
||||
+3
-3
@@ -17,9 +17,9 @@ https://help.github.com/articles/using-pull-requests/) to ask us to pull your
|
||||
changes into our repo.
|
||||
|
||||
Some other points to follow:
|
||||
|
||||
|
||||
* Please base your changes on the `develop` branch.
|
||||
|
||||
|
||||
* Please follow the [code style requirements](#code-style).
|
||||
|
||||
* Please include a [changelog entry](#changelog) with each PR.
|
||||
@@ -46,7 +46,7 @@ locally. You'll need python 3.6 or later, and to install a number of tools:
|
||||
|
||||
```
|
||||
# Install the dependencies
|
||||
pip install -U black flake8 flake8-comprehensions isort
|
||||
pip install -e ".[lint]"
|
||||
|
||||
# Run the linter script
|
||||
./scripts-dev/lint.sh
|
||||
|
||||
+53
@@ -75,6 +75,59 @@ for example:
|
||||
wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb
|
||||
dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb
|
||||
|
||||
Upgrading to v1.21.0
|
||||
====================
|
||||
|
||||
Forwarding ``/_synapse/client`` through your reverse proxy
|
||||
----------------------------------------------------------
|
||||
|
||||
The `reverse proxy documentation
|
||||
<https://github.com/matrix-org/synapse/blob/develop/docs/reverse_proxy.md>`_ has been updated
|
||||
to include reverse proxy directives for ``/_synapse/client/*`` endpoints. As the user password
|
||||
reset flow now uses endpoints under this prefix, **you must update your reverse proxy
|
||||
configurations for user password reset to work**.
|
||||
|
||||
Additionally, note that the `Synapse worker documentation
|
||||
<https://github.com/matrix-org/synapse/blob/develop/docs/workers.md>`_ has been updated to
|
||||
state that the ``/_synapse/client/password_reset/email/submit_token`` endpoint can be handled
|
||||
by all workers. If you make use of Synapse's worker feature, please update your reverse proxy
|
||||
configuration to reflect this change.
|
||||
|
||||
New HTML templates
|
||||
------------------
|
||||
|
||||
A new HTML template,
|
||||
`password_reset_confirmation.html <https://github.com/matrix-org/synapse/blob/develop/synapse/res/templates/password_reset_confirmation.html>`_,
|
||||
has been added to the ``synapse/res/templates`` directory. If you are using a
|
||||
custom template directory, you may want to copy the template over and modify it.
|
||||
|
||||
Note that as of v1.20.0, templates do not need to be included in custom template
|
||||
directories for Synapse to start. The default templates will be used if a custom
|
||||
template cannot be found.
|
||||
|
||||
This page will appear to the user after clicking a password reset link that has
|
||||
been emailed to them.
|
||||
|
||||
To complete password reset, the page must include a way to make a `POST`
|
||||
request to
|
||||
``/_synapse/client/password_reset/{medium}/submit_token``
|
||||
with the query parameters from the original link, presented as a URL-encoded form. See the file
|
||||
itself for more details.
|
||||
|
||||
Updated Single Sign-on HTML Templates
|
||||
-------------------------------------
|
||||
|
||||
The ``saml_error.html`` template was removed from Synapse and replaced with the
|
||||
``sso_error.html`` template. If your Synapse is configured to use SAML and a
|
||||
custom ``sso_redirect_confirm_template_dir`` configuration then any customisations
|
||||
of the ``saml_error.html`` template will need to be merged into the ``sso_error.html``
|
||||
template. These templates are similar, but the parameters are slightly different:
|
||||
|
||||
* The ``msg`` parameter should be renamed to ``error_description``.
|
||||
* There is no longer a ``code`` parameter for the response code.
|
||||
* A string ``error`` parameter is available that includes a short hint of why a
|
||||
user is seeing the error page.
|
||||
|
||||
Upgrading to v1.18.0
|
||||
====================
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Fix rare bug where sending an event would fail due to a racey assertion.
|
||||
@@ -15,8 +15,6 @@
|
||||
# limitations under the License.
|
||||
|
||||
""" Starts a synapse client console. """
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import cmd
|
||||
import getpass
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import json
|
||||
import urllib
|
||||
from pprint import pformat
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import cgi
|
||||
import datetime
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import cgi
|
||||
import datetime
|
||||
|
||||
@@ -10,8 +10,6 @@ the bridge.
|
||||
Requires:
|
||||
npm install jquery jsdom
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
from __future__ import print_function
|
||||
|
||||
import json
|
||||
import sys
|
||||
@@ -8,11 +7,6 @@ from argparse import ArgumentParser
|
||||
|
||||
import requests
|
||||
|
||||
try:
|
||||
raw_input
|
||||
except NameError: # Python 3
|
||||
raw_input = input
|
||||
|
||||
|
||||
def _mkurl(template, kws):
|
||||
for key in kws:
|
||||
@@ -58,7 +52,7 @@ def main(hs, room_id, access_token, user_id_prefix, why):
|
||||
print("The following user IDs will be kicked from %s" % room_name)
|
||||
for uid in kick_list:
|
||||
print(uid)
|
||||
doit = raw_input("Continue? [Y]es\n")
|
||||
doit = input("Continue? [Y]es\n")
|
||||
if len(doit) > 0 and doit.lower() == "y":
|
||||
print("Kicking members...")
|
||||
# encode them all
|
||||
|
||||
Vendored
+1
-1
@@ -42,7 +42,7 @@ dh_virtualenv \
|
||||
--preinstall="mock" \
|
||||
--extra-pip-arg="--no-cache-dir" \
|
||||
--extra-pip-arg="--compile" \
|
||||
--extras="all,systemd"
|
||||
--extras="all,systemd,test"
|
||||
|
||||
PACKAGE_BUILD_DIR="debian/matrix-synapse-py3"
|
||||
VIRTUALENV_DIR="${PACKAGE_BUILD_DIR}${DH_VIRTUALENV_INSTALL_ROOT}/matrix-synapse"
|
||||
|
||||
Vendored
+16
@@ -1,3 +1,19 @@
|
||||
matrix-synapse-py3 (1.21.1) stable; urgency=medium
|
||||
|
||||
[ Synapse Packaging team ]
|
||||
* New synapse release 1.21.1.
|
||||
|
||||
[ Andrew Morgan ]
|
||||
* Explicitly install "test" python dependencies.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Tue, 13 Oct 2020 10:24:13 +0100
|
||||
|
||||
matrix-synapse-py3 (1.21.0) stable; urgency=medium
|
||||
|
||||
* New synapse release 1.21.0.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Mon, 12 Oct 2020 15:47:44 +0100
|
||||
|
||||
matrix-synapse-py3 (1.20.1) stable; urgency=medium
|
||||
|
||||
* New synapse release 1.20.1.
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
Show reported events
|
||||
====================
|
||||
|
||||
This API returns information about reported events.
|
||||
|
||||
The api is::
|
||||
|
||||
GET /_synapse/admin/v1/event_reports?from=0&limit=10
|
||||
|
||||
To use it, you will need to authenticate by providing an ``access_token`` for a
|
||||
server admin: see `README.rst <README.rst>`_.
|
||||
|
||||
It returns a JSON body like the following:
|
||||
|
||||
.. code:: jsonc
|
||||
|
||||
{
|
||||
"event_reports": [
|
||||
{
|
||||
"content": {
|
||||
"reason": "foo",
|
||||
"score": -100
|
||||
},
|
||||
"event_id": "$bNUFCwGzWca1meCGkjp-zwslF-GfVcXukvRLI1_FaVY",
|
||||
"event_json": {
|
||||
"auth_events": [
|
||||
"$YK4arsKKcc0LRoe700pS8DSjOvUT4NDv0HfInlMFw2M",
|
||||
"$oggsNXxzPFRE3y53SUNd7nsj69-QzKv03a1RucHu-ws"
|
||||
],
|
||||
"content": {
|
||||
"body": "matrix.org: This Week in Matrix",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<strong>matrix.org</strong>:<br><a href=\"https://matrix.org/blog/\"><strong>This Week in Matrix</strong></a>",
|
||||
"msgtype": "m.notice"
|
||||
},
|
||||
"depth": 546,
|
||||
"hashes": {
|
||||
"sha256": "xK1//xnmvHJIOvbgXlkI8eEqdvoMmihVDJ9J4SNlsAw"
|
||||
},
|
||||
"origin": "matrix.org",
|
||||
"origin_server_ts": 1592291711430,
|
||||
"prev_events": [
|
||||
"$YK4arsKKcc0LRoe700pS8DSjOvUT4NDv0HfInlMFw2M"
|
||||
],
|
||||
"prev_state": [],
|
||||
"room_id": "!ERAgBpSOcCCuTJqQPk:matrix.org",
|
||||
"sender": "@foobar:matrix.org",
|
||||
"signatures": {
|
||||
"matrix.org": {
|
||||
"ed25519:a_JaEG": "cs+OUKW/iHx5pEidbWxh0UiNNHwe46Ai9LwNz+Ah16aWDNszVIe2gaAcVZfvNsBhakQTew51tlKmL2kspXk/Dg"
|
||||
}
|
||||
},
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age_ts": 1592291711430,
|
||||
}
|
||||
},
|
||||
"id": 2,
|
||||
"reason": "foo",
|
||||
"received_ts": 1570897107409,
|
||||
"room_alias": "#alias1:matrix.org",
|
||||
"room_id": "!ERAgBpSOcCCuTJqQPk:matrix.org",
|
||||
"sender": "@foobar:matrix.org",
|
||||
"user_id": "@foo:matrix.org"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"reason": "bar",
|
||||
"score": -100
|
||||
},
|
||||
"event_id": "$3IcdZsDaN_En-S1DF4EMCy3v4gNRKeOJs8W5qTOKj4I",
|
||||
"event_json": {
|
||||
// hidden items
|
||||
// see above
|
||||
},
|
||||
"id": 3,
|
||||
"reason": "bar",
|
||||
"received_ts": 1598889612059,
|
||||
"room_alias": "#alias2:matrix.org",
|
||||
"room_id": "!eGvUQuTCkHGVwNMOjv:matrix.org",
|
||||
"sender": "@foobar:matrix.org",
|
||||
"user_id": "@bar:matrix.org"
|
||||
}
|
||||
],
|
||||
"next_token": 2,
|
||||
"total": 4
|
||||
}
|
||||
|
||||
To paginate, check for ``next_token`` and if present, call the endpoint again
|
||||
with ``from`` set to the value of ``next_token``. This will return a new page.
|
||||
|
||||
If the endpoint does not return a ``next_token`` then there are no more
|
||||
reports to paginate through.
|
||||
|
||||
**URL parameters:**
|
||||
|
||||
- ``limit``: integer - Is optional but is used for pagination,
|
||||
denoting the maximum number of items to return in this call. Defaults to ``100``.
|
||||
- ``from``: integer - Is optional but used for pagination,
|
||||
denoting the offset in the returned results. This should be treated as an opaque value and
|
||||
not explicitly set to anything other than the return value of ``next_token`` from a previous call.
|
||||
Defaults to ``0``.
|
||||
- ``dir``: string - Direction of event report order. Whether to fetch the most recent first (``b``) or the
|
||||
oldest first (``f``). Defaults to ``b``.
|
||||
- ``user_id``: string - Is optional and filters to only return users with user IDs that contain this value.
|
||||
This is the user who reported the event and wrote the reason.
|
||||
- ``room_id``: string - Is optional and filters to only return rooms with room IDs that contain this value.
|
||||
|
||||
**Response**
|
||||
|
||||
The following fields are returned in the JSON response body:
|
||||
|
||||
- ``id``: integer - ID of event report.
|
||||
- ``received_ts``: integer - The timestamp (in milliseconds since the unix epoch) when this report was sent.
|
||||
- ``room_id``: string - The ID of the room in which the event being reported is located.
|
||||
- ``event_id``: string - The ID of the reported event.
|
||||
- ``user_id``: string - This is the user who reported the event and wrote the reason.
|
||||
- ``reason``: string - Comment made by the ``user_id`` in this report. May be blank.
|
||||
- ``content``: object - Content of reported event.
|
||||
|
||||
- ``reason``: string - Comment made by the ``user_id`` in this report. May be blank.
|
||||
- ``score``: integer - Content is reported based upon a negative score, where -100 is "most offensive" and 0 is "inoffensive".
|
||||
|
||||
- ``sender``: string - This is the ID of the user who sent the original message/event that was reported.
|
||||
- ``room_alias``: string - The alias of the room. ``null`` if the room does not have a canonical alias set.
|
||||
- ``event_json``: object - Details of the original event that was reported.
|
||||
- ``next_token``: integer - Indication for pagination. See above.
|
||||
- ``total``: integer - Total number of event reports related to the query (``user_id`` and ``room_id``).
|
||||
|
||||
@@ -275,6 +275,8 @@ The following fields are possible in the JSON response body:
|
||||
|
||||
* `room_id` - The ID of the room.
|
||||
* `name` - The name of the room.
|
||||
* `topic` - The topic of the room.
|
||||
* `avatar` - The `mxc` URI to the avatar of the room.
|
||||
* `canonical_alias` - The canonical (main) alias address of the room.
|
||||
* `joined_members` - How many users are currently in the room.
|
||||
* `joined_local_members` - How many local users are currently in the room.
|
||||
@@ -304,6 +306,8 @@ Response:
|
||||
{
|
||||
"room_id": "!mscvqgqpHYjBGDxNym:matrix.org",
|
||||
"name": "Music Theory",
|
||||
"avatar": "mxc://matrix.org/AQDaVFlbkQoErdOgqWRgiGSV",
|
||||
"topic": "Theory, Composition, Notation, Analysis",
|
||||
"canonical_alias": "#musictheory:matrix.org",
|
||||
"joined_members": 127
|
||||
"joined_local_members": 2,
|
||||
|
||||
@@ -304,6 +304,43 @@ To use it, you will need to authenticate by providing an ``access_token`` for a
|
||||
server admin: see `README.rst <README.rst>`_.
|
||||
|
||||
|
||||
List room memberships of an user
|
||||
================================
|
||||
Gets a list of all ``room_id`` that a specific ``user_id`` is member.
|
||||
|
||||
The API is::
|
||||
|
||||
GET /_synapse/admin/v1/users/<user_id>/joined_rooms
|
||||
|
||||
To use it, you will need to authenticate by providing an ``access_token`` for a
|
||||
server admin: see `README.rst <README.rst>`_.
|
||||
|
||||
A response body like the following is returned:
|
||||
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"joined_rooms": [
|
||||
"!DuGcnbhHGaSZQoNQR:matrix.org",
|
||||
"!ZtSaPCawyWtxfWiIy:matrix.org"
|
||||
],
|
||||
"total": 2
|
||||
}
|
||||
|
||||
**Parameters**
|
||||
|
||||
The following parameters should be set in the URL:
|
||||
|
||||
- ``user_id`` - fully qualified: for example, ``@user:server.com``.
|
||||
|
||||
**Response**
|
||||
|
||||
The following fields are returned in the JSON response body:
|
||||
|
||||
- ``joined_rooms`` - An array of ``room_id``.
|
||||
- ``total`` - Number of rooms.
|
||||
|
||||
|
||||
User devices
|
||||
============
|
||||
|
||||
|
||||
@@ -106,6 +106,17 @@ Note that the above may fail with an error about duplicate rows if corruption
|
||||
has already occurred, and such duplicate rows will need to be manually removed.
|
||||
|
||||
|
||||
## Fixing inconsistent sequences error
|
||||
|
||||
Synapse uses Postgres sequences to generate IDs for various tables. A sequence
|
||||
and associated table can get out of sync if, for example, Synapse has been
|
||||
downgraded and then upgraded again.
|
||||
|
||||
To fix the issue shut down Synapse (including any and all workers) and run the
|
||||
SQL command included in the error message. Once done Synapse should start
|
||||
successfully.
|
||||
|
||||
|
||||
## Tuning Postgres
|
||||
|
||||
The default settings should be fine for most deployments. For larger
|
||||
|
||||
+29
-2
@@ -11,7 +11,7 @@ privileges.
|
||||
|
||||
**NOTE**: Your reverse proxy must not `canonicalise` or `normalise`
|
||||
the requested URI in any way (for example, by decoding `%xx` escapes).
|
||||
Beware that Apache *will* canonicalise URIs unless you specifify
|
||||
Beware that Apache *will* canonicalise URIs unless you specify
|
||||
`nocanon`.
|
||||
|
||||
When setting up a reverse proxy, remember that Matrix clients and other
|
||||
@@ -23,6 +23,10 @@ specification](https://matrix.org/docs/spec/server_server/latest#resolving-serve
|
||||
for more details of the algorithm used for federation connections, and
|
||||
[delegate.md](<delegate.md>) for instructions on setting up delegation.
|
||||
|
||||
Endpoints that are part of the standardised Matrix specification are
|
||||
located under `/_matrix`, whereas endpoints specific to Synapse are
|
||||
located under `/_synapse/client`.
|
||||
|
||||
Let's assume that we expect clients to connect to our server at
|
||||
`https://matrix.example.com`, and other servers to connect at
|
||||
`https://example.com:8448`. The following sections detail the configuration of
|
||||
@@ -45,7 +49,7 @@ server {
|
||||
|
||||
server_name matrix.example.com;
|
||||
|
||||
location /_matrix {
|
||||
location ~* ^(\/_matrix|\/_synapse\/client) {
|
||||
proxy_pass http://localhost:8008;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
# Nginx by default only allows file uploads up to 1M in size
|
||||
@@ -65,6 +69,10 @@ matrix.example.com {
|
||||
proxy /_matrix http://localhost:8008 {
|
||||
transparent
|
||||
}
|
||||
|
||||
proxy /_synapse/client http://localhost:8008 {
|
||||
transparent
|
||||
}
|
||||
}
|
||||
|
||||
example.com:8448 {
|
||||
@@ -79,6 +87,7 @@ example.com:8448 {
|
||||
```
|
||||
matrix.example.com {
|
||||
reverse_proxy /_matrix/* http://localhost:8008
|
||||
reverse_proxy /_synapse/client/* http://localhost:8008
|
||||
}
|
||||
|
||||
example.com:8448 {
|
||||
@@ -96,6 +105,8 @@ example.com:8448 {
|
||||
AllowEncodedSlashes NoDecode
|
||||
ProxyPass /_matrix http://127.0.0.1:8008/_matrix nocanon
|
||||
ProxyPassReverse /_matrix http://127.0.0.1:8008/_matrix
|
||||
ProxyPass /_synapse/client http://127.0.0.1:8008/_synapse/client nocanon
|
||||
ProxyPassReverse /_synapse/client http://127.0.0.1:8008/_synapse/client
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:8448>
|
||||
@@ -110,6 +121,14 @@ example.com:8448 {
|
||||
|
||||
**NOTE**: ensure the `nocanon` options are included.
|
||||
|
||||
**NOTE 2**: It appears that Synapse is currently incompatible with the ModSecurity module for Apache (`mod_security2`). If you need it enabled for other services on your web server, you can disable it for Synapse's two VirtualHosts by including the following lines before each of the two `</VirtualHost>` above:
|
||||
|
||||
```
|
||||
<IfModule security2_module>
|
||||
SecRuleEngine off
|
||||
</IfModule>
|
||||
```
|
||||
|
||||
### HAProxy
|
||||
|
||||
```
|
||||
@@ -119,6 +138,7 @@ frontend https
|
||||
# Matrix client traffic
|
||||
acl matrix-host hdr(host) -i matrix.example.com
|
||||
acl matrix-path path_beg /_matrix
|
||||
acl matrix-path path_beg /_synapse/client
|
||||
|
||||
use_backend matrix if matrix-host matrix-path
|
||||
|
||||
@@ -146,3 +166,10 @@ connecting to Synapse from a client.
|
||||
Synapse exposes a health check endpoint for use by reverse proxies.
|
||||
Each configured HTTP listener has a `/health` endpoint which always returns
|
||||
200 OK (and doesn't get logged).
|
||||
|
||||
## Synapse administration endpoints
|
||||
|
||||
Endpoints for administering your Synapse instance are placed under
|
||||
`/_synapse/admin`. These require authentication through an access token of an
|
||||
admin user. However as access to these endpoints grants the caller a lot of power,
|
||||
we do not recommend exposing them to the public internet without good reason.
|
||||
|
||||
+71
-33
@@ -33,10 +33,23 @@
|
||||
|
||||
## Server ##
|
||||
|
||||
# The domain name of the server, with optional explicit port.
|
||||
# This is used by remote servers to connect to this server,
|
||||
# e.g. matrix.org, localhost:8080, etc.
|
||||
# This is also the last part of your UserID.
|
||||
# The public-facing domain of the server
|
||||
#
|
||||
# The server_name name will appear at the end of usernames and room addresses
|
||||
# created on this server. For example if the server_name was example.com,
|
||||
# usernames on this server would be in the format @user:example.com
|
||||
#
|
||||
# In most cases you should avoid using a matrix specific subdomain such as
|
||||
# matrix.example.com or synapse.example.com as the server_name for the same
|
||||
# reasons you wouldn't use user@email.example.com as your email address.
|
||||
# See https://github.com/matrix-org/synapse/blob/master/docs/delegate.md
|
||||
# for information on how to host Synapse on a subdomain while preserving
|
||||
# a clean server_name.
|
||||
#
|
||||
# The server_name cannot be changed later so it is important to
|
||||
# configure this correctly before you start Synapse. It should be all
|
||||
# lowercase and may contain an explicit port.
|
||||
# Examples: matrix.org, localhost:8080
|
||||
#
|
||||
server_name: "SERVERNAME"
|
||||
|
||||
@@ -432,6 +445,24 @@ retention:
|
||||
#
|
||||
#request_token_inhibit_3pid_errors: true
|
||||
|
||||
# A list of domains that the domain portion of 'next_link' parameters
|
||||
# must match.
|
||||
#
|
||||
# This parameter is optionally provided by clients while requesting
|
||||
# validation of an email or phone number, and maps to a link that
|
||||
# users will be automatically redirected to after validation
|
||||
# succeeds. Clients can make use this parameter to aid the validation
|
||||
# process.
|
||||
#
|
||||
# The whitelist is applied whether the homeserver or an
|
||||
# identity server is handling validation.
|
||||
#
|
||||
# The default value is no whitelist functionality; all domains are
|
||||
# allowed. Setting this value to an empty list will instead disallow
|
||||
# all domains.
|
||||
#
|
||||
#next_link_domain_whitelist: ["matrix.org"]
|
||||
|
||||
|
||||
## TLS ##
|
||||
|
||||
@@ -598,6 +629,7 @@ acme:
|
||||
#tls_fingerprints: [{"sha256": "<base64_encoded_sha256_fingerprint>"}]
|
||||
|
||||
|
||||
## Federation ##
|
||||
|
||||
# Restrict federation to the following whitelist of domains.
|
||||
# N.B. we recommend also firewalling your federation listener to limit
|
||||
@@ -631,6 +663,17 @@ federation_ip_range_blacklist:
|
||||
- 'fe80::/64'
|
||||
- 'fc00::/7'
|
||||
|
||||
# Report prometheus metrics on the age of PDUs being sent to and received from
|
||||
# the following domains. This can be used to give an idea of "delay" on inbound
|
||||
# and outbound federation, though be aware that any delay can be due to problems
|
||||
# at either end or with the intermediate network.
|
||||
#
|
||||
# By default, no domains are monitored in this way.
|
||||
#
|
||||
#federation_metrics_domains:
|
||||
# - matrix.org
|
||||
# - example.com
|
||||
|
||||
|
||||
## Caching ##
|
||||
|
||||
@@ -1472,11 +1515,14 @@ trusted_key_servers:
|
||||
# At least one of `sp_config` or `config_path` must be set in this section to
|
||||
# enable SAML login.
|
||||
#
|
||||
# (You will probably also want to set the following options to `false` to
|
||||
# You will probably also want to set the following options to `false` to
|
||||
# disable the regular login/registration flows:
|
||||
# * enable_registration
|
||||
# * password_config.enabled
|
||||
#
|
||||
# You will also want to investigate the settings under the "sso" configuration
|
||||
# section below.
|
||||
#
|
||||
# Once SAML support is enabled, a metadata file will be exposed at
|
||||
# https://<server>:<port>/_matrix/saml2/metadata.xml, which you may be able to
|
||||
# use to configure your SAML IdP with. Alternatively, you can manually configure
|
||||
@@ -1599,31 +1645,6 @@ saml2_config:
|
||||
# - attribute: department
|
||||
# value: "sales"
|
||||
|
||||
# Directory in which Synapse will try to find the template files below.
|
||||
# If not set, default templates from within the Synapse package will be used.
|
||||
#
|
||||
# DO NOT UNCOMMENT THIS SETTING unless you want to customise the templates.
|
||||
# If you *do* uncomment it, you will need to make sure that all the templates
|
||||
# below are in the directory.
|
||||
#
|
||||
# Synapse will look for the following templates in this directory:
|
||||
#
|
||||
# * HTML page to display to users if something goes wrong during the
|
||||
# authentication process: 'saml_error.html'.
|
||||
#
|
||||
# When rendering, this template is given the following variables:
|
||||
# * code: an HTML error code corresponding to the error that is being
|
||||
# returned (typically 400 or 500)
|
||||
#
|
||||
# * msg: a textual message describing the error.
|
||||
#
|
||||
# The variables will automatically be HTML-escaped.
|
||||
#
|
||||
# You can see the default templates at:
|
||||
# https://github.com/matrix-org/synapse/tree/master/synapse/res/templates
|
||||
#
|
||||
#template_dir: "res/templates"
|
||||
|
||||
|
||||
# OpenID Connect integration. The following settings can be used to make Synapse
|
||||
# use an OpenID Connect Provider for authentication, instead of its internal
|
||||
@@ -1698,6 +1719,11 @@ oidc_config:
|
||||
#
|
||||
#skip_verification: true
|
||||
|
||||
# Uncomment to allow a user logging in via OIDC to match a pre-existing account instead
|
||||
# of failing. This could be used if switching from password logins to OIDC. Defaults to false.
|
||||
#
|
||||
#allow_existing_users: true
|
||||
|
||||
# An external module can be provided here as a custom solution to mapping
|
||||
# attributes returned from a OIDC provider onto a matrix user.
|
||||
#
|
||||
@@ -1739,6 +1765,14 @@ oidc_config:
|
||||
#
|
||||
#display_name_template: "{{ user.given_name }} {{ user.last_name }}"
|
||||
|
||||
# Jinja2 templates for extra attributes to send back to the client during
|
||||
# login.
|
||||
#
|
||||
# Note that these are non-standard and clients will ignore them without modifications.
|
||||
#
|
||||
#extra_attributes:
|
||||
#birthdate: "{{ user.birthdate }}"
|
||||
|
||||
|
||||
|
||||
# Enable CAS for registration and login.
|
||||
@@ -2026,9 +2060,13 @@ email:
|
||||
# * The contents of password reset emails sent by the homeserver:
|
||||
# 'password_reset.html' and 'password_reset.txt'
|
||||
#
|
||||
# * HTML pages for success and failure that a user will see when they follow
|
||||
# the link in the password reset email: 'password_reset_success.html' and
|
||||
# 'password_reset_failure.html'
|
||||
# * An HTML page that a user will see when they follow the link in the password
|
||||
# reset email. The user will be asked to confirm the action before their
|
||||
# password is reset: 'password_reset_confirmation.html'
|
||||
#
|
||||
# * HTML pages for success and failure that a user will see when they confirm
|
||||
# the password reset flow using the page above: 'password_reset_success.html'
|
||||
# and 'password_reset_failure.html'
|
||||
#
|
||||
# * The contents of address verification emails sent during registration:
|
||||
# 'registration.html' and 'registration.txt'
|
||||
|
||||
@@ -57,7 +57,7 @@ A custom mapping provider must specify the following methods:
|
||||
- This method must return a string, which is the unique identifier for the
|
||||
user. Commonly the ``sub`` claim of the response.
|
||||
* `map_user_attributes(self, userinfo, token)`
|
||||
- This method should be async.
|
||||
- This method must be async.
|
||||
- Arguments:
|
||||
- `userinfo` - A `authlib.oidc.core.claims.UserInfo` object to extract user
|
||||
information from.
|
||||
@@ -66,6 +66,18 @@ A custom mapping provider must specify the following methods:
|
||||
- Returns a dictionary with two keys:
|
||||
- localpart: A required string, used to generate the Matrix ID.
|
||||
- displayname: An optional string, the display name for the user.
|
||||
* `get_extra_attributes(self, userinfo, token)`
|
||||
- This method must be async.
|
||||
- Arguments:
|
||||
- `userinfo` - A `authlib.oidc.core.claims.UserInfo` object to extract user
|
||||
information from.
|
||||
- `token` - A dictionary which includes information necessary to make
|
||||
further requests to the OpenID provider.
|
||||
- Returns a dictionary that is suitable to be serialized to JSON. This
|
||||
will be returned as part of the response during a successful login.
|
||||
|
||||
Note that care should be taken to not overwrite any of the parameters
|
||||
usually returned as part of the [login response](https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login).
|
||||
|
||||
### Default OpenID Mapping Provider
|
||||
|
||||
|
||||
@@ -217,6 +217,7 @@ expressions:
|
||||
^/_matrix/client/(api/v1|r0|unstable)/joined_groups$
|
||||
^/_matrix/client/(api/v1|r0|unstable)/publicised_groups$
|
||||
^/_matrix/client/(api/v1|r0|unstable)/publicised_groups/
|
||||
^/_synapse/client/password_reset/email/submit_token$
|
||||
|
||||
# Registration/login requests
|
||||
^/_matrix/client/(api/v1|r0|unstable)/login$
|
||||
@@ -242,6 +243,22 @@ for the room are in flight:
|
||||
|
||||
^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/messages$
|
||||
|
||||
Additionally, the following endpoints should be included if Synapse is configured
|
||||
to use SSO (you only need to include the ones for whichever SSO provider you're
|
||||
using):
|
||||
|
||||
# OpenID Connect requests.
|
||||
^/_matrix/client/(api/v1|r0|unstable)/login/sso/redirect$
|
||||
^/_synapse/oidc/callback$
|
||||
|
||||
# SAML requests.
|
||||
^/_matrix/client/(api/v1|r0|unstable)/login/sso/redirect$
|
||||
^/_matrix/saml2/authn_response$
|
||||
|
||||
# CAS requests.
|
||||
^/_matrix/client/(api/v1|r0|unstable)/login/(cas|sso)/redirect$
|
||||
^/_matrix/client/(api/v1|r0|unstable)/login/cas/ticket$
|
||||
|
||||
Note that a HTTP listener with `client` and `federation` resources must be
|
||||
configured in the `worker_listeners` option in the worker config.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ files =
|
||||
synapse/http/federation/well_known_resolver.py,
|
||||
synapse/http/server.py,
|
||||
synapse/http/site.py,
|
||||
synapse/logging/,
|
||||
synapse/logging,
|
||||
synapse/metrics,
|
||||
synapse/module_api,
|
||||
synapse/notifier.py,
|
||||
@@ -46,14 +46,17 @@ files =
|
||||
synapse/server_notices,
|
||||
synapse/spam_checker_api,
|
||||
synapse/state,
|
||||
synapse/storage/databases/main/events.py,
|
||||
synapse/storage/databases/main/stream.py,
|
||||
synapse/storage/databases/main/ui_auth.py,
|
||||
synapse/storage/database.py,
|
||||
synapse/storage/engines,
|
||||
synapse/storage/persist_events.py,
|
||||
synapse/storage/state.py,
|
||||
synapse/storage/util,
|
||||
synapse/streams,
|
||||
synapse/types.py,
|
||||
synapse/util/async_helpers.py,
|
||||
synapse/util/caches/descriptors.py,
|
||||
synapse/util/caches/stream_change_cache.py,
|
||||
synapse/util/metrics.py,
|
||||
|
||||
@@ -25,6 +25,7 @@ DISTS = (
|
||||
"ubuntu:xenial",
|
||||
"ubuntu:bionic",
|
||||
"ubuntu:focal",
|
||||
"ubuntu:groovy",
|
||||
)
|
||||
|
||||
DESC = '''\
|
||||
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#! /bin/bash -eu
|
||||
# This script is designed for developers who want to test their code
|
||||
# against Complement.
|
||||
#
|
||||
# It makes a Synapse image which represents the current checkout,
|
||||
# then downloads Complement and runs it with that image.
|
||||
|
||||
cd "$(dirname $0)/.."
|
||||
|
||||
# Build the base Synapse image from the local checkout
|
||||
docker build -t matrixdotorg/synapse:latest -f docker/Dockerfile .
|
||||
|
||||
# Download Complement
|
||||
wget -N https://github.com/matrix-org/complement/archive/master.tar.gz
|
||||
tar -xzf master.tar.gz
|
||||
cd complement-master
|
||||
|
||||
# Build the Synapse image from Complement, based on the above image we just built
|
||||
docker build -t complement-synapse -f dockerfiles/Synapse.Dockerfile ./dockerfiles
|
||||
|
||||
# Run the tests on the resulting image!
|
||||
COMPLEMENT_BASE_IMAGE=complement-synapse go test -v -count=1 ./tests
|
||||
@@ -1,7 +1,5 @@
|
||||
#! /usr/bin/python
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import os
|
||||
@@ -13,7 +11,7 @@ import yaml
|
||||
|
||||
class DefinitionVisitor(ast.NodeVisitor):
|
||||
def __init__(self):
|
||||
super(DefinitionVisitor, self).__init__()
|
||||
super().__init__()
|
||||
self.functions = {}
|
||||
self.classes = {}
|
||||
self.names = {}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python2
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
|
||||
import pymacaroons
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
@@ -323,7 +321,7 @@ class MatrixConnectionAdapter(HTTPAdapter):
|
||||
url = urlparse.urlunparse(
|
||||
("https", netloc, parsed.path, parsed.params, parsed.query, parsed.fragment)
|
||||
)
|
||||
return super(MatrixConnectionAdapter, self).get_connection(url, proxies)
|
||||
return super().get_connection(url, proxies)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
|
||||
@@ -32,8 +32,6 @@ To use, pipe the above into::
|
||||
PYTHON_PATH=. ./scripts/move_remote_media_to_new_store.py <source repo> <dest repo>
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from synapse._scripts.register_new_matrix_user import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -145,6 +145,7 @@ IGNORED_TABLES = {
|
||||
# the sessions are transient anyway, so ignore them.
|
||||
"ui_auth_sessions",
|
||||
"ui_auth_sessions_credentials",
|
||||
"ui_auth_sessions_ips",
|
||||
}
|
||||
|
||||
|
||||
@@ -628,6 +629,7 @@ class Porter(object):
|
||||
self.progress.set_state("Setting up sequence generators")
|
||||
await self._setup_state_group_id_seq()
|
||||
await self._setup_user_id_seq()
|
||||
await self._setup_events_stream_seqs()
|
||||
|
||||
self.progress.done()
|
||||
except Exception as e:
|
||||
@@ -804,6 +806,29 @@ class Porter(object):
|
||||
|
||||
return self.postgres_store.db_pool.runInteraction("setup_user_id_seq", r)
|
||||
|
||||
def _setup_events_stream_seqs(self):
|
||||
def r(txn):
|
||||
txn.execute("SELECT MAX(stream_ordering) FROM events")
|
||||
curr_id = txn.fetchone()[0]
|
||||
if curr_id:
|
||||
next_id = curr_id + 1
|
||||
txn.execute(
|
||||
"ALTER SEQUENCE events_stream_seq RESTART WITH %s", (next_id,)
|
||||
)
|
||||
|
||||
txn.execute("SELECT -MIN(stream_ordering) FROM events")
|
||||
curr_id = txn.fetchone()[0]
|
||||
if curr_id:
|
||||
next_id = curr_id + 1
|
||||
txn.execute(
|
||||
"ALTER SEQUENCE events_backfill_stream_seq RESTART WITH %s",
|
||||
(next_id,),
|
||||
)
|
||||
|
||||
return self.postgres_store.db_pool.runInteraction(
|
||||
"_setup_events_stream_seqs", r
|
||||
)
|
||||
|
||||
|
||||
##############################################
|
||||
# The following is simply UI stuff
|
||||
|
||||
@@ -94,6 +94,22 @@ ALL_OPTIONAL_REQUIREMENTS = dependencies["ALL_OPTIONAL_REQUIREMENTS"]
|
||||
# Make `pip install matrix-synapse[all]` install all the optional dependencies.
|
||||
CONDITIONAL_REQUIREMENTS["all"] = list(ALL_OPTIONAL_REQUIREMENTS)
|
||||
|
||||
# Developer dependencies should not get included in "all".
|
||||
#
|
||||
# We pin black so that our tests don't start failing on new releases.
|
||||
CONDITIONAL_REQUIREMENTS["lint"] = [
|
||||
"isort==5.0.3",
|
||||
"black==19.10b0",
|
||||
"flake8-comprehensions",
|
||||
"flake8",
|
||||
]
|
||||
|
||||
# Dependencies which are exclusively required by unit test code. This is
|
||||
# NOT a list of all modules that are necessary to run the unit tests.
|
||||
# Tests assume that all optional dependencies are installed.
|
||||
#
|
||||
# parameterized_class decorator was introduced in parameterized 0.7.0
|
||||
CONDITIONAL_REQUIREMENTS["test"] = ["mock>=2.0", "parameterized>=0.7.0"]
|
||||
|
||||
setup(
|
||||
name="matrix-synapse",
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
__version__ = "1.20.1"
|
||||
__version__ = "1.21.1"
|
||||
|
||||
if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)):
|
||||
# We import here so that we don't have to install a bunch of deps when
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import hashlib
|
||||
|
||||
+1
-5
@@ -218,11 +218,7 @@ class Auth:
|
||||
# Deny the request if the user account has expired.
|
||||
if self._account_validity.enabled and not allow_expired:
|
||||
user_id = user.to_string()
|
||||
expiration_ts = await self.store.get_expiration_ts_for_user(user_id)
|
||||
if (
|
||||
expiration_ts is not None
|
||||
and self.clock.time_msec() >= expiration_ts
|
||||
):
|
||||
if await self.store.is_account_expired(user_id, self.clock.time_msec()):
|
||||
raise AuthError(
|
||||
403, "User account has expired", errcode=Codes.EXPIRED_ACCOUNT
|
||||
)
|
||||
|
||||
+23
-27
@@ -87,7 +87,7 @@ class CodeMessageException(RuntimeError):
|
||||
"""
|
||||
|
||||
def __init__(self, code: Union[int, HTTPStatus], msg: str):
|
||||
super(CodeMessageException, self).__init__("%d: %s" % (code, msg))
|
||||
super().__init__("%d: %s" % (code, msg))
|
||||
|
||||
# Some calls to this method pass instances of http.HTTPStatus for `code`.
|
||||
# While HTTPStatus is a subclass of int, it has magic __str__ methods
|
||||
@@ -138,7 +138,7 @@ class SynapseError(CodeMessageException):
|
||||
msg: The human-readable error message.
|
||||
errcode: The matrix error code e.g 'M_FORBIDDEN'
|
||||
"""
|
||||
super(SynapseError, self).__init__(code, msg)
|
||||
super().__init__(code, msg)
|
||||
self.errcode = errcode
|
||||
|
||||
def error_dict(self):
|
||||
@@ -159,7 +159,7 @@ class ProxiedRequestError(SynapseError):
|
||||
errcode: str = Codes.UNKNOWN,
|
||||
additional_fields: Optional[Dict] = None,
|
||||
):
|
||||
super(ProxiedRequestError, self).__init__(code, msg, errcode)
|
||||
super().__init__(code, msg, errcode)
|
||||
if additional_fields is None:
|
||||
self._additional_fields = {} # type: Dict
|
||||
else:
|
||||
@@ -181,7 +181,7 @@ class ConsentNotGivenError(SynapseError):
|
||||
msg: The human-readable error message
|
||||
consent_url: The URL where the user can give their consent
|
||||
"""
|
||||
super(ConsentNotGivenError, self).__init__(
|
||||
super().__init__(
|
||||
code=HTTPStatus.FORBIDDEN, msg=msg, errcode=Codes.CONSENT_NOT_GIVEN
|
||||
)
|
||||
self._consent_uri = consent_uri
|
||||
@@ -201,7 +201,7 @@ class UserDeactivatedError(SynapseError):
|
||||
Args:
|
||||
msg: The human-readable error message
|
||||
"""
|
||||
super(UserDeactivatedError, self).__init__(
|
||||
super().__init__(
|
||||
code=HTTPStatus.FORBIDDEN, msg=msg, errcode=Codes.USER_DEACTIVATED
|
||||
)
|
||||
|
||||
@@ -225,7 +225,7 @@ class FederationDeniedError(SynapseError):
|
||||
|
||||
self.destination = destination
|
||||
|
||||
super(FederationDeniedError, self).__init__(
|
||||
super().__init__(
|
||||
code=403,
|
||||
msg="Federation denied with %s." % (self.destination,),
|
||||
errcode=Codes.FORBIDDEN,
|
||||
@@ -244,9 +244,7 @@ class InteractiveAuthIncompleteError(Exception):
|
||||
"""
|
||||
|
||||
def __init__(self, session_id: str, result: "JsonDict"):
|
||||
super(InteractiveAuthIncompleteError, self).__init__(
|
||||
"Interactive auth not yet complete"
|
||||
)
|
||||
super().__init__("Interactive auth not yet complete")
|
||||
self.session_id = session_id
|
||||
self.result = result
|
||||
|
||||
@@ -261,14 +259,14 @@ class UnrecognizedRequestError(SynapseError):
|
||||
message = "Unrecognized request"
|
||||
else:
|
||||
message = args[0]
|
||||
super(UnrecognizedRequestError, self).__init__(400, message, **kwargs)
|
||||
super().__init__(400, message, **kwargs)
|
||||
|
||||
|
||||
class NotFoundError(SynapseError):
|
||||
"""An error indicating we can't find the thing you asked for"""
|
||||
|
||||
def __init__(self, msg: str = "Not found", errcode: str = Codes.NOT_FOUND):
|
||||
super(NotFoundError, self).__init__(404, msg, errcode=errcode)
|
||||
super().__init__(404, msg, errcode=errcode)
|
||||
|
||||
|
||||
class AuthError(SynapseError):
|
||||
@@ -279,7 +277,7 @@ class AuthError(SynapseError):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "errcode" not in kwargs:
|
||||
kwargs["errcode"] = Codes.FORBIDDEN
|
||||
super(AuthError, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class InvalidClientCredentialsError(SynapseError):
|
||||
@@ -335,7 +333,7 @@ class ResourceLimitError(SynapseError):
|
||||
):
|
||||
self.admin_contact = admin_contact
|
||||
self.limit_type = limit_type
|
||||
super(ResourceLimitError, self).__init__(code, msg, errcode=errcode)
|
||||
super().__init__(code, msg, errcode=errcode)
|
||||
|
||||
def error_dict(self):
|
||||
return cs_error(
|
||||
@@ -352,7 +350,7 @@ class EventSizeError(SynapseError):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "errcode" not in kwargs:
|
||||
kwargs["errcode"] = Codes.TOO_LARGE
|
||||
super(EventSizeError, self).__init__(413, *args, **kwargs)
|
||||
super().__init__(413, *args, **kwargs)
|
||||
|
||||
|
||||
class EventStreamError(SynapseError):
|
||||
@@ -361,7 +359,7 @@ class EventStreamError(SynapseError):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "errcode" not in kwargs:
|
||||
kwargs["errcode"] = Codes.BAD_PAGINATION
|
||||
super(EventStreamError, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class LoginError(SynapseError):
|
||||
@@ -384,7 +382,7 @@ class InvalidCaptchaError(SynapseError):
|
||||
error_url: Optional[str] = None,
|
||||
errcode: str = Codes.CAPTCHA_INVALID,
|
||||
):
|
||||
super(InvalidCaptchaError, self).__init__(code, msg, errcode)
|
||||
super().__init__(code, msg, errcode)
|
||||
self.error_url = error_url
|
||||
|
||||
def error_dict(self):
|
||||
@@ -402,7 +400,7 @@ class LimitExceededError(SynapseError):
|
||||
retry_after_ms: Optional[int] = None,
|
||||
errcode: str = Codes.LIMIT_EXCEEDED,
|
||||
):
|
||||
super(LimitExceededError, self).__init__(code, msg, errcode)
|
||||
super().__init__(code, msg, errcode)
|
||||
self.retry_after_ms = retry_after_ms
|
||||
|
||||
def error_dict(self):
|
||||
@@ -418,9 +416,7 @@ class RoomKeysVersionError(SynapseError):
|
||||
Args:
|
||||
current_version: the current version of the store they should have used
|
||||
"""
|
||||
super(RoomKeysVersionError, self).__init__(
|
||||
403, "Wrong room_keys version", Codes.WRONG_ROOM_KEYS_VERSION
|
||||
)
|
||||
super().__init__(403, "Wrong room_keys version", Codes.WRONG_ROOM_KEYS_VERSION)
|
||||
self.current_version = current_version
|
||||
|
||||
|
||||
@@ -429,7 +425,7 @@ class UnsupportedRoomVersionError(SynapseError):
|
||||
not support."""
|
||||
|
||||
def __init__(self, msg: str = "Homeserver does not support this room version"):
|
||||
super(UnsupportedRoomVersionError, self).__init__(
|
||||
super().__init__(
|
||||
code=400, msg=msg, errcode=Codes.UNSUPPORTED_ROOM_VERSION,
|
||||
)
|
||||
|
||||
@@ -440,7 +436,7 @@ class ThreepidValidationError(SynapseError):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "errcode" not in kwargs:
|
||||
kwargs["errcode"] = Codes.FORBIDDEN
|
||||
super(ThreepidValidationError, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class IncompatibleRoomVersionError(SynapseError):
|
||||
@@ -451,7 +447,7 @@ class IncompatibleRoomVersionError(SynapseError):
|
||||
"""
|
||||
|
||||
def __init__(self, room_version: str):
|
||||
super(IncompatibleRoomVersionError, self).__init__(
|
||||
super().__init__(
|
||||
code=400,
|
||||
msg="Your homeserver does not support the features required to "
|
||||
"join this room",
|
||||
@@ -473,7 +469,7 @@ class PasswordRefusedError(SynapseError):
|
||||
msg: str = "This password doesn't comply with the server's policy",
|
||||
errcode: str = Codes.WEAK_PASSWORD,
|
||||
):
|
||||
super(PasswordRefusedError, self).__init__(
|
||||
super().__init__(
|
||||
code=400, msg=msg, errcode=errcode,
|
||||
)
|
||||
|
||||
@@ -488,7 +484,7 @@ class RequestSendFailed(RuntimeError):
|
||||
"""
|
||||
|
||||
def __init__(self, inner_exception, can_retry):
|
||||
super(RequestSendFailed, self).__init__(
|
||||
super().__init__(
|
||||
"Failed to send request: %s: %s"
|
||||
% (type(inner_exception).__name__, inner_exception)
|
||||
)
|
||||
@@ -542,7 +538,7 @@ class FederationError(RuntimeError):
|
||||
self.source = source
|
||||
|
||||
msg = "%s %s: %s" % (level, code, reason)
|
||||
super(FederationError, self).__init__(msg)
|
||||
super().__init__(msg)
|
||||
|
||||
def get_dict(self):
|
||||
return {
|
||||
@@ -570,7 +566,7 @@ class HttpResponseException(CodeMessageException):
|
||||
msg: reason phrase from HTTP response status line
|
||||
response: body of response
|
||||
"""
|
||||
super(HttpResponseException, self).__init__(code, msg)
|
||||
super().__init__(code, msg)
|
||||
self.response = response
|
||||
|
||||
def to_synapse_error(self):
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import json
|
||||
from typing import List
|
||||
|
||||
import jsonschema
|
||||
from canonicaljson import json
|
||||
from jsonschema import FormatChecker
|
||||
|
||||
from synapse.api.constants import EventContentFields
|
||||
@@ -132,7 +132,7 @@ def matrix_user_id_validator(user_id_str):
|
||||
|
||||
class Filtering:
|
||||
def __init__(self, hs):
|
||||
super(Filtering, self).__init__()
|
||||
super().__init__()
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
async def get_user_filter(self, user_localpart, filter_id):
|
||||
|
||||
@@ -21,6 +21,7 @@ from urllib.parse import urlencode
|
||||
|
||||
from synapse.config import ConfigError
|
||||
|
||||
SYNAPSE_CLIENT_API_PREFIX = "/_synapse/client"
|
||||
CLIENT_API_PREFIX = "/_matrix/client"
|
||||
FEDERATION_PREFIX = "/_matrix/federation"
|
||||
FEDERATION_V1_PREFIX = FEDERATION_PREFIX + "/v1"
|
||||
|
||||
@@ -14,13 +14,12 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from canonicaljson import json
|
||||
|
||||
from twisted.internet import defer, task
|
||||
|
||||
import synapse
|
||||
|
||||
@@ -152,7 +152,7 @@ class PresenceStatusStubServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/presence/(?P<user_id>[^/]*)/status")
|
||||
|
||||
def __init__(self, hs):
|
||||
super(PresenceStatusStubServlet, self).__init__()
|
||||
super().__init__()
|
||||
self.auth = hs.get_auth()
|
||||
|
||||
async def on_GET(self, request, user_id):
|
||||
@@ -176,7 +176,7 @@ class KeyUploadServlet(RestServlet):
|
||||
Args:
|
||||
hs (synapse.server.HomeServer): server
|
||||
"""
|
||||
super(KeyUploadServlet, self).__init__()
|
||||
super().__init__()
|
||||
self.auth = hs.get_auth()
|
||||
self.store = hs.get_datastore()
|
||||
self.http_client = hs.get_simple_http_client()
|
||||
@@ -646,7 +646,7 @@ class GenericWorkerServer(HomeServer):
|
||||
|
||||
class GenericWorkerReplicationHandler(ReplicationDataHandler):
|
||||
def __init__(self, hs):
|
||||
super(GenericWorkerReplicationHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
self.store = hs.get_datastore()
|
||||
self.presence_handler = hs.get_presence_handler() # type: GenericWorkerPresence
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import gc
|
||||
import logging
|
||||
import math
|
||||
@@ -48,6 +46,7 @@ from synapse.api.urls import (
|
||||
from synapse.app import _base
|
||||
from synapse.app._base import listen_ssl, listen_tcp, quit_with_error
|
||||
from synapse.config._base import ConfigError
|
||||
from synapse.config.emailconfig import ThreepidBehaviour
|
||||
from synapse.config.homeserver import HomeServerConfig
|
||||
from synapse.config.server import ListenerConfig
|
||||
from synapse.federation.transport.server import TransportLayerServer
|
||||
@@ -209,6 +208,15 @@ class SynapseHomeServer(HomeServer):
|
||||
|
||||
resources["/_matrix/saml2"] = SAML2Resource(self)
|
||||
|
||||
if self.get_config().threepid_behaviour_email == ThreepidBehaviour.LOCAL:
|
||||
from synapse.rest.synapse.client.password_reset import (
|
||||
PasswordResetSubmitTokenResource,
|
||||
)
|
||||
|
||||
resources[
|
||||
"/_synapse/client/password_reset/email/submit_token"
|
||||
] = PasswordResetSubmitTokenResource(self)
|
||||
|
||||
if name == "consent":
|
||||
from synapse.rest.consent.consent_resource import ConsentResource
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ class ApplicationServiceApi(SimpleHttpClient):
|
||||
"""
|
||||
|
||||
def __init__(self, hs):
|
||||
super(ApplicationServiceApi, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
self.clock = hs.get_clock()
|
||||
|
||||
self.protocol_meta_cache = ResponseCache(
|
||||
@@ -178,7 +178,7 @@ class ApplicationServiceApi(SimpleHttpClient):
|
||||
urllib.parse.quote(protocol),
|
||||
)
|
||||
try:
|
||||
info = await self.get_json(uri, {})
|
||||
info = await self.get_json(uri)
|
||||
|
||||
if not _is_valid_3pe_metadata(info):
|
||||
logger.warning(
|
||||
|
||||
+23
-9
@@ -242,12 +242,11 @@ class Config:
|
||||
env = jinja2.Environment(loader=loader, autoescape=autoescape)
|
||||
|
||||
# Update the environment with our custom filters
|
||||
env.filters.update(
|
||||
{
|
||||
"format_ts": _format_ts_filter,
|
||||
"mxc_to_http": _create_mxc_to_http_filter(self.public_baseurl),
|
||||
}
|
||||
)
|
||||
env.filters.update({"format_ts": _format_ts_filter})
|
||||
if self.public_baseurl:
|
||||
env.filters.update(
|
||||
{"mxc_to_http": _create_mxc_to_http_filter(self.public_baseurl)}
|
||||
)
|
||||
|
||||
for filename in filenames:
|
||||
# Load the template
|
||||
@@ -838,11 +837,26 @@ class ShardedWorkerHandlingConfig:
|
||||
def should_handle(self, instance_name: str, key: str) -> bool:
|
||||
"""Whether this instance is responsible for handling the given key.
|
||||
"""
|
||||
|
||||
# If multiple instances are not defined we always return true.
|
||||
# If multiple instances are not defined we always return true
|
||||
if not self.instances or len(self.instances) == 1:
|
||||
return True
|
||||
|
||||
return self.get_instance(key) == instance_name
|
||||
|
||||
def get_instance(self, key: str) -> str:
|
||||
"""Get the instance responsible for handling the given key.
|
||||
|
||||
Note: For things like federation sending the config for which instance
|
||||
is sending is known only to the sender instance if there is only one.
|
||||
Therefore `should_handle` should be used where possible.
|
||||
"""
|
||||
|
||||
if not self.instances:
|
||||
return "master"
|
||||
|
||||
if len(self.instances) == 1:
|
||||
return self.instances[0]
|
||||
|
||||
# We shard by taking the hash, modulo it by the number of instances and
|
||||
# then checking whether this instance matches the instance at that
|
||||
# index.
|
||||
@@ -852,7 +866,7 @@ class ShardedWorkerHandlingConfig:
|
||||
dest_hash = sha256(key.encode("utf8")).digest()
|
||||
dest_int = int.from_bytes(dest_hash, byteorder="little")
|
||||
remainder = dest_int % (len(self.instances))
|
||||
return self.instances[remainder] == instance_name
|
||||
return self.instances[remainder]
|
||||
|
||||
|
||||
__all__ = ["Config", "RootConfig", "ShardedWorkerHandlingConfig"]
|
||||
|
||||
@@ -142,3 +142,4 @@ class ShardedWorkerHandlingConfig:
|
||||
instances: List[str]
|
||||
def __init__(self, instances: List[str]) -> None: ...
|
||||
def should_handle(self, instance_name: str, key: str) -> bool: ...
|
||||
def get_instance(self, key: str) -> str: ...
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from typing import Any, List
|
||||
from typing import Any, Iterable
|
||||
|
||||
import jsonschema
|
||||
|
||||
@@ -20,7 +20,9 @@ from synapse.config._base import ConfigError
|
||||
from synapse.types import JsonDict
|
||||
|
||||
|
||||
def validate_config(json_schema: JsonDict, config: Any, config_path: List[str]) -> None:
|
||||
def validate_config(
|
||||
json_schema: JsonDict, config: Any, config_path: Iterable[str]
|
||||
) -> None:
|
||||
"""Validates a config setting against a JsonSchema definition
|
||||
|
||||
This can be used to validate a section of the config file against a schema
|
||||
|
||||
@@ -28,6 +28,9 @@ class CaptchaConfig(Config):
|
||||
"recaptcha_siteverify_api",
|
||||
"https://www.recaptcha.net/recaptcha/api/siteverify",
|
||||
)
|
||||
self.recaptcha_template = self.read_templates(
|
||||
["recaptcha.html"], autoescape=True
|
||||
)[0]
|
||||
|
||||
def generate_config_section(self, **kwargs):
|
||||
return """\
|
||||
|
||||
@@ -77,7 +77,7 @@ class ConsentConfig(Config):
|
||||
section = "consent"
|
||||
|
||||
def __init__(self, *args):
|
||||
super(ConsentConfig, self).__init__(*args)
|
||||
super().__init__(*args)
|
||||
|
||||
self.user_consent_version = None
|
||||
self.user_consent_template_dir = None
|
||||
@@ -89,6 +89,8 @@ class ConsentConfig(Config):
|
||||
|
||||
def read_config(self, config, **kwargs):
|
||||
consent_config = config.get("user_consent")
|
||||
self.terms_template = self.read_templates(["terms.html"], autoescape=True)[0]
|
||||
|
||||
if consent_config is None:
|
||||
return
|
||||
self.user_consent_version = str(consent_config["version"])
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import print_function
|
||||
|
||||
# This file can't be called email.py because if it is, we cannot:
|
||||
import email.utils
|
||||
@@ -228,6 +227,7 @@ class EmailConfig(Config):
|
||||
self.email_registration_template_text,
|
||||
self.email_add_threepid_template_html,
|
||||
self.email_add_threepid_template_text,
|
||||
self.email_password_reset_template_confirmation_html,
|
||||
self.email_password_reset_template_failure_html,
|
||||
self.email_registration_template_failure_html,
|
||||
self.email_add_threepid_template_failure_html,
|
||||
@@ -242,6 +242,7 @@ class EmailConfig(Config):
|
||||
registration_template_text,
|
||||
add_threepid_template_html,
|
||||
add_threepid_template_text,
|
||||
"password_reset_confirmation.html",
|
||||
password_reset_template_failure_html,
|
||||
registration_template_failure_html,
|
||||
add_threepid_template_failure_html,
|
||||
@@ -404,9 +405,13 @@ class EmailConfig(Config):
|
||||
# * The contents of password reset emails sent by the homeserver:
|
||||
# 'password_reset.html' and 'password_reset.txt'
|
||||
#
|
||||
# * HTML pages for success and failure that a user will see when they follow
|
||||
# the link in the password reset email: 'password_reset_success.html' and
|
||||
# 'password_reset_failure.html'
|
||||
# * An HTML page that a user will see when they follow the link in the password
|
||||
# reset email. The user will be asked to confirm the action before their
|
||||
# password is reset: 'password_reset_confirmation.html'
|
||||
#
|
||||
# * HTML pages for success and failure that a user will see when they confirm
|
||||
# the password reset flow using the page above: 'password_reset_success.html'
|
||||
# and 'password_reset_failure.html'
|
||||
#
|
||||
# * The contents of address verification emails sent during registration:
|
||||
# 'registration.html' and 'registration.txt'
|
||||
|
||||
@@ -17,7 +17,8 @@ from typing import Optional
|
||||
|
||||
from netaddr import IPSet
|
||||
|
||||
from ._base import Config, ConfigError
|
||||
from synapse.config._base import Config, ConfigError
|
||||
from synapse.config._util import validate_config
|
||||
|
||||
|
||||
class FederationConfig(Config):
|
||||
@@ -52,8 +53,18 @@ class FederationConfig(Config):
|
||||
"Invalid range(s) provided in federation_ip_range_blacklist: %s" % e
|
||||
)
|
||||
|
||||
federation_metrics_domains = config.get("federation_metrics_domains") or []
|
||||
validate_config(
|
||||
_METRICS_FOR_DOMAINS_SCHEMA,
|
||||
federation_metrics_domains,
|
||||
("federation_metrics_domains",),
|
||||
)
|
||||
self.federation_metrics_domains = set(federation_metrics_domains)
|
||||
|
||||
def generate_config_section(self, config_dir_path, server_name, **kwargs):
|
||||
return """\
|
||||
## Federation ##
|
||||
|
||||
# Restrict federation to the following whitelist of domains.
|
||||
# N.B. we recommend also firewalling your federation listener to limit
|
||||
# inbound federation traffic as early as possible, rather than relying
|
||||
@@ -85,4 +96,18 @@ class FederationConfig(Config):
|
||||
- '::1/128'
|
||||
- 'fe80::/64'
|
||||
- 'fc00::/7'
|
||||
|
||||
# Report prometheus metrics on the age of PDUs being sent to and received from
|
||||
# the following domains. This can be used to give an idea of "delay" on inbound
|
||||
# and outbound federation, though be aware that any delay can be due to problems
|
||||
# at either end or with the intermediate network.
|
||||
#
|
||||
# By default, no domains are monitored in this way.
|
||||
#
|
||||
#federation_metrics_domains:
|
||||
# - matrix.org
|
||||
# - example.com
|
||||
"""
|
||||
|
||||
|
||||
_METRICS_FOR_DOMAINS_SCHEMA = {"type": "array", "items": {"type": "string"}}
|
||||
|
||||
@@ -92,5 +92,4 @@ class HomeServerConfig(RootConfig):
|
||||
TracerConfig,
|
||||
WorkerConfig,
|
||||
RedisConfig,
|
||||
FederationConfig,
|
||||
]
|
||||
|
||||
@@ -17,6 +17,7 @@ import logging
|
||||
import logging.config
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from string import Template
|
||||
|
||||
import yaml
|
||||
@@ -25,6 +26,7 @@ from twisted.logger import (
|
||||
ILogObserver,
|
||||
LogBeginner,
|
||||
STDLibLogObserver,
|
||||
eventAsText,
|
||||
globalLogBeginner,
|
||||
)
|
||||
|
||||
@@ -216,8 +218,9 @@ def _setup_stdlib_logging(config, log_config, logBeginner: LogBeginner):
|
||||
# system.
|
||||
observer = STDLibLogObserver()
|
||||
|
||||
def _log(event):
|
||||
threadlocal = threading.local()
|
||||
|
||||
def _log(event):
|
||||
if "log_text" in event:
|
||||
if event["log_text"].startswith("DNSDatagramProtocol starting on "):
|
||||
return
|
||||
@@ -228,7 +231,25 @@ def _setup_stdlib_logging(config, log_config, logBeginner: LogBeginner):
|
||||
if event["log_text"].startswith("Timing out client"):
|
||||
return
|
||||
|
||||
return observer(event)
|
||||
# this is a workaround to make sure we don't get stack overflows when the
|
||||
# logging system raises an error which is written to stderr which is redirected
|
||||
# to the logging system, etc.
|
||||
if getattr(threadlocal, "active", False):
|
||||
# write the text of the event, if any, to the *real* stderr (which may
|
||||
# be redirected to /dev/null, but there's not much we can do)
|
||||
try:
|
||||
event_text = eventAsText(event)
|
||||
print("logging during logging: %s" % event_text, file=sys.__stderr__)
|
||||
except Exception:
|
||||
# gah.
|
||||
pass
|
||||
return
|
||||
|
||||
try:
|
||||
threadlocal.active = True
|
||||
return observer(event)
|
||||
finally:
|
||||
threadlocal.active = False
|
||||
|
||||
logBeginner.beginLoggingTo([_log], redirectStandardIO=not config.no_redirect_stdio)
|
||||
if not config.no_redirect_stdio:
|
||||
|
||||
@@ -56,6 +56,7 @@ class OIDCConfig(Config):
|
||||
self.oidc_userinfo_endpoint = oidc_config.get("userinfo_endpoint")
|
||||
self.oidc_jwks_uri = oidc_config.get("jwks_uri")
|
||||
self.oidc_skip_verification = oidc_config.get("skip_verification", False)
|
||||
self.oidc_allow_existing_users = oidc_config.get("allow_existing_users", False)
|
||||
|
||||
ump_config = oidc_config.get("user_mapping_provider", {})
|
||||
ump_config.setdefault("module", DEFAULT_USER_MAPPING_PROVIDER)
|
||||
@@ -158,6 +159,11 @@ class OIDCConfig(Config):
|
||||
#
|
||||
#skip_verification: true
|
||||
|
||||
# Uncomment to allow a user logging in via OIDC to match a pre-existing account instead
|
||||
# of failing. This could be used if switching from password logins to OIDC. Defaults to false.
|
||||
#
|
||||
#allow_existing_users: true
|
||||
|
||||
# An external module can be provided here as a custom solution to mapping
|
||||
# attributes returned from a OIDC provider onto a matrix user.
|
||||
#
|
||||
@@ -198,6 +204,14 @@ class OIDCConfig(Config):
|
||||
# If unset, no displayname will be set.
|
||||
#
|
||||
#display_name_template: "{{{{ user.given_name }}}} {{{{ user.last_name }}}}"
|
||||
|
||||
# Jinja2 templates for extra attributes to send back to the client during
|
||||
# login.
|
||||
#
|
||||
# Note that these are non-standard and clients will ignore them without modifications.
|
||||
#
|
||||
#extra_attributes:
|
||||
#birthdate: "{{{{ user.birthdate }}}}"
|
||||
""".format(
|
||||
mapping_provider=DEFAULT_USER_MAPPING_PROVIDER
|
||||
)
|
||||
|
||||
@@ -30,7 +30,7 @@ class AccountValidityConfig(Config):
|
||||
def __init__(self, config, synapse_config):
|
||||
if config is None:
|
||||
return
|
||||
super(AccountValidityConfig, self).__init__()
|
||||
super().__init__()
|
||||
self.enabled = config.get("enabled", False)
|
||||
self.renew_by_email_enabled = "renew_at" in config
|
||||
|
||||
@@ -190,6 +190,11 @@ class RegistrationConfig(Config):
|
||||
session_lifetime = self.parse_duration(session_lifetime)
|
||||
self.session_lifetime = session_lifetime
|
||||
|
||||
# The success template used during fallback auth.
|
||||
self.fallback_success_template = self.read_templates(
|
||||
["auth_success.html"], autoescape=True
|
||||
)[0]
|
||||
|
||||
def generate_config_section(self, generate_secrets=False, **kwargs):
|
||||
if generate_secrets:
|
||||
registration_shared_secret = 'registration_shared_secret: "%s"' % (
|
||||
|
||||
@@ -169,12 +169,6 @@ class SAML2Config(Config):
|
||||
saml2_config.get("saml_session_lifetime", "15m")
|
||||
)
|
||||
|
||||
# We enable autoescape here as the message may potentially come from a
|
||||
# remote resource
|
||||
self.saml2_error_html_template = self.read_templates(
|
||||
["saml_error.html"], saml2_config.get("template_dir"), autoescape=True
|
||||
)[0]
|
||||
|
||||
def _default_saml_config_dict(
|
||||
self, required_attributes: set, optional_attributes: set
|
||||
):
|
||||
@@ -227,11 +221,14 @@ class SAML2Config(Config):
|
||||
# At least one of `sp_config` or `config_path` must be set in this section to
|
||||
# enable SAML login.
|
||||
#
|
||||
# (You will probably also want to set the following options to `false` to
|
||||
# You will probably also want to set the following options to `false` to
|
||||
# disable the regular login/registration flows:
|
||||
# * enable_registration
|
||||
# * password_config.enabled
|
||||
#
|
||||
# You will also want to investigate the settings under the "sso" configuration
|
||||
# section below.
|
||||
#
|
||||
# Once SAML support is enabled, a metadata file will be exposed at
|
||||
# https://<server>:<port>/_matrix/saml2/metadata.xml, which you may be able to
|
||||
# use to configure your SAML IdP with. Alternatively, you can manually configure
|
||||
@@ -353,31 +350,6 @@ class SAML2Config(Config):
|
||||
# value: "staff"
|
||||
# - attribute: department
|
||||
# value: "sales"
|
||||
|
||||
# Directory in which Synapse will try to find the template files below.
|
||||
# If not set, default templates from within the Synapse package will be used.
|
||||
#
|
||||
# DO NOT UNCOMMENT THIS SETTING unless you want to customise the templates.
|
||||
# If you *do* uncomment it, you will need to make sure that all the templates
|
||||
# below are in the directory.
|
||||
#
|
||||
# Synapse will look for the following templates in this directory:
|
||||
#
|
||||
# * HTML page to display to users if something goes wrong during the
|
||||
# authentication process: 'saml_error.html'.
|
||||
#
|
||||
# When rendering, this template is given the following variables:
|
||||
# * code: an HTML error code corresponding to the error that is being
|
||||
# returned (typically 400 or 500)
|
||||
#
|
||||
# * msg: a textual message describing the error.
|
||||
#
|
||||
# The variables will automatically be HTML-escaped.
|
||||
#
|
||||
# You can see the default templates at:
|
||||
# https://github.com/matrix-org/synapse/tree/master/synapse/res/templates
|
||||
#
|
||||
#template_dir: "res/templates"
|
||||
""" % {
|
||||
"config_dir_path": config_dir_path
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import logging
|
||||
import os.path
|
||||
import re
|
||||
from textwrap import indent
|
||||
from typing import Any, Dict, Iterable, List, Optional
|
||||
from typing import Any, Dict, Iterable, List, Optional, Set
|
||||
|
||||
import attr
|
||||
import yaml
|
||||
@@ -542,6 +542,19 @@ class ServerConfig(Config):
|
||||
users_new_default_push_rules
|
||||
) # type: set
|
||||
|
||||
# Whitelist of domain names that given next_link parameters must have
|
||||
next_link_domain_whitelist = config.get(
|
||||
"next_link_domain_whitelist"
|
||||
) # type: Optional[List[str]]
|
||||
|
||||
self.next_link_domain_whitelist = None # type: Optional[Set[str]]
|
||||
if next_link_domain_whitelist is not None:
|
||||
if not isinstance(next_link_domain_whitelist, list):
|
||||
raise ConfigError("'next_link_domain_whitelist' must be a list")
|
||||
|
||||
# Turn the list into a set to improve lookup speed.
|
||||
self.next_link_domain_whitelist = set(next_link_domain_whitelist)
|
||||
|
||||
def has_tls_listener(self) -> bool:
|
||||
return any(listener.tls for listener in self.listeners)
|
||||
|
||||
@@ -628,10 +641,23 @@ class ServerConfig(Config):
|
||||
"""\
|
||||
## Server ##
|
||||
|
||||
# The domain name of the server, with optional explicit port.
|
||||
# This is used by remote servers to connect to this server,
|
||||
# e.g. matrix.org, localhost:8080, etc.
|
||||
# This is also the last part of your UserID.
|
||||
# The public-facing domain of the server
|
||||
#
|
||||
# The server_name name will appear at the end of usernames and room addresses
|
||||
# created on this server. For example if the server_name was example.com,
|
||||
# usernames on this server would be in the format @user:example.com
|
||||
#
|
||||
# In most cases you should avoid using a matrix specific subdomain such as
|
||||
# matrix.example.com or synapse.example.com as the server_name for the same
|
||||
# reasons you wouldn't use user@email.example.com as your email address.
|
||||
# See https://github.com/matrix-org/synapse/blob/master/docs/delegate.md
|
||||
# for information on how to host Synapse on a subdomain while preserving
|
||||
# a clean server_name.
|
||||
#
|
||||
# The server_name cannot be changed later so it is important to
|
||||
# configure this correctly before you start Synapse. It should be all
|
||||
# lowercase and may contain an explicit port.
|
||||
# Examples: matrix.org, localhost:8080
|
||||
#
|
||||
server_name: "%(server_name)s"
|
||||
|
||||
@@ -1014,6 +1040,24 @@ class ServerConfig(Config):
|
||||
# act as if no error happened and return a fake session ID ('sid') to clients.
|
||||
#
|
||||
#request_token_inhibit_3pid_errors: true
|
||||
|
||||
# A list of domains that the domain portion of 'next_link' parameters
|
||||
# must match.
|
||||
#
|
||||
# This parameter is optionally provided by clients while requesting
|
||||
# validation of an email or phone number, and maps to a link that
|
||||
# users will be automatically redirected to after validation
|
||||
# succeeds. Clients can make use this parameter to aid the validation
|
||||
# process.
|
||||
#
|
||||
# The whitelist is applied whether the homeserver or an
|
||||
# identity server is handling validation.
|
||||
#
|
||||
# The default value is no whitelist functionality; all domains are
|
||||
# allowed. Setting this value to an empty list will instead disallow
|
||||
# all domains.
|
||||
#
|
||||
#next_link_domain_whitelist: ["matrix.org"]
|
||||
"""
|
||||
% locals()
|
||||
)
|
||||
|
||||
@@ -62,7 +62,7 @@ class ServerNoticesConfig(Config):
|
||||
section = "servernotices"
|
||||
|
||||
def __init__(self, *args):
|
||||
super(ServerNoticesConfig, self).__init__(*args)
|
||||
super().__init__(*args)
|
||||
self.server_notices_mxid = None
|
||||
self.server_notices_mxid_display_name = None
|
||||
self.server_notices_mxid_avatar_url = None
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import sys
|
||||
|
||||
from ._base import Config
|
||||
|
||||
@@ -471,7 +471,6 @@ class TlsConfig(Config):
|
||||
# or by checking matrix.org/federationtester/api/report?server_name=$host
|
||||
#
|
||||
#tls_fingerprints: [{"sha256": "<base64_encoded_sha256_fingerprint>"}]
|
||||
|
||||
"""
|
||||
# Lowercase the string representation of boolean values
|
||||
% {
|
||||
|
||||
+27
-10
@@ -13,12 +13,24 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import List, Union
|
||||
|
||||
import attr
|
||||
|
||||
from ._base import Config, ConfigError, ShardedWorkerHandlingConfig
|
||||
from .server import ListenerConfig, parse_listener_def
|
||||
|
||||
|
||||
def _instance_to_list_converter(obj: Union[str, List[str]]) -> List[str]:
|
||||
"""Helper for allowing parsing a string or list of strings to a config
|
||||
option expecting a list of strings.
|
||||
"""
|
||||
|
||||
if isinstance(obj, str):
|
||||
return [obj]
|
||||
return obj
|
||||
|
||||
|
||||
@attr.s
|
||||
class InstanceLocationConfig:
|
||||
"""The host and port to talk to an instance via HTTP replication.
|
||||
@@ -33,11 +45,13 @@ class WriterLocations:
|
||||
"""Specifies the instances that write various streams.
|
||||
|
||||
Attributes:
|
||||
events: The instance that writes to the event and backfill streams.
|
||||
events: The instance that writes to the typing stream.
|
||||
events: The instances that write to the event and backfill streams.
|
||||
typing: The instance that writes to the typing stream.
|
||||
"""
|
||||
|
||||
events = attr.ib(default="master", type=str)
|
||||
events = attr.ib(
|
||||
default=["master"], type=List[str], converter=_instance_to_list_converter
|
||||
)
|
||||
typing = attr.ib(default="master", type=str)
|
||||
|
||||
|
||||
@@ -105,15 +119,18 @@ class WorkerConfig(Config):
|
||||
writers = config.get("stream_writers") or {}
|
||||
self.writers = WriterLocations(**writers)
|
||||
|
||||
# Check that the configured writer for events and typing also appears in
|
||||
# Check that the configured writers for events and typing also appears in
|
||||
# `instance_map`.
|
||||
for stream in ("events", "typing"):
|
||||
instance = getattr(self.writers, stream)
|
||||
if instance != "master" and instance not in self.instance_map:
|
||||
raise ConfigError(
|
||||
"Instance %r is configured to write %s but does not appear in `instance_map` config."
|
||||
% (instance, stream)
|
||||
)
|
||||
instances = _instance_to_list_converter(getattr(self.writers, stream))
|
||||
for instance in instances:
|
||||
if instance != "master" and instance not in self.instance_map:
|
||||
raise ConfigError(
|
||||
"Instance %r is configured to write %s but does not appear in `instance_map` config."
|
||||
% (instance, stream)
|
||||
)
|
||||
|
||||
self.events_shard_config = ShardedWorkerHandlingConfig(self.writers.events)
|
||||
|
||||
def generate_config_section(self, config_dir_path, server_name, **kwargs):
|
||||
return """\
|
||||
|
||||
@@ -45,7 +45,11 @@ _TLS_VERSION_MAP = {
|
||||
|
||||
class ServerContextFactory(ContextFactory):
|
||||
"""Factory for PyOpenSSL SSL contexts that are used to handle incoming
|
||||
connections."""
|
||||
connections.
|
||||
|
||||
TODO: replace this with an implementation of IOpenSSLServerConnectionCreator,
|
||||
per https://github.com/matrix-org/synapse/issues/1691
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
# TODO: once pyOpenSSL exposes TLS_METHOD and SSL_CTX_set_min_proto_version,
|
||||
|
||||
+46
-28
@@ -42,7 +42,6 @@ from synapse.api.errors import (
|
||||
)
|
||||
from synapse.logging.context import (
|
||||
PreserveLoggingContext,
|
||||
current_context,
|
||||
make_deferred_yieldable,
|
||||
preserve_fn,
|
||||
run_in_background,
|
||||
@@ -233,8 +232,6 @@ class Keyring:
|
||||
"""
|
||||
|
||||
try:
|
||||
ctx = current_context()
|
||||
|
||||
# map from server name to a set of outstanding request ids
|
||||
server_to_request_ids = {}
|
||||
|
||||
@@ -265,12 +262,8 @@ class Keyring:
|
||||
|
||||
# if there are no more requests for this server, we can drop the lock.
|
||||
if not server_requests:
|
||||
with PreserveLoggingContext(ctx):
|
||||
logger.debug("Releasing key lookup lock on %s", server_name)
|
||||
|
||||
# ... but not immediately, as that can cause stack explosions if
|
||||
# we get a long queue of lookups.
|
||||
self.clock.call_later(0, drop_server_lock, server_name)
|
||||
logger.debug("Releasing key lookup lock on %s", server_name)
|
||||
drop_server_lock(server_name)
|
||||
|
||||
return res
|
||||
|
||||
@@ -335,20 +328,32 @@ class Keyring:
|
||||
)
|
||||
|
||||
# look for any requests which weren't satisfied
|
||||
with PreserveLoggingContext():
|
||||
for verify_request in remaining_requests:
|
||||
verify_request.key_ready.errback(
|
||||
SynapseError(
|
||||
401,
|
||||
"No key for %s with ids in %s (min_validity %i)"
|
||||
% (
|
||||
verify_request.server_name,
|
||||
verify_request.key_ids,
|
||||
verify_request.minimum_valid_until_ts,
|
||||
),
|
||||
Codes.UNAUTHORIZED,
|
||||
)
|
||||
while remaining_requests:
|
||||
verify_request = remaining_requests.pop()
|
||||
rq_str = (
|
||||
"VerifyJsonRequest(server=%s, key_ids=%s, min_valid=%i)"
|
||||
% (
|
||||
verify_request.server_name,
|
||||
verify_request.key_ids,
|
||||
verify_request.minimum_valid_until_ts,
|
||||
)
|
||||
)
|
||||
|
||||
# If we run the errback immediately, it may cancel our
|
||||
# loggingcontext while we are still in it, so instead we
|
||||
# schedule it for the next time round the reactor.
|
||||
#
|
||||
# (this also ensures that we don't get a stack overflow if we
|
||||
# has a massive queue of lookups waiting for this server).
|
||||
self.clock.call_later(
|
||||
0,
|
||||
verify_request.key_ready.errback,
|
||||
SynapseError(
|
||||
401,
|
||||
"Failed to find any key to satisfy %s" % (rq_str,),
|
||||
Codes.UNAUTHORIZED,
|
||||
),
|
||||
)
|
||||
except Exception as err:
|
||||
# we don't really expect to get here, because any errors should already
|
||||
# have been caught and logged. But if we do, let's log the error and make
|
||||
@@ -410,10 +415,23 @@ class Keyring:
|
||||
# key was not valid at this point
|
||||
continue
|
||||
|
||||
with PreserveLoggingContext():
|
||||
verify_request.key_ready.callback(
|
||||
(server_name, key_id, fetch_key_result.verify_key)
|
||||
)
|
||||
# we have a valid key for this request. If we run the callback
|
||||
# immediately, it may cancel our loggingcontext while we are still in
|
||||
# it, so instead we schedule it for the next time round the reactor.
|
||||
#
|
||||
# (this also ensures that we don't get a stack overflow if we had
|
||||
# a massive queue of lookups waiting for this server).
|
||||
logger.debug(
|
||||
"Found key %s:%s for %s",
|
||||
server_name,
|
||||
key_id,
|
||||
verify_request.request_name,
|
||||
)
|
||||
self.clock.call_later(
|
||||
0,
|
||||
verify_request.key_ready.callback,
|
||||
(server_name, key_id, fetch_key_result.verify_key),
|
||||
)
|
||||
completed.append(verify_request)
|
||||
break
|
||||
|
||||
@@ -558,7 +576,7 @@ class PerspectivesKeyFetcher(BaseV2KeyFetcher):
|
||||
"""KeyFetcher impl which fetches keys from the "perspectives" servers"""
|
||||
|
||||
def __init__(self, hs):
|
||||
super(PerspectivesKeyFetcher, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
self.clock = hs.get_clock()
|
||||
self.client = hs.get_http_client()
|
||||
self.key_servers = self.config.key_servers
|
||||
@@ -728,7 +746,7 @@ class ServerKeyFetcher(BaseV2KeyFetcher):
|
||||
"""KeyFetcher impl which fetches keys from the origin servers"""
|
||||
|
||||
def __init__(self, hs):
|
||||
super(ServerKeyFetcher, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
self.clock = hs.get_clock()
|
||||
self.client = hs.get_http_client()
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ from typing import Dict, Optional, Tuple, Type
|
||||
from unpaddedbase64 import encode_base64
|
||||
|
||||
from synapse.api.room_versions import EventFormatVersions, RoomVersion, RoomVersions
|
||||
from synapse.types import JsonDict
|
||||
from synapse.types import JsonDict, RoomStreamToken
|
||||
from synapse.util.caches import intern_dict
|
||||
from synapse.util.frozenutils import freeze
|
||||
|
||||
@@ -118,8 +118,8 @@ class _EventInternalMetadata:
|
||||
# XXX: These are set by StreamWorkerStore._set_before_and_after.
|
||||
# I'm pretty sure that these are never persisted to the database, so shouldn't
|
||||
# be here
|
||||
before = DictProperty("before") # type: str
|
||||
after = DictProperty("after") # type: str
|
||||
before = DictProperty("before") # type: RoomStreamToken
|
||||
after = DictProperty("after") # type: RoomStreamToken
|
||||
order = DictProperty("order") # type: Tuple[int, int]
|
||||
|
||||
def get_dict(self) -> JsonDict:
|
||||
|
||||
@@ -24,10 +24,12 @@ from typing import (
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from prometheus_client import Counter
|
||||
@@ -79,7 +81,7 @@ class InvalidResponseError(RuntimeError):
|
||||
|
||||
class FederationClient(FederationBase):
|
||||
def __init__(self, hs):
|
||||
super(FederationClient, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
self.pdu_destination_tried = {}
|
||||
self._clock.looping_call(self._clear_tried_cache, 60 * 1000)
|
||||
@@ -501,7 +503,7 @@ class FederationClient(FederationBase):
|
||||
user_id: str,
|
||||
membership: str,
|
||||
content: dict,
|
||||
params: Dict[str, str],
|
||||
params: Optional[Mapping[str, Union[str, Iterable[str]]]],
|
||||
) -> Tuple[str, EventBase, RoomVersion]:
|
||||
"""
|
||||
Creates an m.room.member event, with context, without participating in the room.
|
||||
|
||||
@@ -28,7 +28,7 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from prometheus_client import Counter, Histogram
|
||||
from prometheus_client import Counter, Gauge, Histogram
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.internet.abstract import isIPAddress
|
||||
@@ -88,9 +88,16 @@ pdu_process_time = Histogram(
|
||||
)
|
||||
|
||||
|
||||
last_pdu_age_metric = Gauge(
|
||||
"synapse_federation_last_received_pdu_age",
|
||||
"The age (in seconds) of the last PDU successfully received from the given domain",
|
||||
labelnames=("server_name",),
|
||||
)
|
||||
|
||||
|
||||
class FederationServer(FederationBase):
|
||||
def __init__(self, hs):
|
||||
super(FederationServer, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
self.auth = hs.get_auth()
|
||||
self.handler = hs.get_handlers().federation_handler
|
||||
@@ -118,6 +125,10 @@ class FederationServer(FederationBase):
|
||||
hs, "state_ids_resp", timeout_ms=30000
|
||||
)
|
||||
|
||||
self._federation_metrics_domains = (
|
||||
hs.get_config().federation.federation_metrics_domains
|
||||
)
|
||||
|
||||
async def on_backfill_request(
|
||||
self, origin: str, room_id: str, versions: List[str], limit: int
|
||||
) -> Tuple[int, Dict[str, Any]]:
|
||||
@@ -262,7 +273,11 @@ class FederationServer(FederationBase):
|
||||
|
||||
pdus_by_room = {} # type: Dict[str, List[EventBase]]
|
||||
|
||||
newest_pdu_ts = 0
|
||||
|
||||
for p in transaction.pdus: # type: ignore
|
||||
# FIXME (richardv): I don't think this works:
|
||||
# https://github.com/matrix-org/synapse/issues/8429
|
||||
if "unsigned" in p:
|
||||
unsigned = p["unsigned"]
|
||||
if "age" in unsigned:
|
||||
@@ -300,6 +315,9 @@ class FederationServer(FederationBase):
|
||||
event = event_from_pdu_json(p, room_version)
|
||||
pdus_by_room.setdefault(room_id, []).append(event)
|
||||
|
||||
if event.origin_server_ts > newest_pdu_ts:
|
||||
newest_pdu_ts = event.origin_server_ts
|
||||
|
||||
pdu_results = {}
|
||||
|
||||
# we can process different rooms in parallel (which is useful if they
|
||||
@@ -340,6 +358,10 @@ class FederationServer(FederationBase):
|
||||
process_pdus_for_room, pdus_by_room.keys(), TRANSACTION_CONCURRENCY_LIMIT
|
||||
)
|
||||
|
||||
if newest_pdu_ts and origin in self._federation_metrics_domains:
|
||||
newest_pdu_age = self._clock.time_msec() - newest_pdu_ts
|
||||
last_pdu_age_metric.labels(server_name=origin).set(newest_pdu_age / 1000)
|
||||
|
||||
return pdu_results
|
||||
|
||||
async def _handle_edus_in_txn(self, origin: str, transaction: Transaction):
|
||||
|
||||
@@ -55,6 +55,15 @@ sent_pdus_destination_dist_total = Counter(
|
||||
"Total number of PDUs queued for sending across all destinations",
|
||||
)
|
||||
|
||||
# Time (in s) after Synapse's startup that we will begin to wake up destinations
|
||||
# that have catch-up outstanding.
|
||||
CATCH_UP_STARTUP_DELAY_SEC = 15
|
||||
|
||||
# Time (in s) to wait in between waking up each destination, i.e. one destination
|
||||
# will be woken up every <x> seconds after Synapse's startup until we have woken
|
||||
# every destination has outstanding catch-up.
|
||||
CATCH_UP_STARTUP_INTERVAL_SEC = 5
|
||||
|
||||
|
||||
class FederationSender:
|
||||
def __init__(self, hs: "synapse.server.HomeServer"):
|
||||
@@ -125,6 +134,14 @@ class FederationSender:
|
||||
1000.0 / hs.config.federation_rr_transactions_per_room_per_second
|
||||
)
|
||||
|
||||
# wake up destinations that have outstanding PDUs to be caught up
|
||||
self._catchup_after_startup_timer = self.clock.call_later(
|
||||
CATCH_UP_STARTUP_DELAY_SEC,
|
||||
run_as_background_process,
|
||||
"wake_destinations_needing_catchup",
|
||||
self._wake_destinations_needing_catchup,
|
||||
)
|
||||
|
||||
def _get_per_destination_queue(self, destination: str) -> PerDestinationQueue:
|
||||
"""Get or create a PerDestinationQueue for the given destination
|
||||
|
||||
@@ -209,7 +226,7 @@ class FederationSender:
|
||||
logger.debug("Sending %s to %r", event, destinations)
|
||||
|
||||
if destinations:
|
||||
self._send_pdu(event, destinations)
|
||||
await self._send_pdu(event, destinations)
|
||||
|
||||
now = self.clock.time_msec()
|
||||
ts = await self.store.get_received_ts(event.event_id)
|
||||
@@ -265,7 +282,7 @@ class FederationSender:
|
||||
finally:
|
||||
self._is_processing = False
|
||||
|
||||
def _send_pdu(self, pdu: EventBase, destinations: Iterable[str]) -> None:
|
||||
async def _send_pdu(self, pdu: EventBase, destinations: Iterable[str]) -> None:
|
||||
# We loop through all destinations to see whether we already have
|
||||
# a transaction in progress. If we do, stick it in the pending_pdus
|
||||
# table and we'll get back to it later.
|
||||
@@ -280,6 +297,13 @@ class FederationSender:
|
||||
sent_pdus_destination_dist_total.inc(len(destinations))
|
||||
sent_pdus_destination_dist_count.inc()
|
||||
|
||||
# track the fact that we have a PDU for these destinations,
|
||||
# to allow us to perform catch-up later on if the remote is unreachable
|
||||
# for a while.
|
||||
await self.store.store_destination_rooms_entries(
|
||||
destinations, pdu.room_id, pdu.internal_metadata.stream_ordering,
|
||||
)
|
||||
|
||||
for destination in destinations:
|
||||
self._get_per_destination_queue(destination).send_pdu(pdu)
|
||||
|
||||
@@ -553,3 +577,37 @@ class FederationSender:
|
||||
# Dummy implementation for case where federation sender isn't offloaded
|
||||
# to a worker.
|
||||
return [], 0, False
|
||||
|
||||
async def _wake_destinations_needing_catchup(self):
|
||||
"""
|
||||
Wakes up destinations that need catch-up and are not currently being
|
||||
backed off from.
|
||||
|
||||
In order to reduce load spikes, adds a delay between each destination.
|
||||
"""
|
||||
|
||||
last_processed = None # type: Optional[str]
|
||||
|
||||
while True:
|
||||
destinations_to_wake = await self.store.get_catch_up_outstanding_destinations(
|
||||
last_processed
|
||||
)
|
||||
|
||||
if not destinations_to_wake:
|
||||
# finished waking all destinations!
|
||||
self._catchup_after_startup_timer = None
|
||||
break
|
||||
|
||||
destinations_to_wake = [
|
||||
d
|
||||
for d in destinations_to_wake
|
||||
if self._federation_shard_config.should_handle(self._instance_name, d)
|
||||
]
|
||||
|
||||
for last_processed in destinations_to_wake:
|
||||
logger.info(
|
||||
"Destination %s has outstanding catch-up, waking up.",
|
||||
last_processed,
|
||||
)
|
||||
self.wake_destination(last_processed)
|
||||
await self.clock.sleep(CATCH_UP_STARTUP_INTERVAL_SEC)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# limitations under the License.
|
||||
import datetime
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Dict, Hashable, Iterable, List, Tuple
|
||||
from typing import TYPE_CHECKING, Dict, Hashable, Iterable, List, Optional, Tuple, cast
|
||||
|
||||
from prometheus_client import Counter
|
||||
|
||||
@@ -92,6 +92,21 @@ class PerDestinationQueue:
|
||||
self._destination = destination
|
||||
self.transmission_loop_running = False
|
||||
|
||||
# True whilst we are sending events that the remote homeserver missed
|
||||
# because it was unreachable. We start in this state so we can perform
|
||||
# catch-up at startup.
|
||||
# New events will only be sent once this is finished, at which point
|
||||
# _catching_up is flipped to False.
|
||||
self._catching_up = True # type: bool
|
||||
|
||||
# The stream_ordering of the most recent PDU that was discarded due to
|
||||
# being in catch-up mode.
|
||||
self._catchup_last_skipped = 0 # type: int
|
||||
|
||||
# Cache of the last successfully-transmitted stream ordering for this
|
||||
# destination (we are the only updater so this is safe)
|
||||
self._last_successful_stream_ordering = None # type: Optional[int]
|
||||
|
||||
# a list of pending PDUs
|
||||
self._pending_pdus = [] # type: List[EventBase]
|
||||
|
||||
@@ -138,7 +153,13 @@ class PerDestinationQueue:
|
||||
Args:
|
||||
pdu: pdu to send
|
||||
"""
|
||||
self._pending_pdus.append(pdu)
|
||||
if not self._catching_up or self._last_successful_stream_ordering is None:
|
||||
# only enqueue the PDU if we are not catching up (False) or do not
|
||||
# yet know if we have anything to catch up (None)
|
||||
self._pending_pdus.append(pdu)
|
||||
else:
|
||||
self._catchup_last_skipped = pdu.internal_metadata.stream_ordering
|
||||
|
||||
self.attempt_new_transaction()
|
||||
|
||||
def send_presence(self, states: Iterable[UserPresenceState]) -> None:
|
||||
@@ -218,6 +239,13 @@ class PerDestinationQueue:
|
||||
# hence why we throw the result away.
|
||||
await get_retry_limiter(self._destination, self._clock, self._store)
|
||||
|
||||
if self._catching_up:
|
||||
# we potentially need to catch-up first
|
||||
await self._catch_up_transmission_loop()
|
||||
if self._catching_up:
|
||||
# not caught up yet
|
||||
return
|
||||
|
||||
pending_pdus = []
|
||||
while True:
|
||||
# We have to keep 2 free slots for presence and rr_edus
|
||||
@@ -325,6 +353,17 @@ class PerDestinationQueue:
|
||||
|
||||
self._last_device_stream_id = device_stream_id
|
||||
self._last_device_list_stream_id = dev_list_id
|
||||
|
||||
if pending_pdus:
|
||||
# we sent some PDUs and it was successful, so update our
|
||||
# last_successful_stream_ordering in the destinations table.
|
||||
final_pdu = pending_pdus[-1]
|
||||
last_successful_stream_ordering = (
|
||||
final_pdu.internal_metadata.stream_ordering
|
||||
)
|
||||
await self._store.set_destination_last_successful_stream_ordering(
|
||||
self._destination, last_successful_stream_ordering
|
||||
)
|
||||
else:
|
||||
break
|
||||
except NotRetryingDestination as e:
|
||||
@@ -340,8 +379,9 @@ class PerDestinationQueue:
|
||||
if e.retry_interval > 60 * 60 * 1000:
|
||||
# we won't retry for another hour!
|
||||
# (this suggests a significant outage)
|
||||
# We drop pending PDUs and EDUs because otherwise they will
|
||||
# We drop pending EDUs because otherwise they will
|
||||
# rack up indefinitely.
|
||||
# (Dropping PDUs is already performed by `_start_catching_up`.)
|
||||
# Note that:
|
||||
# - the EDUs that are being dropped here are those that we can
|
||||
# afford to drop (specifically, only typing notifications,
|
||||
@@ -353,11 +393,12 @@ class PerDestinationQueue:
|
||||
|
||||
# dropping read receipts is a bit sad but should be solved
|
||||
# through another mechanism, because this is all volatile!
|
||||
self._pending_pdus = []
|
||||
self._pending_edus = []
|
||||
self._pending_edus_keyed = {}
|
||||
self._pending_presence = {}
|
||||
self._pending_rrs = {}
|
||||
|
||||
self._start_catching_up()
|
||||
except FederationDeniedError as e:
|
||||
logger.info(e)
|
||||
except HttpResponseException as e:
|
||||
@@ -367,6 +408,8 @@ class PerDestinationQueue:
|
||||
e.code,
|
||||
e,
|
||||
)
|
||||
|
||||
self._start_catching_up()
|
||||
except RequestSendFailed as e:
|
||||
logger.warning(
|
||||
"TX [%s] Failed to send transaction: %s", self._destination, e
|
||||
@@ -376,16 +419,96 @@ class PerDestinationQueue:
|
||||
logger.info(
|
||||
"Failed to send event %s to %s", p.event_id, self._destination
|
||||
)
|
||||
|
||||
self._start_catching_up()
|
||||
except Exception:
|
||||
logger.exception("TX [%s] Failed to send transaction", self._destination)
|
||||
for p in pending_pdus:
|
||||
logger.info(
|
||||
"Failed to send event %s to %s", p.event_id, self._destination
|
||||
)
|
||||
|
||||
self._start_catching_up()
|
||||
finally:
|
||||
# We want to be *very* sure we clear this after we stop processing
|
||||
self.transmission_loop_running = False
|
||||
|
||||
async def _catch_up_transmission_loop(self) -> None:
|
||||
first_catch_up_check = self._last_successful_stream_ordering is None
|
||||
|
||||
if first_catch_up_check:
|
||||
# first catchup so get last_successful_stream_ordering from database
|
||||
self._last_successful_stream_ordering = await self._store.get_destination_last_successful_stream_ordering(
|
||||
self._destination
|
||||
)
|
||||
|
||||
if self._last_successful_stream_ordering is None:
|
||||
# if it's still None, then this means we don't have the information
|
||||
# in our database we haven't successfully sent a PDU to this server
|
||||
# (at least since the introduction of the feature tracking
|
||||
# last_successful_stream_ordering).
|
||||
# Sadly, this means we can't do anything here as we don't know what
|
||||
# needs catching up — so catching up is futile; let's stop.
|
||||
self._catching_up = False
|
||||
return
|
||||
|
||||
# get at most 50 catchup room/PDUs
|
||||
while True:
|
||||
event_ids = await self._store.get_catch_up_room_event_ids(
|
||||
self._destination, self._last_successful_stream_ordering,
|
||||
)
|
||||
|
||||
if not event_ids:
|
||||
# No more events to catch up on, but we can't ignore the chance
|
||||
# of a race condition, so we check that no new events have been
|
||||
# skipped due to us being in catch-up mode
|
||||
|
||||
if self._catchup_last_skipped > self._last_successful_stream_ordering:
|
||||
# another event has been skipped because we were in catch-up mode
|
||||
continue
|
||||
|
||||
# we are done catching up!
|
||||
self._catching_up = False
|
||||
break
|
||||
|
||||
if first_catch_up_check:
|
||||
# as this is our check for needing catch-up, we may have PDUs in
|
||||
# the queue from before we *knew* we had to do catch-up, so
|
||||
# clear those out now.
|
||||
self._start_catching_up()
|
||||
|
||||
# fetch the relevant events from the event store
|
||||
# - redacted behaviour of REDACT is fine, since we only send metadata
|
||||
# of redacted events to the destination.
|
||||
# - don't need to worry about rejected events as we do not actively
|
||||
# forward received events over federation.
|
||||
catchup_pdus = await self._store.get_events_as_list(event_ids)
|
||||
if not catchup_pdus:
|
||||
raise AssertionError(
|
||||
"No events retrieved when we asked for %r. "
|
||||
"This should not happen." % event_ids
|
||||
)
|
||||
|
||||
if logger.isEnabledFor(logging.INFO):
|
||||
rooms = [p.room_id for p in catchup_pdus]
|
||||
logger.info("Catching up rooms to %s: %r", self._destination, rooms)
|
||||
|
||||
success = await self._transaction_manager.send_new_transaction(
|
||||
self._destination, catchup_pdus, []
|
||||
)
|
||||
|
||||
if not success:
|
||||
return
|
||||
|
||||
sent_transactions_counter.inc()
|
||||
final_pdu = catchup_pdus[-1]
|
||||
self._last_successful_stream_ordering = cast(
|
||||
int, final_pdu.internal_metadata.stream_ordering
|
||||
)
|
||||
await self._store.set_destination_last_successful_stream_ordering(
|
||||
self._destination, self._last_successful_stream_ordering
|
||||
)
|
||||
|
||||
def _get_rr_edus(self, force_flush: bool) -> Iterable[Edu]:
|
||||
if not self._pending_rrs:
|
||||
return
|
||||
@@ -446,3 +569,12 @@ class PerDestinationQueue:
|
||||
]
|
||||
|
||||
return (edus, stream_id)
|
||||
|
||||
def _start_catching_up(self) -> None:
|
||||
"""
|
||||
Marks this destination as being in catch-up mode.
|
||||
|
||||
This throws away the PDU queue.
|
||||
"""
|
||||
self._catching_up = True
|
||||
self._pending_pdus = []
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from prometheus_client import Gauge
|
||||
|
||||
from synapse.api.errors import HttpResponseException
|
||||
from synapse.events import EventBase
|
||||
from synapse.federation.persistence import TransactionActions
|
||||
@@ -34,6 +36,12 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
last_pdu_age_metric = Gauge(
|
||||
"synapse_federation_last_sent_pdu_age",
|
||||
"The age (in seconds) of the last PDU successfully sent to the given domain",
|
||||
labelnames=("server_name",),
|
||||
)
|
||||
|
||||
|
||||
class TransactionManager:
|
||||
"""Helper class which handles building and sending transactions
|
||||
@@ -48,6 +56,10 @@ class TransactionManager:
|
||||
self._transaction_actions = TransactionActions(self._store)
|
||||
self._transport_layer = hs.get_federation_transport_client()
|
||||
|
||||
self._federation_metrics_domains = (
|
||||
hs.get_config().federation.federation_metrics_domains
|
||||
)
|
||||
|
||||
# HACK to get unique tx id
|
||||
self._next_txn_id = int(self.clock.time_msec())
|
||||
|
||||
@@ -119,6 +131,9 @@ class TransactionManager:
|
||||
|
||||
# FIXME (erikj): This is a bit of a hack to make the Pdu age
|
||||
# keys work
|
||||
# FIXME (richardv): I also believe it no longer works. We (now?) store
|
||||
# "age_ts" in "unsigned" rather than at the top level. See
|
||||
# https://github.com/matrix-org/synapse/issues/8429.
|
||||
def json_data_cb():
|
||||
data = transaction.get_dict()
|
||||
now = int(self.clock.time_msec())
|
||||
@@ -167,5 +182,12 @@ class TransactionManager:
|
||||
)
|
||||
success = False
|
||||
|
||||
if success and pdus and destination in self._federation_metrics_domains:
|
||||
last_pdu = pdus[-1]
|
||||
last_pdu_age = self.clock.time_msec() - last_pdu.origin_server_ts
|
||||
last_pdu_age_metric.labels(server_name=destination).set(
|
||||
last_pdu_age / 1000
|
||||
)
|
||||
|
||||
set_tag(tags.ERROR, not success)
|
||||
return success
|
||||
|
||||
@@ -68,7 +68,7 @@ class TransportLayerServer(JsonResource):
|
||||
self.clock = hs.get_clock()
|
||||
self.servlet_groups = servlet_groups
|
||||
|
||||
super(TransportLayerServer, self).__init__(hs, canonical_json=False)
|
||||
super().__init__(hs, canonical_json=False)
|
||||
|
||||
self.authenticator = Authenticator(hs)
|
||||
self.ratelimiter = hs.get_federation_ratelimiter()
|
||||
@@ -376,9 +376,7 @@ class FederationSendServlet(BaseFederationServlet):
|
||||
RATELIMIT = False
|
||||
|
||||
def __init__(self, handler, server_name, **kwargs):
|
||||
super(FederationSendServlet, self).__init__(
|
||||
handler, server_name=server_name, **kwargs
|
||||
)
|
||||
super().__init__(handler, server_name=server_name, **kwargs)
|
||||
self.server_name = server_name
|
||||
|
||||
# This is when someone is trying to send us a bunch of data.
|
||||
@@ -773,9 +771,7 @@ class PublicRoomList(BaseFederationServlet):
|
||||
PATH = "/publicRooms"
|
||||
|
||||
def __init__(self, handler, authenticator, ratelimiter, server_name, allow_access):
|
||||
super(PublicRoomList, self).__init__(
|
||||
handler, authenticator, ratelimiter, server_name
|
||||
)
|
||||
super().__init__(handler, authenticator, ratelimiter, server_name)
|
||||
self.allow_access = allow_access
|
||||
|
||||
async def on_GET(self, origin, content, query):
|
||||
|
||||
@@ -336,7 +336,7 @@ class GroupsServerWorkerHandler:
|
||||
|
||||
class GroupsServerHandler(GroupsServerWorkerHandler):
|
||||
def __init__(self, hs):
|
||||
super(GroupsServerHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
# Ensure attestations get renewed
|
||||
hs.get_groups_attestation_renewer()
|
||||
|
||||
@@ -76,7 +76,7 @@ def create_issuing_service(reactor, acme_url, account_key_file, well_known_resou
|
||||
)
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(slots=True)
|
||||
@implementer(ICertificateStore)
|
||||
class ErsatzStore:
|
||||
"""
|
||||
|
||||
@@ -28,7 +28,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class AdminHandler(BaseHandler):
|
||||
def __init__(self, hs):
|
||||
super(AdminHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
self.storage = hs.get_storage()
|
||||
self.state_store = self.storage.state
|
||||
@@ -125,8 +125,8 @@ class AdminHandler(BaseHandler):
|
||||
else:
|
||||
stream_ordering = room.stream_ordering
|
||||
|
||||
from_key = str(RoomStreamToken(0, 0))
|
||||
to_key = str(RoomStreamToken(None, stream_ordering))
|
||||
from_key = RoomStreamToken(0, 0)
|
||||
to_key = RoomStreamToken(None, stream_ordering)
|
||||
|
||||
written_events = set() # Events that we've processed in this room
|
||||
|
||||
|
||||
@@ -137,6 +137,15 @@ def login_id_phone_to_thirdparty(identifier: JsonDict) -> Dict[str, str]:
|
||||
}
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class SsoLoginExtraAttributes:
|
||||
"""Data we track about SAML2 sessions"""
|
||||
|
||||
# time the session was created, in milliseconds
|
||||
creation_time = attr.ib(type=int)
|
||||
extra_attributes = attr.ib(type=JsonDict)
|
||||
|
||||
|
||||
class AuthHandler(BaseHandler):
|
||||
SESSION_EXPIRE_MS = 48 * 60 * 60 * 1000
|
||||
|
||||
@@ -145,7 +154,7 @@ class AuthHandler(BaseHandler):
|
||||
Args:
|
||||
hs (synapse.server.HomeServer):
|
||||
"""
|
||||
super(AuthHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
self.checkers = {} # type: Dict[str, UserInteractiveAuthChecker]
|
||||
for auth_checker_class in INTERACTIVE_AUTH_CHECKERS:
|
||||
@@ -239,6 +248,10 @@ class AuthHandler(BaseHandler):
|
||||
# cast to tuple for use with str.startswith
|
||||
self._whitelisted_sso_clients = tuple(hs.config.sso_client_whitelist)
|
||||
|
||||
# A mapping of user ID to extra attributes to include in the login
|
||||
# response.
|
||||
self._extra_attributes = {} # type: Dict[str, SsoLoginExtraAttributes]
|
||||
|
||||
async def validate_user_via_ui_auth(
|
||||
self,
|
||||
requester: Requester,
|
||||
@@ -1165,6 +1178,7 @@ class AuthHandler(BaseHandler):
|
||||
registered_user_id: str,
|
||||
request: SynapseRequest,
|
||||
client_redirect_url: str,
|
||||
extra_attributes: Optional[JsonDict] = None,
|
||||
):
|
||||
"""Having figured out a mxid for this user, complete the HTTP request
|
||||
|
||||
@@ -1173,6 +1187,8 @@ class AuthHandler(BaseHandler):
|
||||
request: The request to complete.
|
||||
client_redirect_url: The URL to which to redirect the user at the end of the
|
||||
process.
|
||||
extra_attributes: Extra attributes which will be passed to the client
|
||||
during successful login. Must be JSON serializable.
|
||||
"""
|
||||
# If the account has been deactivated, do not proceed with the login
|
||||
# flow.
|
||||
@@ -1181,19 +1197,30 @@ class AuthHandler(BaseHandler):
|
||||
respond_with_html(request, 403, self._sso_account_deactivated_template)
|
||||
return
|
||||
|
||||
self._complete_sso_login(registered_user_id, request, client_redirect_url)
|
||||
self._complete_sso_login(
|
||||
registered_user_id, request, client_redirect_url, extra_attributes
|
||||
)
|
||||
|
||||
def _complete_sso_login(
|
||||
self,
|
||||
registered_user_id: str,
|
||||
request: SynapseRequest,
|
||||
client_redirect_url: str,
|
||||
extra_attributes: Optional[JsonDict] = None,
|
||||
):
|
||||
"""
|
||||
The synchronous portion of complete_sso_login.
|
||||
|
||||
This exists purely for backwards compatibility of synapse.module_api.ModuleApi.
|
||||
"""
|
||||
# Store any extra attributes which will be passed in the login response.
|
||||
# Note that this is per-user so it may overwrite a previous value, this
|
||||
# is considered OK since the newest SSO attributes should be most valid.
|
||||
if extra_attributes:
|
||||
self._extra_attributes[registered_user_id] = SsoLoginExtraAttributes(
|
||||
self._clock.time_msec(), extra_attributes,
|
||||
)
|
||||
|
||||
# Create a login token
|
||||
login_token = self.macaroon_gen.generate_short_term_login_token(
|
||||
registered_user_id
|
||||
@@ -1226,6 +1253,37 @@ class AuthHandler(BaseHandler):
|
||||
)
|
||||
respond_with_html(request, 200, html)
|
||||
|
||||
async def _sso_login_callback(self, login_result: JsonDict) -> None:
|
||||
"""
|
||||
A login callback which might add additional attributes to the login response.
|
||||
|
||||
Args:
|
||||
login_result: The data to be sent to the client. Includes the user
|
||||
ID and access token.
|
||||
"""
|
||||
# Expire attributes before processing. Note that there shouldn't be any
|
||||
# valid logins that still have extra attributes.
|
||||
self._expire_sso_extra_attributes()
|
||||
|
||||
extra_attributes = self._extra_attributes.get(login_result["user_id"])
|
||||
if extra_attributes:
|
||||
login_result.update(extra_attributes.extra_attributes)
|
||||
|
||||
def _expire_sso_extra_attributes(self) -> None:
|
||||
"""
|
||||
Iterate through the mapping of user IDs to extra attributes and remove any that are no longer valid.
|
||||
"""
|
||||
# TODO This should match the amount of time the macaroon is valid for.
|
||||
LOGIN_TOKEN_EXPIRATION_TIME = 2 * 60 * 1000
|
||||
expire_before = self._clock.time_msec() - LOGIN_TOKEN_EXPIRATION_TIME
|
||||
to_expire = set()
|
||||
for user_id, data in self._extra_attributes.items():
|
||||
if data.creation_time < expire_before:
|
||||
to_expire.add(user_id)
|
||||
for user_id in to_expire:
|
||||
logger.debug("Expiring extra attributes for user %s", user_id)
|
||||
del self._extra_attributes[user_id]
|
||||
|
||||
@staticmethod
|
||||
def add_query_param_to_url(url: str, param_name: str, param: Any):
|
||||
url_parts = list(urllib.parse.urlparse(url))
|
||||
@@ -1235,7 +1293,7 @@ class AuthHandler(BaseHandler):
|
||||
return urllib.parse.urlunparse(url_parts)
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(slots=True)
|
||||
class MacaroonGenerator:
|
||||
|
||||
hs = attr.ib()
|
||||
|
||||
@@ -29,7 +29,7 @@ class DeactivateAccountHandler(BaseHandler):
|
||||
"""Handler which deals with deactivating user accounts."""
|
||||
|
||||
def __init__(self, hs):
|
||||
super(DeactivateAccountHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
self.hs = hs
|
||||
self._auth_handler = hs.get_auth_handler()
|
||||
self._device_handler = hs.get_device_handler()
|
||||
|
||||
+30
-16
@@ -20,6 +20,7 @@ from typing import Any, Dict, List, Optional
|
||||
from synapse.api import errors
|
||||
from synapse.api.constants import EventTypes
|
||||
from synapse.api.errors import (
|
||||
Codes,
|
||||
FederationDeniedError,
|
||||
HttpResponseException,
|
||||
RequestSendFailed,
|
||||
@@ -28,7 +29,7 @@ from synapse.api.errors import (
|
||||
from synapse.logging.opentracing import log_kv, set_tag, trace
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.types import (
|
||||
RoomStreamToken,
|
||||
StreamToken,
|
||||
get_domain_from_id,
|
||||
get_verify_key_from_cross_signing_key,
|
||||
)
|
||||
@@ -47,7 +48,7 @@ MAX_DEVICE_DISPLAY_NAME_LEN = 100
|
||||
|
||||
class DeviceWorkerHandler(BaseHandler):
|
||||
def __init__(self, hs):
|
||||
super(DeviceWorkerHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
self.hs = hs
|
||||
self.state = hs.get_state_handler()
|
||||
@@ -104,18 +105,14 @@ class DeviceWorkerHandler(BaseHandler):
|
||||
|
||||
@trace
|
||||
@measure_func("device.get_user_ids_changed")
|
||||
async def get_user_ids_changed(self, user_id, from_token):
|
||||
async def get_user_ids_changed(self, user_id: str, from_token: StreamToken):
|
||||
"""Get list of users that have had the devices updated, or have newly
|
||||
joined a room, that `user_id` may be interested in.
|
||||
|
||||
Args:
|
||||
user_id (str)
|
||||
from_token (StreamToken)
|
||||
"""
|
||||
|
||||
set_tag("user_id", user_id)
|
||||
set_tag("from_token", from_token)
|
||||
now_room_key = await self.store.get_room_events_max_id()
|
||||
now_room_key = self.store.get_room_max_token()
|
||||
|
||||
room_ids = await self.store.get_rooms_for_user(user_id)
|
||||
|
||||
@@ -142,7 +139,7 @@ class DeviceWorkerHandler(BaseHandler):
|
||||
)
|
||||
rooms_changed.update(event.room_id for event in member_events)
|
||||
|
||||
stream_ordering = RoomStreamToken.parse_stream_token(from_token.room_key).stream
|
||||
stream_ordering = from_token.room_key.stream
|
||||
|
||||
possibly_changed = set(changed)
|
||||
possibly_left = set()
|
||||
@@ -253,7 +250,7 @@ class DeviceWorkerHandler(BaseHandler):
|
||||
|
||||
class DeviceHandler(DeviceWorkerHandler):
|
||||
def __init__(self, hs):
|
||||
super(DeviceHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
self.federation_sender = hs.get_federation_sender()
|
||||
|
||||
@@ -267,6 +264,24 @@ class DeviceHandler(DeviceWorkerHandler):
|
||||
|
||||
hs.get_distributor().observe("user_left_room", self.user_left_room)
|
||||
|
||||
def _check_device_name_length(self, name: str):
|
||||
"""
|
||||
Checks whether a device name is longer than the maximum allowed length.
|
||||
|
||||
Args:
|
||||
name: The name of the device.
|
||||
|
||||
Raises:
|
||||
SynapseError: if the device name is too long.
|
||||
"""
|
||||
if name and len(name) > MAX_DEVICE_DISPLAY_NAME_LEN:
|
||||
raise SynapseError(
|
||||
400,
|
||||
"Device display name is too long (max %i)"
|
||||
% (MAX_DEVICE_DISPLAY_NAME_LEN,),
|
||||
errcode=Codes.TOO_LARGE,
|
||||
)
|
||||
|
||||
async def check_device_registered(
|
||||
self, user_id, device_id, initial_device_display_name=None
|
||||
):
|
||||
@@ -284,6 +299,9 @@ class DeviceHandler(DeviceWorkerHandler):
|
||||
Returns:
|
||||
str: device id (generated if none was supplied)
|
||||
"""
|
||||
|
||||
self._check_device_name_length(initial_device_display_name)
|
||||
|
||||
if device_id is not None:
|
||||
new_device = await self.store.store_device(
|
||||
user_id=user_id,
|
||||
@@ -399,12 +417,8 @@ class DeviceHandler(DeviceWorkerHandler):
|
||||
|
||||
# Reject a new displayname which is too long.
|
||||
new_display_name = content.get("display_name")
|
||||
if new_display_name and len(new_display_name) > MAX_DEVICE_DISPLAY_NAME_LEN:
|
||||
raise SynapseError(
|
||||
400,
|
||||
"Device display name is too long (max %i)"
|
||||
% (MAX_DEVICE_DISPLAY_NAME_LEN,),
|
||||
)
|
||||
|
||||
self._check_device_name_length(new_display_name)
|
||||
|
||||
try:
|
||||
await self.store.update_device(
|
||||
|
||||
@@ -37,7 +37,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class DirectoryHandler(BaseHandler):
|
||||
def __init__(self, hs):
|
||||
super(DirectoryHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
self.state = hs.get_state_handler()
|
||||
self.appservice_handler = hs.get_application_service_handler()
|
||||
|
||||
@@ -1201,7 +1201,7 @@ def _one_time_keys_match(old_key_json, new_key):
|
||||
return old_key == new_key_copy
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(slots=True)
|
||||
class SignatureListItem:
|
||||
"""An item in the signature list as used by upload_signatures_for_device_keys.
|
||||
"""
|
||||
|
||||
@@ -37,11 +37,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class EventStreamHandler(BaseHandler):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super(EventStreamHandler, self).__init__(hs)
|
||||
|
||||
self.distributor = hs.get_distributor()
|
||||
self.distributor.declare("started_user_eventstream")
|
||||
self.distributor.declare("stopped_user_eventstream")
|
||||
super().__init__(hs)
|
||||
|
||||
self.clock = hs.get_clock()
|
||||
|
||||
@@ -137,8 +133,8 @@ class EventStreamHandler(BaseHandler):
|
||||
|
||||
chunk = {
|
||||
"chunk": chunks,
|
||||
"start": tokens[0].to_string(),
|
||||
"end": tokens[1].to_string(),
|
||||
"start": await tokens[0].to_string(self.store),
|
||||
"end": await tokens[1].to_string(self.store),
|
||||
}
|
||||
|
||||
return chunk
|
||||
@@ -146,7 +142,7 @@ class EventStreamHandler(BaseHandler):
|
||||
|
||||
class EventHandler(BaseHandler):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super(EventHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
self.storage = hs.get_storage()
|
||||
|
||||
async def get_event(
|
||||
|
||||
@@ -21,7 +21,7 @@ import itertools
|
||||
import logging
|
||||
from collections.abc import Container
|
||||
from http import HTTPStatus
|
||||
from typing import Dict, Iterable, List, Optional, Sequence, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Tuple, Union
|
||||
|
||||
import attr
|
||||
from signedjson.key import decode_verify_key_bytes
|
||||
@@ -69,26 +69,29 @@ from synapse.replication.http.federation import (
|
||||
ReplicationFederationSendEventsRestServlet,
|
||||
ReplicationStoreRoomOnInviteRestServlet,
|
||||
)
|
||||
from synapse.replication.http.membership import ReplicationUserJoinedLeftRoomRestServlet
|
||||
from synapse.state import StateResolutionStore, resolve_events_with_store
|
||||
from synapse.state import StateResolutionStore
|
||||
from synapse.storage.databases.main.events_worker import EventRedactBehaviour
|
||||
from synapse.types import (
|
||||
JsonDict,
|
||||
MutableStateMap,
|
||||
PersistedEventPosition,
|
||||
RoomStreamToken,
|
||||
StateMap,
|
||||
UserID,
|
||||
get_domain_from_id,
|
||||
)
|
||||
from synapse.util.async_helpers import Linearizer, concurrently_execute
|
||||
from synapse.util.distributor import user_joined_room
|
||||
from synapse.util.retryutils import NotRetryingDestination
|
||||
from synapse.util.stringutils import shortstr
|
||||
from synapse.visibility import filter_events_for_server
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(slots=True)
|
||||
class _NewEventInfo:
|
||||
"""Holds information about a received event, ready for passing to _handle_new_events
|
||||
|
||||
@@ -116,8 +119,8 @@ class FederationHandler(BaseHandler):
|
||||
rooms.
|
||||
"""
|
||||
|
||||
def __init__(self, hs):
|
||||
super(FederationHandler, self).__init__(hs)
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super().__init__(hs)
|
||||
|
||||
self.hs = hs
|
||||
|
||||
@@ -126,11 +129,11 @@ class FederationHandler(BaseHandler):
|
||||
self.state_store = self.storage.state
|
||||
self.federation_client = hs.get_federation_client()
|
||||
self.state_handler = hs.get_state_handler()
|
||||
self._state_resolution_handler = hs.get_state_resolution_handler()
|
||||
self.server_name = hs.hostname
|
||||
self.keyring = hs.get_keyring()
|
||||
self.action_generator = hs.get_action_generator()
|
||||
self.is_mine_id = hs.is_mine_id
|
||||
self.pusher_pool = hs.get_pusherpool()
|
||||
self.spam_checker = hs.get_spam_checker()
|
||||
self.event_creation_handler = hs.get_event_creation_handler()
|
||||
self._message_handler = hs.get_message_handler()
|
||||
@@ -141,9 +144,6 @@ class FederationHandler(BaseHandler):
|
||||
self._replication = hs.get_replication_data_handler()
|
||||
|
||||
self._send_events = ReplicationFederationSendEventsRestServlet.make_client(hs)
|
||||
self._notify_user_membership_change = ReplicationUserJoinedLeftRoomRestServlet.make_client(
|
||||
hs
|
||||
)
|
||||
self._clean_room_for_join_client = ReplicationCleanRoomRestServlet.make_client(
|
||||
hs
|
||||
)
|
||||
@@ -159,8 +159,9 @@ class FederationHandler(BaseHandler):
|
||||
self._device_list_updater = hs.get_device_handler().device_list_updater
|
||||
self._maybe_store_room_on_invite = self.store.maybe_store_room_on_invite
|
||||
|
||||
# When joining a room we need to queue any events for that room up
|
||||
self.room_queues = {}
|
||||
# When joining a room we need to queue any events for that room up.
|
||||
# For each room, a list of (pdu, origin) tuples.
|
||||
self.room_queues = {} # type: Dict[str, List[Tuple[EventBase, str]]]
|
||||
self._room_pdu_linearizer = Linearizer("fed_room_pdu")
|
||||
|
||||
self.third_party_event_rules = hs.get_third_party_event_rules()
|
||||
@@ -285,7 +286,7 @@ class FederationHandler(BaseHandler):
|
||||
raise Exception(
|
||||
"Error fetching missing prev_events for %s: %s"
|
||||
% (event_id, e)
|
||||
)
|
||||
) from e
|
||||
|
||||
# Update the set of things we've seen after trying to
|
||||
# fetch the missing stuff
|
||||
@@ -384,8 +385,7 @@ class FederationHandler(BaseHandler):
|
||||
event_map[x.event_id] = x
|
||||
|
||||
room_version = await self.store.get_room_version_id(room_id)
|
||||
state_map = await resolve_events_with_store(
|
||||
self.clock,
|
||||
state_map = await self._state_resolution_handler.resolve_events_with_store(
|
||||
room_id,
|
||||
room_version,
|
||||
state_maps,
|
||||
@@ -704,31 +704,10 @@ class FederationHandler(BaseHandler):
|
||||
logger.debug("[%s %s] Processing event: %s", room_id, event_id, event)
|
||||
|
||||
try:
|
||||
context = await self._handle_new_event(origin, event, state=state)
|
||||
await self._handle_new_event(origin, event, state=state)
|
||||
except AuthError as e:
|
||||
raise FederationError("ERROR", e.code, e.msg, affected=event.event_id)
|
||||
|
||||
if event.type == EventTypes.Member:
|
||||
if event.membership == Membership.JOIN:
|
||||
# Only fire user_joined_room if the user has acutally
|
||||
# joined the room. Don't bother if the user is just
|
||||
# changing their profile info.
|
||||
newly_joined = True
|
||||
|
||||
prev_state_ids = await context.get_prev_state_ids()
|
||||
|
||||
prev_state_id = prev_state_ids.get((event.type, event.state_key))
|
||||
if prev_state_id:
|
||||
prev_state = await self.store.get_event(
|
||||
prev_state_id, allow_none=True
|
||||
)
|
||||
if prev_state and prev_state.membership == Membership.JOIN:
|
||||
newly_joined = False
|
||||
|
||||
if newly_joined:
|
||||
user = UserID.from_string(event.state_key)
|
||||
await self.user_joined_room(user, room_id)
|
||||
|
||||
# For encrypted messages we check that we know about the sending device,
|
||||
# if we don't then we mark the device cache for that user as stale.
|
||||
if event.type == EventTypes.Encrypted:
|
||||
@@ -839,6 +818,9 @@ class FederationHandler(BaseHandler):
|
||||
dest, room_id, limit=limit, extremities=extremities
|
||||
)
|
||||
|
||||
if not events:
|
||||
return []
|
||||
|
||||
# ideally we'd sanity check the events here for excess prev_events etc,
|
||||
# but it's hard to reject events at this point without completely
|
||||
# breaking backfill in the same way that it is currently broken by
|
||||
@@ -923,7 +905,8 @@ class FederationHandler(BaseHandler):
|
||||
)
|
||||
)
|
||||
|
||||
await self._handle_new_events(dest, ev_infos, backfilled=True)
|
||||
if ev_infos:
|
||||
await self._handle_new_events(dest, room_id, ev_infos, backfilled=True)
|
||||
|
||||
# Step 2: Persist the rest of the events in the chunk one by one
|
||||
events.sort(key=lambda e: e.depth)
|
||||
@@ -1265,7 +1248,7 @@ class FederationHandler(BaseHandler):
|
||||
event_infos.append(_NewEventInfo(event, None, auth))
|
||||
|
||||
await self._handle_new_events(
|
||||
destination, event_infos,
|
||||
destination, room_id, event_infos,
|
||||
)
|
||||
|
||||
def _sanity_check_event(self, ev):
|
||||
@@ -1412,15 +1395,15 @@ class FederationHandler(BaseHandler):
|
||||
)
|
||||
|
||||
max_stream_id = await self._persist_auth_tree(
|
||||
origin, auth_chain, state, event, room_version_obj
|
||||
origin, room_id, auth_chain, state, event, room_version_obj
|
||||
)
|
||||
|
||||
# We wait here until this instance has seen the events come down
|
||||
# replication (if we're using replication) as the below uses caches.
|
||||
#
|
||||
# TODO: Currently the events stream is written to from master
|
||||
await self._replication.wait_for_stream_position(
|
||||
self.config.worker.writers.events, "events", max_stream_id
|
||||
self.config.worker.events_shard_config.get_instance(room_id),
|
||||
"events",
|
||||
max_stream_id,
|
||||
)
|
||||
|
||||
# Check whether this room is the result of an upgrade of a room we already know
|
||||
@@ -1599,11 +1582,6 @@ class FederationHandler(BaseHandler):
|
||||
event.signatures,
|
||||
)
|
||||
|
||||
if event.type == EventTypes.Member:
|
||||
if event.content["membership"] == Membership.JOIN:
|
||||
user = UserID.from_string(event.state_key)
|
||||
await self.user_joined_room(user, event.room_id)
|
||||
|
||||
prev_state_ids = await context.get_prev_state_ids()
|
||||
|
||||
state_ids = list(prev_state_ids.values())
|
||||
@@ -1674,7 +1652,7 @@ class FederationHandler(BaseHandler):
|
||||
)
|
||||
|
||||
context = await self.state_handler.compute_event_context(event)
|
||||
await self.persist_events_and_notify([(event, context)])
|
||||
await self.persist_events_and_notify(event.room_id, [(event, context)])
|
||||
|
||||
return event
|
||||
|
||||
@@ -1701,7 +1679,9 @@ class FederationHandler(BaseHandler):
|
||||
await self.federation_client.send_leave(host_list, event)
|
||||
|
||||
context = await self.state_handler.compute_event_context(event)
|
||||
stream_id = await self.persist_events_and_notify([(event, context)])
|
||||
stream_id = await self.persist_events_and_notify(
|
||||
event.room_id, [(event, context)]
|
||||
)
|
||||
|
||||
return event, stream_id
|
||||
|
||||
@@ -1949,7 +1929,7 @@ class FederationHandler(BaseHandler):
|
||||
)
|
||||
|
||||
await self.persist_events_and_notify(
|
||||
[(event, context)], backfilled=backfilled
|
||||
event.room_id, [(event, context)], backfilled=backfilled
|
||||
)
|
||||
except Exception:
|
||||
run_in_background(
|
||||
@@ -1962,6 +1942,7 @@ class FederationHandler(BaseHandler):
|
||||
async def _handle_new_events(
|
||||
self,
|
||||
origin: str,
|
||||
room_id: str,
|
||||
event_infos: Iterable[_NewEventInfo],
|
||||
backfilled: bool = False,
|
||||
) -> None:
|
||||
@@ -1993,6 +1974,7 @@ class FederationHandler(BaseHandler):
|
||||
)
|
||||
|
||||
await self.persist_events_and_notify(
|
||||
room_id,
|
||||
[
|
||||
(ev_info.event, context)
|
||||
for ev_info, context in zip(event_infos, contexts)
|
||||
@@ -2003,6 +1985,7 @@ class FederationHandler(BaseHandler):
|
||||
async def _persist_auth_tree(
|
||||
self,
|
||||
origin: str,
|
||||
room_id: str,
|
||||
auth_events: List[EventBase],
|
||||
state: List[EventBase],
|
||||
event: EventBase,
|
||||
@@ -2017,6 +2000,7 @@ class FederationHandler(BaseHandler):
|
||||
|
||||
Args:
|
||||
origin: Where the events came from
|
||||
room_id,
|
||||
auth_events
|
||||
state
|
||||
event
|
||||
@@ -2091,17 +2075,20 @@ class FederationHandler(BaseHandler):
|
||||
events_to_context[e.event_id].rejected = RejectedReason.AUTH_ERROR
|
||||
|
||||
await self.persist_events_and_notify(
|
||||
room_id,
|
||||
[
|
||||
(e, events_to_context[e.event_id])
|
||||
for e in itertools.chain(auth_events, state)
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
new_event_context = await self.state_handler.compute_event_context(
|
||||
event, old_state=state
|
||||
)
|
||||
|
||||
return await self.persist_events_and_notify([(event, new_event_context)])
|
||||
return await self.persist_events_and_notify(
|
||||
room_id, [(event, new_event_context)]
|
||||
)
|
||||
|
||||
async def _prep_event(
|
||||
self,
|
||||
@@ -2184,10 +2171,10 @@ class FederationHandler(BaseHandler):
|
||||
# given state at the event. This should correctly handle cases
|
||||
# like bans, especially with state res v2.
|
||||
|
||||
state_sets = await self.state_store.get_state_groups(
|
||||
state_sets_d = await self.state_store.get_state_groups(
|
||||
event.room_id, extrem_ids
|
||||
)
|
||||
state_sets = list(state_sets.values())
|
||||
state_sets = list(state_sets_d.values()) # type: List[Iterable[EventBase]]
|
||||
state_sets.append(state)
|
||||
current_states = await self.state_handler.resolve_events(
|
||||
room_version, state_sets, event
|
||||
@@ -2952,6 +2939,7 @@ class FederationHandler(BaseHandler):
|
||||
|
||||
async def persist_events_and_notify(
|
||||
self,
|
||||
room_id: str,
|
||||
event_and_contexts: Sequence[Tuple[EventBase, EventContext]],
|
||||
backfilled: bool = False,
|
||||
) -> int:
|
||||
@@ -2959,20 +2947,26 @@ class FederationHandler(BaseHandler):
|
||||
necessary.
|
||||
|
||||
Args:
|
||||
event_and_contexts:
|
||||
room_id: The room ID of events being persisted.
|
||||
event_and_contexts: Sequence of events with their associated
|
||||
context that should be persisted. All events must belong to
|
||||
the same room.
|
||||
backfilled: Whether these events are a result of
|
||||
backfilling or not
|
||||
"""
|
||||
if self.config.worker.writers.events != self._instance_name:
|
||||
instance = self.config.worker.events_shard_config.get_instance(room_id)
|
||||
if instance != self._instance_name:
|
||||
result = await self._send_events(
|
||||
instance_name=self.config.worker.writers.events,
|
||||
instance_name=instance,
|
||||
store=self.store,
|
||||
room_id=room_id,
|
||||
event_and_contexts=event_and_contexts,
|
||||
backfilled=backfilled,
|
||||
)
|
||||
return result["max_stream_id"]
|
||||
else:
|
||||
max_stream_id = await self.storage.persistence.persist_events(
|
||||
assert self.storage.persistence
|
||||
max_stream_token = await self.storage.persistence.persist_events(
|
||||
event_and_contexts, backfilled=backfilled
|
||||
)
|
||||
|
||||
@@ -2983,12 +2977,12 @@ class FederationHandler(BaseHandler):
|
||||
|
||||
if not backfilled: # Never notify for backfilled events
|
||||
for event, _ in event_and_contexts:
|
||||
await self._notify_persisted_event(event, max_stream_id)
|
||||
await self._notify_persisted_event(event, max_stream_token)
|
||||
|
||||
return max_stream_id
|
||||
return max_stream_token.stream
|
||||
|
||||
async def _notify_persisted_event(
|
||||
self, event: EventBase, max_stream_id: int
|
||||
self, event: EventBase, max_stream_token: RoomStreamToken
|
||||
) -> None:
|
||||
"""Checks to see if notifier/pushers should be notified about the
|
||||
event or not.
|
||||
@@ -3014,12 +3008,12 @@ class FederationHandler(BaseHandler):
|
||||
elif event.internal_metadata.is_outlier():
|
||||
return
|
||||
|
||||
event_stream_id = event.internal_metadata.stream_ordering
|
||||
self.notifier.on_new_room_event(
|
||||
event, event_stream_id, max_stream_id, extra_users=extra_users
|
||||
event_pos = PersistedEventPosition(
|
||||
self._instance_name, event.internal_metadata.stream_ordering
|
||||
)
|
||||
self.notifier.on_new_room_event(
|
||||
event, event_pos, max_stream_token, extra_users=extra_users
|
||||
)
|
||||
|
||||
await self.pusher_pool.on_new_notifications(event_stream_id, max_stream_id)
|
||||
|
||||
async def _clean_room_for_join(self, room_id: str) -> None:
|
||||
"""Called to clean up any data in DB for a given room, ready for the
|
||||
@@ -3033,16 +3027,6 @@ class FederationHandler(BaseHandler):
|
||||
else:
|
||||
await self.store.clean_room_for_join(room_id)
|
||||
|
||||
async def user_joined_room(self, user: UserID, room_id: str) -> None:
|
||||
"""Called when a new user has joined the room
|
||||
"""
|
||||
if self.config.worker_app:
|
||||
await self._notify_user_membership_change(
|
||||
room_id=room_id, user_id=user.to_string(), change="joined"
|
||||
)
|
||||
else:
|
||||
user_joined_room(self.distributor, user, room_id)
|
||||
|
||||
async def get_room_complexity(
|
||||
self, remote_room_hosts: List[str], room_id: str
|
||||
) -> Optional[dict]:
|
||||
|
||||
@@ -240,7 +240,7 @@ class GroupsLocalWorkerHandler:
|
||||
|
||||
class GroupsLocalHandler(GroupsLocalWorkerHandler):
|
||||
def __init__(self, hs):
|
||||
super(GroupsLocalHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
# Ensure attestations get renewed
|
||||
hs.get_groups_attestation_renewer()
|
||||
|
||||
@@ -21,8 +21,6 @@ import logging
|
||||
import urllib.parse
|
||||
from typing import Awaitable, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from twisted.internet.error import TimeoutError
|
||||
|
||||
from synapse.api.errors import (
|
||||
CodeMessageException,
|
||||
Codes,
|
||||
@@ -30,6 +28,7 @@ from synapse.api.errors import (
|
||||
SynapseError,
|
||||
)
|
||||
from synapse.config.emailconfig import ThreepidBehaviour
|
||||
from synapse.http import RequestTimedOutError
|
||||
from synapse.http.client import SimpleHttpClient
|
||||
from synapse.types import JsonDict, Requester
|
||||
from synapse.util import json_decoder
|
||||
@@ -45,7 +44,7 @@ id_server_scheme = "https://"
|
||||
|
||||
class IdentityHandler(BaseHandler):
|
||||
def __init__(self, hs):
|
||||
super(IdentityHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
self.http_client = SimpleHttpClient(hs)
|
||||
# We create a blacklisting instance of SimpleHttpClient for contacting identity
|
||||
@@ -93,7 +92,7 @@ class IdentityHandler(BaseHandler):
|
||||
|
||||
try:
|
||||
data = await self.http_client.get_json(url, query_params)
|
||||
except TimeoutError:
|
||||
except RequestTimedOutError:
|
||||
raise SynapseError(500, "Timed out contacting identity server")
|
||||
except HttpResponseException as e:
|
||||
logger.info(
|
||||
@@ -173,7 +172,7 @@ class IdentityHandler(BaseHandler):
|
||||
if e.code != 404 or not use_v2:
|
||||
logger.error("3PID bind failed with Matrix error: %r", e)
|
||||
raise e.to_synapse_error()
|
||||
except TimeoutError:
|
||||
except RequestTimedOutError:
|
||||
raise SynapseError(500, "Timed out contacting identity server")
|
||||
except CodeMessageException as e:
|
||||
data = json_decoder.decode(e.msg) # XXX WAT?
|
||||
@@ -273,7 +272,7 @@ class IdentityHandler(BaseHandler):
|
||||
else:
|
||||
logger.error("Failed to unbind threepid on identity server: %s", e)
|
||||
raise SynapseError(500, "Failed to contact identity server")
|
||||
except TimeoutError:
|
||||
except RequestTimedOutError:
|
||||
raise SynapseError(500, "Timed out contacting identity server")
|
||||
|
||||
await self.store.remove_user_bound_threepid(
|
||||
@@ -419,7 +418,7 @@ class IdentityHandler(BaseHandler):
|
||||
except HttpResponseException as e:
|
||||
logger.info("Proxied requestToken failed: %r", e)
|
||||
raise e.to_synapse_error()
|
||||
except TimeoutError:
|
||||
except RequestTimedOutError:
|
||||
raise SynapseError(500, "Timed out contacting identity server")
|
||||
|
||||
async def requestMsisdnToken(
|
||||
@@ -471,7 +470,7 @@ class IdentityHandler(BaseHandler):
|
||||
except HttpResponseException as e:
|
||||
logger.info("Proxied requestToken failed: %r", e)
|
||||
raise e.to_synapse_error()
|
||||
except TimeoutError:
|
||||
except RequestTimedOutError:
|
||||
raise SynapseError(500, "Timed out contacting identity server")
|
||||
|
||||
assert self.hs.config.public_baseurl
|
||||
@@ -553,7 +552,7 @@ class IdentityHandler(BaseHandler):
|
||||
id_server + "/_matrix/identity/api/v1/validate/msisdn/submitToken",
|
||||
body,
|
||||
)
|
||||
except TimeoutError:
|
||||
except RequestTimedOutError:
|
||||
raise SynapseError(500, "Timed out contacting identity server")
|
||||
except HttpResponseException as e:
|
||||
logger.warning("Error contacting msisdn account_threepid_delegate: %s", e)
|
||||
@@ -627,7 +626,7 @@ class IdentityHandler(BaseHandler):
|
||||
# require or validate it. See the following for context:
|
||||
# https://github.com/matrix-org/synapse/issues/5253#issuecomment-666246950
|
||||
return data["mxid"]
|
||||
except TimeoutError:
|
||||
except RequestTimedOutError:
|
||||
raise SynapseError(500, "Timed out contacting identity server")
|
||||
except IOError as e:
|
||||
logger.warning("Error from v1 identity server lookup: %s" % (e,))
|
||||
@@ -655,7 +654,7 @@ class IdentityHandler(BaseHandler):
|
||||
"%s%s/_matrix/identity/v2/hash_details" % (id_server_scheme, id_server),
|
||||
{"access_token": id_access_token},
|
||||
)
|
||||
except TimeoutError:
|
||||
except RequestTimedOutError:
|
||||
raise SynapseError(500, "Timed out contacting identity server")
|
||||
|
||||
if not isinstance(hash_details, dict):
|
||||
@@ -727,7 +726,7 @@ class IdentityHandler(BaseHandler):
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
except TimeoutError:
|
||||
except RequestTimedOutError:
|
||||
raise SynapseError(500, "Timed out contacting identity server")
|
||||
except Exception as e:
|
||||
logger.warning("Error when performing a v2 3pid lookup: %s", e)
|
||||
@@ -823,7 +822,7 @@ class IdentityHandler(BaseHandler):
|
||||
invite_config,
|
||||
{"Authorization": create_id_access_token_header(id_access_token)},
|
||||
)
|
||||
except TimeoutError:
|
||||
except RequestTimedOutError:
|
||||
raise SynapseError(500, "Timed out contacting identity server")
|
||||
except HttpResponseException as e:
|
||||
if e.code != 404:
|
||||
@@ -841,7 +840,7 @@ class IdentityHandler(BaseHandler):
|
||||
data = await self.blacklisting_http_client.post_json_get_json(
|
||||
url, invite_config
|
||||
)
|
||||
except TimeoutError:
|
||||
except RequestTimedOutError:
|
||||
raise SynapseError(500, "Timed out contacting identity server")
|
||||
except HttpResponseException as e:
|
||||
logger.warning(
|
||||
|
||||
@@ -25,7 +25,7 @@ from synapse.handlers.presence import format_user_presence_state
|
||||
from synapse.logging.context import make_deferred_yieldable, run_in_background
|
||||
from synapse.storage.roommember import RoomsForUser
|
||||
from synapse.streams.config import PaginationConfig
|
||||
from synapse.types import JsonDict, Requester, StreamToken, UserID
|
||||
from synapse.types import JsonDict, Requester, RoomStreamToken, StreamToken, UserID
|
||||
from synapse.util import unwrapFirstError
|
||||
from synapse.util.async_helpers import concurrently_execute
|
||||
from synapse.util.caches.response_cache import ResponseCache
|
||||
@@ -42,7 +42,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class InitialSyncHandler(BaseHandler):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super(InitialSyncHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
self.hs = hs
|
||||
self.state = hs.get_state_handler()
|
||||
self.clock = hs.get_clock()
|
||||
@@ -116,14 +116,13 @@ class InitialSyncHandler(BaseHandler):
|
||||
now_token = self.hs.get_event_sources().get_current_token()
|
||||
|
||||
presence_stream = self.hs.get_event_sources().sources["presence"]
|
||||
pagination_config = PaginationConfig(from_token=now_token)
|
||||
presence, _ = await presence_stream.get_pagination_rows(
|
||||
user, pagination_config.get_source_config("presence"), None
|
||||
presence, _ = await presence_stream.get_new_events(
|
||||
user, from_key=None, include_offline=False
|
||||
)
|
||||
|
||||
receipt_stream = self.hs.get_event_sources().sources["receipt"]
|
||||
receipt, _ = await receipt_stream.get_pagination_rows(
|
||||
user, pagination_config.get_source_config("receipt"), None
|
||||
joined_rooms = [r.room_id for r in room_list if r.membership == Membership.JOIN]
|
||||
receipt = await self.store.get_linearized_receipts_for_rooms(
|
||||
joined_rooms, to_key=int(now_token.receipt_key),
|
||||
)
|
||||
|
||||
tags_by_room = await self.store.get_tags_for_user(user_id)
|
||||
@@ -168,7 +167,7 @@ class InitialSyncHandler(BaseHandler):
|
||||
self.state_handler.get_current_state, event.room_id
|
||||
)
|
||||
elif event.membership == Membership.LEAVE:
|
||||
room_end_token = "s%d" % (event.stream_ordering,)
|
||||
room_end_token = RoomStreamToken(None, event.stream_ordering,)
|
||||
deferred_room_state = run_in_background(
|
||||
self.state_store.get_state_for_events, [event.event_id]
|
||||
)
|
||||
@@ -204,8 +203,8 @@ class InitialSyncHandler(BaseHandler):
|
||||
messages, time_now=time_now, as_client_event=as_client_event
|
||||
)
|
||||
),
|
||||
"start": start_token.to_string(),
|
||||
"end": end_token.to_string(),
|
||||
"start": await start_token.to_string(self.store),
|
||||
"end": await end_token.to_string(self.store),
|
||||
}
|
||||
|
||||
d["state"] = await self._event_serializer.serialize_events(
|
||||
@@ -250,7 +249,7 @@ class InitialSyncHandler(BaseHandler):
|
||||
],
|
||||
"account_data": account_data_events,
|
||||
"receipts": receipt,
|
||||
"end": now_token.to_string(),
|
||||
"end": await now_token.to_string(self.store),
|
||||
}
|
||||
|
||||
return ret
|
||||
@@ -326,7 +325,8 @@ class InitialSyncHandler(BaseHandler):
|
||||
if limit is None:
|
||||
limit = 10
|
||||
|
||||
stream_token = await self.store.get_stream_token_for_event(member_event_id)
|
||||
leave_position = await self.store.get_position_for_event(member_event_id)
|
||||
stream_token = leave_position.to_room_stream_token()
|
||||
|
||||
messages, token = await self.store.get_recent_events_for_room(
|
||||
room_id, limit=limit, end_token=stream_token
|
||||
@@ -348,8 +348,8 @@ class InitialSyncHandler(BaseHandler):
|
||||
"chunk": (
|
||||
await self._event_serializer.serialize_events(messages, time_now)
|
||||
),
|
||||
"start": start_token.to_string(),
|
||||
"end": end_token.to_string(),
|
||||
"start": await start_token.to_string(self.store),
|
||||
"end": await end_token.to_string(self.store),
|
||||
},
|
||||
"state": (
|
||||
await self._event_serializer.serialize_events(
|
||||
@@ -447,8 +447,8 @@ class InitialSyncHandler(BaseHandler):
|
||||
"chunk": (
|
||||
await self._event_serializer.serialize_events(messages, time_now)
|
||||
),
|
||||
"start": start_token.to_string(),
|
||||
"end": end_token.to_string(),
|
||||
"start": await start_token.to_string(self.store),
|
||||
"end": await end_token.to_string(self.store),
|
||||
},
|
||||
"state": state,
|
||||
"presence": presence,
|
||||
|
||||
+66
-61
@@ -376,9 +376,8 @@ class EventCreationHandler:
|
||||
self.notifier = hs.get_notifier()
|
||||
self.config = hs.config
|
||||
self.require_membership_for_aliases = hs.config.require_membership_for_aliases
|
||||
self._is_event_writer = (
|
||||
self.config.worker.writers.events == hs.get_instance_name()
|
||||
)
|
||||
self._events_shard_config = self.config.worker.events_shard_config
|
||||
self._instance_name = hs.get_instance_name()
|
||||
|
||||
self.room_invite_state_types = self.hs.config.room_invite_state_types
|
||||
|
||||
@@ -387,8 +386,6 @@ class EventCreationHandler:
|
||||
# This is only used to get at ratelimit function, and maybe_kick_guest_users
|
||||
self.base_handler = BaseHandler(hs)
|
||||
|
||||
self.pusher_pool = hs.get_pusherpool()
|
||||
|
||||
# We arbitrarily limit concurrent event creation for a room to 5.
|
||||
# This is to stop us from diverging history *too* much.
|
||||
self.limiter = Linearizer(max_count=5, name="room_event_creation_limit")
|
||||
@@ -904,9 +901,10 @@ class EventCreationHandler:
|
||||
|
||||
try:
|
||||
# If we're a worker we need to hit out to the master.
|
||||
if not self._is_event_writer:
|
||||
writer_instance = self._events_shard_config.get_instance(event.room_id)
|
||||
if writer_instance != self._instance_name:
|
||||
result = await self.send_event(
|
||||
instance_name=self.config.worker.writers.events,
|
||||
instance_name=writer_instance,
|
||||
event_id=event.event_id,
|
||||
store=self.store,
|
||||
requester=requester,
|
||||
@@ -974,7 +972,10 @@ class EventCreationHandler:
|
||||
|
||||
This should only be run on the instance in charge of persisting events.
|
||||
"""
|
||||
assert self._is_event_writer
|
||||
assert self.storage.persistence is not None
|
||||
assert self._events_shard_config.should_handle(
|
||||
self._instance_name, event.room_id
|
||||
)
|
||||
|
||||
if ratelimit:
|
||||
# We check if this is a room admin redacting an event so that we
|
||||
@@ -1137,7 +1138,7 @@ class EventCreationHandler:
|
||||
if prev_state_ids:
|
||||
raise AuthError(403, "Changing the room create event is forbidden")
|
||||
|
||||
event_stream_id, max_stream_id = await self.storage.persistence.persist_event(
|
||||
event_pos, max_stream_token = await self.storage.persistence.persist_event(
|
||||
event, context=context
|
||||
)
|
||||
|
||||
@@ -1145,12 +1146,10 @@ class EventCreationHandler:
|
||||
# If there's an expiry timestamp on the event, schedule its expiry.
|
||||
self._message_handler.maybe_schedule_expiry(event)
|
||||
|
||||
await self.pusher_pool.on_new_notifications(event_stream_id, max_stream_id)
|
||||
|
||||
def _notify():
|
||||
try:
|
||||
self.notifier.on_new_room_event(
|
||||
event, event_stream_id, max_stream_id, extra_users=extra_users
|
||||
event, event_pos, max_stream_token, extra_users=extra_users
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Error notifying about new room event")
|
||||
@@ -1162,7 +1161,7 @@ class EventCreationHandler:
|
||||
# matters as sometimes presence code can take a while.
|
||||
run_in_background(self._bump_active_time, requester.user)
|
||||
|
||||
return event_stream_id
|
||||
return event_pos.stream
|
||||
|
||||
async def _bump_active_time(self, user: UserID) -> None:
|
||||
try:
|
||||
@@ -1183,54 +1182,7 @@ class EventCreationHandler:
|
||||
)
|
||||
|
||||
for room_id in room_ids:
|
||||
# For each room we need to find a joined member we can use to send
|
||||
# the dummy event with.
|
||||
|
||||
latest_event_ids = await self.store.get_prev_events_for_room(room_id)
|
||||
|
||||
members = await self.state.get_current_users_in_room(
|
||||
room_id, latest_event_ids=latest_event_ids
|
||||
)
|
||||
dummy_event_sent = False
|
||||
for user_id in members:
|
||||
if not self.hs.is_mine_id(user_id):
|
||||
continue
|
||||
requester = create_requester(user_id)
|
||||
try:
|
||||
event, context = await self.create_event(
|
||||
requester,
|
||||
{
|
||||
"type": "org.matrix.dummy_event",
|
||||
"content": {},
|
||||
"room_id": room_id,
|
||||
"sender": user_id,
|
||||
},
|
||||
prev_event_ids=latest_event_ids,
|
||||
)
|
||||
|
||||
event.internal_metadata.proactively_send = False
|
||||
|
||||
# Since this is a dummy-event it is OK if it is sent by a
|
||||
# shadow-banned user.
|
||||
await self.send_nonmember_event(
|
||||
requester,
|
||||
event,
|
||||
context,
|
||||
ratelimit=False,
|
||||
ignore_shadow_ban=True,
|
||||
)
|
||||
dummy_event_sent = True
|
||||
break
|
||||
except ConsentNotGivenError:
|
||||
logger.info(
|
||||
"Failed to send dummy event into room %s for user %s due to "
|
||||
"lack of consent. Will try another user" % (room_id, user_id)
|
||||
)
|
||||
except AuthError:
|
||||
logger.info(
|
||||
"Failed to send dummy event into room %s for user %s due to "
|
||||
"lack of power. Will try another user" % (room_id, user_id)
|
||||
)
|
||||
dummy_event_sent = await self._send_dummy_event_for_room(room_id)
|
||||
|
||||
if not dummy_event_sent:
|
||||
# Did not find a valid user in the room, so remove from future attempts
|
||||
@@ -1243,6 +1195,59 @@ class EventCreationHandler:
|
||||
now = self.clock.time_msec()
|
||||
self._rooms_to_exclude_from_dummy_event_insertion[room_id] = now
|
||||
|
||||
async def _send_dummy_event_for_room(self, room_id: str) -> bool:
|
||||
"""Attempt to send a dummy event for the given room.
|
||||
|
||||
Args:
|
||||
room_id: room to try to send an event from
|
||||
|
||||
Returns:
|
||||
True if a dummy event was successfully sent. False if no user was able
|
||||
to send an event.
|
||||
"""
|
||||
|
||||
# For each room we need to find a joined member we can use to send
|
||||
# the dummy event with.
|
||||
latest_event_ids = await self.store.get_prev_events_for_room(room_id)
|
||||
members = await self.state.get_current_users_in_room(
|
||||
room_id, latest_event_ids=latest_event_ids
|
||||
)
|
||||
for user_id in members:
|
||||
if not self.hs.is_mine_id(user_id):
|
||||
continue
|
||||
requester = create_requester(user_id)
|
||||
try:
|
||||
event, context = await self.create_event(
|
||||
requester,
|
||||
{
|
||||
"type": "org.matrix.dummy_event",
|
||||
"content": {},
|
||||
"room_id": room_id,
|
||||
"sender": user_id,
|
||||
},
|
||||
prev_event_ids=latest_event_ids,
|
||||
)
|
||||
|
||||
event.internal_metadata.proactively_send = False
|
||||
|
||||
# Since this is a dummy-event it is OK if it is sent by a
|
||||
# shadow-banned user.
|
||||
await self.send_nonmember_event(
|
||||
requester, event, context, ratelimit=False, ignore_shadow_ban=True,
|
||||
)
|
||||
return True
|
||||
except ConsentNotGivenError:
|
||||
logger.info(
|
||||
"Failed to send dummy event into room %s for user %s due to "
|
||||
"lack of consent. Will try another user" % (room_id, user_id)
|
||||
)
|
||||
except AuthError:
|
||||
logger.info(
|
||||
"Failed to send dummy event into room %s for user %s due to "
|
||||
"lack of power. Will try another user" % (room_id, user_id)
|
||||
)
|
||||
return False
|
||||
|
||||
def _expire_rooms_to_exclude_from_dummy_event_insertion(self):
|
||||
expire_before = self.clock.time_msec() - _DUMMY_EVENT_ROOM_EXCLUSION_EXPIRY
|
||||
to_expire = set()
|
||||
|
||||
@@ -37,7 +37,7 @@ from synapse.config import ConfigError
|
||||
from synapse.http.server import respond_with_html
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.logging.context import make_deferred_yieldable
|
||||
from synapse.types import UserID, map_username_to_mxid_localpart
|
||||
from synapse.types import JsonDict, UserID, map_username_to_mxid_localpart
|
||||
from synapse.util import json_decoder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -114,6 +114,7 @@ class OidcHandler:
|
||||
hs.config.oidc_user_mapping_provider_config
|
||||
) # type: OidcMappingProvider
|
||||
self._skip_verification = hs.config.oidc_skip_verification # type: bool
|
||||
self._allow_existing_users = hs.config.oidc_allow_existing_users # type: bool
|
||||
|
||||
self._http_client = hs.get_proxied_http_client()
|
||||
self._auth_handler = hs.get_auth_handler()
|
||||
@@ -131,10 +132,10 @@ class OidcHandler:
|
||||
def _render_error(
|
||||
self, request, error: str, error_description: Optional[str] = None
|
||||
) -> None:
|
||||
"""Renders the error template and respond with it.
|
||||
"""Render the error template and respond to the request with it.
|
||||
|
||||
This is used to show errors to the user. The template of this page can
|
||||
be found under ``synapse/res/templates/sso_error.html``.
|
||||
be found under `synapse/res/templates/sso_error.html`.
|
||||
|
||||
Args:
|
||||
request: The incoming request from the browser.
|
||||
@@ -706,6 +707,15 @@ class OidcHandler:
|
||||
self._render_error(request, "mapping_error", str(e))
|
||||
return
|
||||
|
||||
# Mapping providers might not have get_extra_attributes: only call this
|
||||
# method if it exists.
|
||||
extra_attributes = None
|
||||
get_extra_attributes = getattr(
|
||||
self._user_mapping_provider, "get_extra_attributes", None
|
||||
)
|
||||
if get_extra_attributes:
|
||||
extra_attributes = await get_extra_attributes(userinfo, token)
|
||||
|
||||
# and finally complete the login
|
||||
if ui_auth_session_id:
|
||||
await self._auth_handler.complete_sso_ui_auth(
|
||||
@@ -713,7 +723,7 @@ class OidcHandler:
|
||||
)
|
||||
else:
|
||||
await self._auth_handler.complete_sso_login(
|
||||
user_id, request, client_redirect_url
|
||||
user_id, request, client_redirect_url, extra_attributes
|
||||
)
|
||||
|
||||
def _generate_oidc_session_token(
|
||||
@@ -849,7 +859,8 @@ class OidcHandler:
|
||||
If we don't find the user that way, we should register the user,
|
||||
mapping the localpart and the display name from the UserInfo.
|
||||
|
||||
If a user already exists with the mxid we've mapped, raise an exception.
|
||||
If a user already exists with the mxid we've mapped and allow_existing_users
|
||||
is disabled, raise an exception.
|
||||
|
||||
Args:
|
||||
userinfo: an object representing the user
|
||||
@@ -905,21 +916,31 @@ class OidcHandler:
|
||||
|
||||
localpart = map_username_to_mxid_localpart(attributes["localpart"])
|
||||
|
||||
user_id = UserID(localpart, self._hostname)
|
||||
if await self._datastore.get_users_by_id_case_insensitive(user_id.to_string()):
|
||||
# This mxid is taken
|
||||
raise MappingException(
|
||||
"mxid '{}' is already taken".format(user_id.to_string())
|
||||
user_id = UserID(localpart, self._hostname).to_string()
|
||||
users = await self._datastore.get_users_by_id_case_insensitive(user_id)
|
||||
if users:
|
||||
if self._allow_existing_users:
|
||||
if len(users) == 1:
|
||||
registered_user_id = next(iter(users))
|
||||
elif user_id in users:
|
||||
registered_user_id = user_id
|
||||
else:
|
||||
raise MappingException(
|
||||
"Attempted to login as '{}' but it matches more than one user inexactly: {}".format(
|
||||
user_id, list(users.keys())
|
||||
)
|
||||
)
|
||||
else:
|
||||
# This mxid is taken
|
||||
raise MappingException("mxid '{}' is already taken".format(user_id))
|
||||
else:
|
||||
# It's the first time this user is logging in and the mapped mxid was
|
||||
# not taken, register the user
|
||||
registered_user_id = await self._registration_handler.register_user(
|
||||
localpart=localpart,
|
||||
default_display_name=attributes["display_name"],
|
||||
user_agent_ips=(user_agent, ip_address),
|
||||
)
|
||||
|
||||
# It's the first time this user is logging in and the mapped mxid was
|
||||
# not taken, register the user
|
||||
registered_user_id = await self._registration_handler.register_user(
|
||||
localpart=localpart,
|
||||
default_display_name=attributes["display_name"],
|
||||
user_agent_ips=(user_agent, ip_address),
|
||||
)
|
||||
|
||||
await self._datastore.record_user_external_id(
|
||||
self._auth_provider_id, remote_user_id, registered_user_id,
|
||||
)
|
||||
@@ -972,7 +993,7 @@ class OidcMappingProvider(Generic[C]):
|
||||
async def map_user_attributes(
|
||||
self, userinfo: UserInfo, token: Token
|
||||
) -> UserAttribute:
|
||||
"""Map a ``UserInfo`` objects into user attributes.
|
||||
"""Map a `UserInfo` object into user attributes.
|
||||
|
||||
Args:
|
||||
userinfo: An object representing the user given by the OIDC provider
|
||||
@@ -983,6 +1004,18 @@ class OidcMappingProvider(Generic[C]):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_extra_attributes(self, userinfo: UserInfo, token: Token) -> JsonDict:
|
||||
"""Map a `UserInfo` object into additional attributes passed to the client during login.
|
||||
|
||||
Args:
|
||||
userinfo: An object representing the user given by the OIDC provider
|
||||
token: A dict with the tokens returned by the provider
|
||||
|
||||
Returns:
|
||||
A dict containing additional attributes. Must be JSON serializable.
|
||||
"""
|
||||
return {}
|
||||
|
||||
|
||||
# Used to clear out "None" values in templates
|
||||
def jinja_finalize(thing):
|
||||
@@ -997,6 +1030,7 @@ class JinjaOidcMappingConfig:
|
||||
subject_claim = attr.ib() # type: str
|
||||
localpart_template = attr.ib() # type: Template
|
||||
display_name_template = attr.ib() # type: Optional[Template]
|
||||
extra_attributes = attr.ib() # type: Dict[str, Template]
|
||||
|
||||
|
||||
class JinjaOidcMappingProvider(OidcMappingProvider[JinjaOidcMappingConfig]):
|
||||
@@ -1035,10 +1069,28 @@ class JinjaOidcMappingProvider(OidcMappingProvider[JinjaOidcMappingConfig]):
|
||||
% (e,)
|
||||
)
|
||||
|
||||
extra_attributes = {} # type Dict[str, Template]
|
||||
if "extra_attributes" in config:
|
||||
extra_attributes_config = config.get("extra_attributes") or {}
|
||||
if not isinstance(extra_attributes_config, dict):
|
||||
raise ConfigError(
|
||||
"oidc_config.user_mapping_provider.config.extra_attributes must be a dict"
|
||||
)
|
||||
|
||||
for key, value in extra_attributes_config.items():
|
||||
try:
|
||||
extra_attributes[key] = env.from_string(value)
|
||||
except Exception as e:
|
||||
raise ConfigError(
|
||||
"invalid jinja template for oidc_config.user_mapping_provider.config.extra_attributes.%s: %r"
|
||||
% (key, e)
|
||||
)
|
||||
|
||||
return JinjaOidcMappingConfig(
|
||||
subject_claim=subject_claim,
|
||||
localpart_template=localpart_template,
|
||||
display_name_template=display_name_template,
|
||||
extra_attributes=extra_attributes,
|
||||
)
|
||||
|
||||
def get_remote_user_id(self, userinfo: UserInfo) -> str:
|
||||
@@ -1059,3 +1111,13 @@ class JinjaOidcMappingProvider(OidcMappingProvider[JinjaOidcMappingConfig]):
|
||||
display_name = None
|
||||
|
||||
return UserAttribute(localpart=localpart, display_name=display_name)
|
||||
|
||||
async def get_extra_attributes(self, userinfo: UserInfo, token: Token) -> JsonDict:
|
||||
extras = {} # type: Dict[str, str]
|
||||
for key, template in self._config.extra_attributes.items():
|
||||
try:
|
||||
extras[key] = template.render(user=userinfo).strip()
|
||||
except Exception as e:
|
||||
# Log an error and skip this value (don't break login for this).
|
||||
logger.error("Failed to render OIDC extra attribute %s: %s" % (key, e))
|
||||
return extras
|
||||
|
||||
@@ -25,7 +25,7 @@ from synapse.logging.context import run_in_background
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.storage.state import StateFilter
|
||||
from synapse.streams.config import PaginationConfig
|
||||
from synapse.types import Requester, RoomStreamToken
|
||||
from synapse.types import Requester
|
||||
from synapse.util.async_helpers import ReadWriteLock
|
||||
from synapse.util.stringutils import random_string
|
||||
from synapse.visibility import filter_events_for_client
|
||||
@@ -335,20 +335,16 @@ class PaginationHandler:
|
||||
user_id = requester.user.to_string()
|
||||
|
||||
if pagin_config.from_token:
|
||||
room_token = pagin_config.from_token.room_key
|
||||
from_token = pagin_config.from_token
|
||||
else:
|
||||
pagin_config.from_token = (
|
||||
self.hs.get_event_sources().get_current_token_for_pagination()
|
||||
)
|
||||
room_token = pagin_config.from_token.room_key
|
||||
from_token = self.hs.get_event_sources().get_current_token_for_pagination()
|
||||
|
||||
room_token = RoomStreamToken.parse(room_token)
|
||||
if pagin_config.limit is None:
|
||||
# This shouldn't happen as we've set a default limit before this
|
||||
# gets called.
|
||||
raise Exception("limit not set")
|
||||
|
||||
pagin_config.from_token = pagin_config.from_token.copy_and_replace(
|
||||
"room_key", str(room_token)
|
||||
)
|
||||
|
||||
source_config = pagin_config.get_source_config("room")
|
||||
room_token = from_token.room_key
|
||||
|
||||
with await self.pagination_lock.read(room_id):
|
||||
(
|
||||
@@ -358,7 +354,7 @@ class PaginationHandler:
|
||||
room_id, user_id, allow_departed_users=True
|
||||
)
|
||||
|
||||
if source_config.direction == "b":
|
||||
if pagin_config.direction == "b":
|
||||
# if we're going backwards, we might need to backfill. This
|
||||
# requires that we have a topo token.
|
||||
if room_token.topological:
|
||||
@@ -380,23 +376,31 @@ class PaginationHandler:
|
||||
leave_token = await self.store.get_topological_token_for_event(
|
||||
member_event_id
|
||||
)
|
||||
if RoomStreamToken.parse(leave_token).topological < curr_topo:
|
||||
source_config.from_key = str(leave_token)
|
||||
assert leave_token.topological is not None
|
||||
|
||||
if leave_token.topological < curr_topo:
|
||||
from_token = from_token.copy_and_replace(
|
||||
"room_key", leave_token
|
||||
)
|
||||
|
||||
await self.hs.get_handlers().federation_handler.maybe_backfill(
|
||||
room_id, curr_topo, limit=source_config.limit,
|
||||
room_id, curr_topo, limit=pagin_config.limit,
|
||||
)
|
||||
|
||||
to_room_key = None
|
||||
if pagin_config.to_token:
|
||||
to_room_key = pagin_config.to_token.room_key
|
||||
|
||||
events, next_key = await self.store.paginate_room_events(
|
||||
room_id=room_id,
|
||||
from_key=source_config.from_key,
|
||||
to_key=source_config.to_key,
|
||||
direction=source_config.direction,
|
||||
limit=source_config.limit,
|
||||
from_key=from_token.room_key,
|
||||
to_key=to_room_key,
|
||||
direction=pagin_config.direction,
|
||||
limit=pagin_config.limit,
|
||||
event_filter=event_filter,
|
||||
)
|
||||
|
||||
next_token = pagin_config.from_token.copy_and_replace("room_key", next_key)
|
||||
next_token = from_token.copy_and_replace("room_key", next_key)
|
||||
|
||||
if events:
|
||||
if event_filter:
|
||||
@@ -409,8 +413,8 @@ class PaginationHandler:
|
||||
if not events:
|
||||
return {
|
||||
"chunk": [],
|
||||
"start": pagin_config.from_token.to_string(),
|
||||
"end": next_token.to_string(),
|
||||
"start": await from_token.to_string(self.store),
|
||||
"end": await next_token.to_string(self.store),
|
||||
}
|
||||
|
||||
state = None
|
||||
@@ -438,8 +442,8 @@ class PaginationHandler:
|
||||
events, time_now, as_client_event=as_client_event
|
||||
)
|
||||
),
|
||||
"start": pagin_config.from_token.to_string(),
|
||||
"end": next_token.to_string(),
|
||||
"start": await from_token.to_string(self.store),
|
||||
"end": await next_token.to_string(self.store),
|
||||
}
|
||||
|
||||
if state:
|
||||
|
||||
@@ -1108,9 +1108,6 @@ class PresenceEventSource:
|
||||
def get_current_key(self):
|
||||
return self.store.get_current_presence_token()
|
||||
|
||||
async def get_pagination_rows(self, user, pagination_config, key):
|
||||
return await self.get_new_events(user, from_key=None, include_offline=False)
|
||||
|
||||
@cached(num_args=2, cache_context=True)
|
||||
async def _get_interested_in(self, user, explicit_room_id, cache_context):
|
||||
"""Returns the set of users that the given user should see presence
|
||||
|
||||
@@ -44,7 +44,7 @@ class BaseProfileHandler(BaseHandler):
|
||||
"""
|
||||
|
||||
def __init__(self, hs):
|
||||
super(BaseProfileHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
self.federation = hs.get_federation_client()
|
||||
hs.get_federation_registry().register_query_handler(
|
||||
@@ -369,7 +369,7 @@ class MasterProfileHandler(BaseProfileHandler):
|
||||
PROFILE_UPDATE_EVERY_MS = 24 * 60 * 60 * 1000
|
||||
|
||||
def __init__(self, hs):
|
||||
super(MasterProfileHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
assert hs.config.worker_app is None
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class ReadMarkerHandler(BaseHandler):
|
||||
def __init__(self, hs):
|
||||
super(ReadMarkerHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
self.server_name = hs.config.server_name
|
||||
self.store = hs.get_datastore()
|
||||
self.read_marker_linearizer = Linearizer(name="read_marker")
|
||||
|
||||
@@ -23,7 +23,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class ReceiptsHandler(BaseHandler):
|
||||
def __init__(self, hs):
|
||||
super(ReceiptsHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
self.server_name = hs.config.server_name
|
||||
self.store = hs.get_datastore()
|
||||
@@ -142,18 +142,3 @@ class ReceiptEventSource:
|
||||
|
||||
def get_current_key(self, direction="f"):
|
||||
return self.store.get_max_receipt_stream_id()
|
||||
|
||||
async def get_pagination_rows(self, user, config, key):
|
||||
to_key = int(config.from_key)
|
||||
|
||||
if config.to_key:
|
||||
from_key = int(config.to_key)
|
||||
else:
|
||||
from_key = None
|
||||
|
||||
room_ids = await self.store.get_rooms_for_user(user.to_string())
|
||||
events = await self.store.get_linearized_receipts_for_rooms(
|
||||
room_ids, from_key=from_key, to_key=to_key
|
||||
)
|
||||
|
||||
return (events, to_key)
|
||||
|
||||
@@ -42,7 +42,7 @@ class RegistrationHandler(BaseHandler):
|
||||
Args:
|
||||
hs (synapse.server.HomeServer):
|
||||
"""
|
||||
super(RegistrationHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self._auth_handler = hs.get_auth_handler()
|
||||
|
||||
+21
-16
@@ -70,7 +70,7 @@ FIVE_MINUTES_IN_MS = 5 * 60 * 1000
|
||||
|
||||
class RoomCreationHandler(BaseHandler):
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
super(RoomCreationHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
self.spam_checker = hs.get_spam_checker()
|
||||
self.event_creation_handler = hs.get_event_creation_handler()
|
||||
@@ -804,7 +804,9 @@ class RoomCreationHandler(BaseHandler):
|
||||
|
||||
# Always wait for room creation to progate before returning
|
||||
await self._replication.wait_for_stream_position(
|
||||
self.hs.config.worker.writers.events, "events", last_stream_id
|
||||
self.hs.config.worker.events_shard_config.get_instance(room_id),
|
||||
"events",
|
||||
last_stream_id,
|
||||
)
|
||||
|
||||
return result, last_stream_id
|
||||
@@ -1075,11 +1077,13 @@ class RoomContextHandler:
|
||||
# the token, which we replace.
|
||||
token = StreamToken.START
|
||||
|
||||
results["start"] = token.copy_and_replace(
|
||||
results["start"] = await token.copy_and_replace(
|
||||
"room_key", results["start"]
|
||||
).to_string()
|
||||
).to_string(self.store)
|
||||
|
||||
results["end"] = token.copy_and_replace("room_key", results["end"]).to_string()
|
||||
results["end"] = await token.copy_and_replace(
|
||||
"room_key", results["end"]
|
||||
).to_string(self.store)
|
||||
|
||||
return results
|
||||
|
||||
@@ -1091,20 +1095,19 @@ class RoomEventSource:
|
||||
async def get_new_events(
|
||||
self,
|
||||
user: UserID,
|
||||
from_key: str,
|
||||
from_key: RoomStreamToken,
|
||||
limit: int,
|
||||
room_ids: List[str],
|
||||
is_guest: bool,
|
||||
explicit_room_id: Optional[str] = None,
|
||||
) -> Tuple[List[EventBase], str]:
|
||||
) -> Tuple[List[EventBase], RoomStreamToken]:
|
||||
# We just ignore the key for now.
|
||||
|
||||
to_key = self.get_current_key()
|
||||
|
||||
from_token = RoomStreamToken.parse(from_key)
|
||||
if from_token.topological:
|
||||
if from_key.topological:
|
||||
logger.warning("Stream has topological part!!!! %r", from_key)
|
||||
from_key = "s%s" % (from_token.stream,)
|
||||
from_key = RoomStreamToken(None, from_key.stream)
|
||||
|
||||
app_service = self.store.get_app_service_by_user_id(user.to_string())
|
||||
if app_service:
|
||||
@@ -1139,8 +1142,8 @@ class RoomEventSource:
|
||||
|
||||
return (events, end_key)
|
||||
|
||||
def get_current_key(self) -> str:
|
||||
return "s%d" % (self.store.get_room_max_stream_ordering(),)
|
||||
def get_current_key(self) -> RoomStreamToken:
|
||||
return self.store.get_room_max_token()
|
||||
|
||||
def get_current_key_for_room(self, room_id: str) -> Awaitable[str]:
|
||||
return self.store.get_room_events_max_id(room_id)
|
||||
@@ -1260,10 +1263,10 @@ class RoomShutdownHandler:
|
||||
# We now wait for the create room to come back in via replication so
|
||||
# that we can assume that all the joins/invites have propogated before
|
||||
# we try and auto join below.
|
||||
#
|
||||
# TODO: Currently the events stream is written to from master
|
||||
await self._replication.wait_for_stream_position(
|
||||
self.hs.config.worker.writers.events, "events", stream_id
|
||||
self.hs.config.worker.events_shard_config.get_instance(new_room_id),
|
||||
"events",
|
||||
stream_id,
|
||||
)
|
||||
else:
|
||||
new_room_id = None
|
||||
@@ -1293,7 +1296,9 @@ class RoomShutdownHandler:
|
||||
|
||||
# Wait for leave to come in over replication before trying to forget.
|
||||
await self._replication.wait_for_stream_position(
|
||||
self.hs.config.worker.writers.events, "events", stream_id
|
||||
self.hs.config.worker.events_shard_config.get_instance(room_id),
|
||||
"events",
|
||||
stream_id,
|
||||
)
|
||||
|
||||
await self.room_member_handler.forget(target_requester.user, room_id)
|
||||
|
||||
@@ -38,7 +38,7 @@ EMPTY_THIRD_PARTY_ID = ThirdPartyInstanceID(None, None)
|
||||
|
||||
class RoomListHandler(BaseHandler):
|
||||
def __init__(self, hs):
|
||||
super(RoomListHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
self.enable_room_list_search = hs.config.enable_room_list_search
|
||||
self.response_cache = ResponseCache(hs, "room_list")
|
||||
self.remote_response_cache = ResponseCache(
|
||||
|
||||
@@ -40,7 +40,7 @@ from synapse.events.validator import EventValidator
|
||||
from synapse.storage.roommember import RoomsForUser
|
||||
from synapse.types import JsonDict, Requester, RoomAlias, RoomID, StateMap, UserID
|
||||
from synapse.util.async_helpers import Linearizer
|
||||
from synapse.util.distributor import user_joined_room, user_left_room
|
||||
from synapse.util.distributor import user_left_room
|
||||
|
||||
from ._base import BaseHandler
|
||||
|
||||
@@ -51,14 +51,12 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RoomMemberHandler:
|
||||
class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||
# TODO(paul): This handler currently contains a messy conflation of
|
||||
# low-level API that works on UserID objects and so on, and REST-level
|
||||
# API that takes ID strings and returns pagination chunks. These concerns
|
||||
# ought to be separated out a lot better.
|
||||
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self.hs = hs
|
||||
self.store = hs.get_datastore()
|
||||
@@ -82,13 +80,6 @@ class RoomMemberHandler:
|
||||
self._enable_lookup = hs.config.enable_3pid_lookup
|
||||
self.allow_per_room_profiles = self.config.allow_per_room_profiles
|
||||
|
||||
self._event_stream_writer_instance = hs.config.worker.writers.events
|
||||
self._is_on_event_persistence_instance = (
|
||||
self._event_stream_writer_instance == hs.get_instance_name()
|
||||
)
|
||||
if self._is_on_event_persistence_instance:
|
||||
self.persist_event_storage = hs.get_storage().persistence
|
||||
|
||||
self._join_rate_limiter_local = Ratelimiter(
|
||||
clock=self.clock,
|
||||
rate_hz=hs.config.ratelimiting.rc_joins_local.per_second,
|
||||
@@ -148,17 +139,6 @@ class RoomMemberHandler:
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def _user_joined_room(self, target: UserID, room_id: str) -> None:
|
||||
"""Notifies distributor on master process that the user has joined the
|
||||
room.
|
||||
|
||||
Args:
|
||||
target
|
||||
room_id
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
async def _user_left_room(self, target: UserID, room_id: str) -> None:
|
||||
"""Notifies distributor on master process that the user has left the
|
||||
@@ -221,7 +201,6 @@ class RoomMemberHandler:
|
||||
|
||||
prev_member_event_id = prev_state_ids.get((EventTypes.Member, user_id), None)
|
||||
|
||||
newly_joined = False
|
||||
if event.membership == Membership.JOIN:
|
||||
newly_joined = True
|
||||
if prev_member_event_id:
|
||||
@@ -246,12 +225,7 @@ class RoomMemberHandler:
|
||||
requester, event, context, extra_users=[target], ratelimit=ratelimit,
|
||||
)
|
||||
|
||||
if event.membership == Membership.JOIN and newly_joined:
|
||||
# Only fire user_joined_room if the user has actually joined the
|
||||
# room. Don't bother if the user is just changing their profile
|
||||
# info.
|
||||
await self._user_joined_room(target, room_id)
|
||||
elif event.membership == Membership.LEAVE:
|
||||
if event.membership == Membership.LEAVE:
|
||||
if prev_member_event_id:
|
||||
prev_member_event = await self.store.get_event(prev_member_event_id)
|
||||
if prev_member_event.membership == Membership.JOIN:
|
||||
@@ -726,17 +700,7 @@ class RoomMemberHandler:
|
||||
(EventTypes.Member, event.state_key), None
|
||||
)
|
||||
|
||||
if event.membership == Membership.JOIN:
|
||||
# Only fire user_joined_room if the user has actually joined the
|
||||
# room. Don't bother if the user is just changing their profile
|
||||
# info.
|
||||
newly_joined = True
|
||||
if prev_member_event_id:
|
||||
prev_member_event = await self.store.get_event(prev_member_event_id)
|
||||
newly_joined = prev_member_event.membership != Membership.JOIN
|
||||
if newly_joined:
|
||||
await self._user_joined_room(target_user, room_id)
|
||||
elif event.membership == Membership.LEAVE:
|
||||
if event.membership == Membership.LEAVE:
|
||||
if prev_member_event_id:
|
||||
prev_member_event = await self.store.get_event(prev_member_event_id)
|
||||
if prev_member_event.membership == Membership.JOIN:
|
||||
@@ -1002,10 +966,9 @@ class RoomMemberHandler:
|
||||
|
||||
class RoomMemberMasterHandler(RoomMemberHandler):
|
||||
def __init__(self, hs):
|
||||
super(RoomMemberMasterHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
self.distributor = hs.get_distributor()
|
||||
self.distributor.declare("user_joined_room")
|
||||
self.distributor.declare("user_left_room")
|
||||
|
||||
async def _is_remote_room_too_complex(
|
||||
@@ -1085,7 +1048,6 @@ class RoomMemberMasterHandler(RoomMemberHandler):
|
||||
event_id, stream_id = await self.federation_handler.do_invite_join(
|
||||
remote_room_hosts, room_id, user.to_string(), content
|
||||
)
|
||||
await self._user_joined_room(user, room_id)
|
||||
|
||||
# Check the room we just joined wasn't too large, if we didn't fetch the
|
||||
# complexity of it before.
|
||||
@@ -1228,11 +1190,6 @@ class RoomMemberMasterHandler(RoomMemberHandler):
|
||||
)
|
||||
return event.event_id, stream_id
|
||||
|
||||
async def _user_joined_room(self, target: UserID, room_id: str) -> None:
|
||||
"""Implements RoomMemberHandler._user_joined_room
|
||||
"""
|
||||
user_joined_room(self.distributor, target, room_id)
|
||||
|
||||
async def _user_left_room(self, target: UserID, room_id: str) -> None:
|
||||
"""Implements RoomMemberHandler._user_left_room
|
||||
"""
|
||||
|
||||
@@ -30,7 +30,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class RoomMemberWorkerHandler(RoomMemberHandler):
|
||||
def __init__(self, hs):
|
||||
super(RoomMemberWorkerHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
self._remote_join_client = ReplRemoteJoin.make_client(hs)
|
||||
self._remote_reject_client = ReplRejectInvite.make_client(hs)
|
||||
@@ -57,8 +57,6 @@ class RoomMemberWorkerHandler(RoomMemberHandler):
|
||||
content=content,
|
||||
)
|
||||
|
||||
await self._user_joined_room(user, room_id)
|
||||
|
||||
return ret["event_id"], ret["stream_id"]
|
||||
|
||||
async def remote_reject_invite(
|
||||
@@ -81,13 +79,6 @@ class RoomMemberWorkerHandler(RoomMemberHandler):
|
||||
)
|
||||
return ret["event_id"], ret["stream_id"]
|
||||
|
||||
async def _user_joined_room(self, target: UserID, room_id: str) -> None:
|
||||
"""Implements RoomMemberHandler._user_joined_room
|
||||
"""
|
||||
await self._notify_change_client(
|
||||
user_id=target.to_string(), room_id=room_id, change="joined"
|
||||
)
|
||||
|
||||
async def _user_left_room(self, target: UserID, room_id: str) -> None:
|
||||
"""Implements RoomMemberHandler._user_left_room
|
||||
"""
|
||||
|
||||
@@ -21,9 +21,10 @@ import saml2
|
||||
import saml2.response
|
||||
from saml2.client import Saml2Client
|
||||
|
||||
from synapse.api.errors import AuthError, SynapseError
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.config import ConfigError
|
||||
from synapse.config.saml2_config import SamlAttributeRequirement
|
||||
from synapse.http.server import respond_with_html
|
||||
from synapse.http.servlet import parse_string
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.module_api import ModuleApi
|
||||
@@ -41,7 +42,11 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@attr.s
|
||||
class MappingException(Exception):
|
||||
"""Used to catch errors when mapping the SAML2 response to a user."""
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class Saml2SessionData:
|
||||
"""Data we track about SAML2 sessions"""
|
||||
|
||||
@@ -68,6 +73,7 @@ class SamlHandler:
|
||||
hs.config.saml2_grandfathered_mxid_source_attribute
|
||||
)
|
||||
self._saml2_attribute_requirements = hs.config.saml2.attribute_requirements
|
||||
self._error_template = hs.config.sso_error_template
|
||||
|
||||
# plugin to do custom mapping from saml response to mxid
|
||||
self._user_mapping_provider = hs.config.saml2_user_mapping_provider_class(
|
||||
@@ -84,6 +90,25 @@ class SamlHandler:
|
||||
# a lock on the mappings
|
||||
self._mapping_lock = Linearizer(name="saml_mapping", clock=self._clock)
|
||||
|
||||
def _render_error(
|
||||
self, request, error: str, error_description: Optional[str] = None
|
||||
) -> None:
|
||||
"""Render the error template and respond to the request with it.
|
||||
|
||||
This is used to show errors to the user. The template of this page can
|
||||
be found under `synapse/res/templates/sso_error.html`.
|
||||
|
||||
Args:
|
||||
request: The incoming request from the browser.
|
||||
We'll respond with an HTML page describing the error.
|
||||
error: A technical identifier for this error.
|
||||
error_description: A human-readable description of the error.
|
||||
"""
|
||||
html = self._error_template.render(
|
||||
error=error, error_description=error_description
|
||||
)
|
||||
respond_with_html(request, 400, html)
|
||||
|
||||
def handle_redirect_request(
|
||||
self, client_redirect_url: bytes, ui_auth_session_id: Optional[str] = None
|
||||
) -> bytes:
|
||||
@@ -134,49 +159,6 @@ class SamlHandler:
|
||||
# the dict.
|
||||
self.expire_sessions()
|
||||
|
||||
# Pull out the user-agent and IP from the request.
|
||||
user_agent = request.requestHeaders.getRawHeaders(b"User-Agent", default=[b""])[
|
||||
0
|
||||
].decode("ascii", "surrogateescape")
|
||||
ip_address = self.hs.get_ip_from_request(request)
|
||||
|
||||
user_id, current_session = await self._map_saml_response_to_user(
|
||||
resp_bytes, relay_state, user_agent, ip_address
|
||||
)
|
||||
|
||||
# Complete the interactive auth session or the login.
|
||||
if current_session and current_session.ui_auth_session_id:
|
||||
await self._auth_handler.complete_sso_ui_auth(
|
||||
user_id, current_session.ui_auth_session_id, request
|
||||
)
|
||||
|
||||
else:
|
||||
await self._auth_handler.complete_sso_login(user_id, request, relay_state)
|
||||
|
||||
async def _map_saml_response_to_user(
|
||||
self,
|
||||
resp_bytes: str,
|
||||
client_redirect_url: str,
|
||||
user_agent: str,
|
||||
ip_address: str,
|
||||
) -> Tuple[str, Optional[Saml2SessionData]]:
|
||||
"""
|
||||
Given a sample response, retrieve the cached session and user for it.
|
||||
|
||||
Args:
|
||||
resp_bytes: The SAML response.
|
||||
client_redirect_url: The redirect URL passed in by the client.
|
||||
user_agent: The user agent of the client making the request.
|
||||
ip_address: The IP address of the client making the request.
|
||||
|
||||
Returns:
|
||||
Tuple of the user ID and SAML session associated with this response.
|
||||
|
||||
Raises:
|
||||
SynapseError if there was a problem with the response.
|
||||
RedirectException: some mapping providers may raise this if they need
|
||||
to redirect to an interstitial page.
|
||||
"""
|
||||
try:
|
||||
saml2_auth = self._saml_client.parse_authn_request_response(
|
||||
resp_bytes,
|
||||
@@ -189,12 +171,23 @@ class SamlHandler:
|
||||
# in the (user-visible) exception message, so let's log the exception here
|
||||
# so we can track down the session IDs later.
|
||||
logger.warning(str(e))
|
||||
raise SynapseError(400, "Unexpected SAML2 login.")
|
||||
self._render_error(
|
||||
request, "unsolicited_response", "Unexpected SAML2 login."
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
raise SynapseError(400, "Unable to parse SAML2 response: %s." % (e,))
|
||||
self._render_error(
|
||||
request,
|
||||
"invalid_response",
|
||||
"Unable to parse SAML2 response: %s." % (e,),
|
||||
)
|
||||
return
|
||||
|
||||
if saml2_auth.not_signed:
|
||||
raise SynapseError(400, "SAML2 response was not signed.")
|
||||
self._render_error(
|
||||
request, "unsigned_respond", "SAML2 response was not signed."
|
||||
)
|
||||
return
|
||||
|
||||
logger.debug("SAML2 response: %s", saml2_auth.origxml)
|
||||
for assertion in saml2_auth.assertions:
|
||||
@@ -213,15 +206,73 @@ class SamlHandler:
|
||||
saml2_auth.in_response_to, None
|
||||
)
|
||||
|
||||
# Ensure that the attributes of the logged in user meet the required
|
||||
# attributes.
|
||||
for requirement in self._saml2_attribute_requirements:
|
||||
_check_attribute_requirement(saml2_auth.ava, requirement)
|
||||
if not _check_attribute_requirement(saml2_auth.ava, requirement):
|
||||
self._render_error(
|
||||
request, "unauthorised", "You are not authorised to log in here."
|
||||
)
|
||||
return
|
||||
|
||||
# Pull out the user-agent and IP from the request.
|
||||
user_agent = request.requestHeaders.getRawHeaders(b"User-Agent", default=[b""])[
|
||||
0
|
||||
].decode("ascii", "surrogateescape")
|
||||
ip_address = self.hs.get_ip_from_request(request)
|
||||
|
||||
# Call the mapper to register/login the user
|
||||
try:
|
||||
user_id = await self._map_saml_response_to_user(
|
||||
saml2_auth, relay_state, user_agent, ip_address
|
||||
)
|
||||
except MappingException as e:
|
||||
logger.exception("Could not map user")
|
||||
self._render_error(request, "mapping_error", str(e))
|
||||
return
|
||||
|
||||
# Complete the interactive auth session or the login.
|
||||
if current_session and current_session.ui_auth_session_id:
|
||||
await self._auth_handler.complete_sso_ui_auth(
|
||||
user_id, current_session.ui_auth_session_id, request
|
||||
)
|
||||
|
||||
else:
|
||||
await self._auth_handler.complete_sso_login(user_id, request, relay_state)
|
||||
|
||||
async def _map_saml_response_to_user(
|
||||
self,
|
||||
saml2_auth: saml2.response.AuthnResponse,
|
||||
client_redirect_url: str,
|
||||
user_agent: str,
|
||||
ip_address: str,
|
||||
) -> str:
|
||||
"""
|
||||
Given a SAML response, retrieve the user ID for it and possibly register the user.
|
||||
|
||||
Args:
|
||||
saml2_auth: The parsed SAML2 response.
|
||||
client_redirect_url: The redirect URL passed in by the client.
|
||||
user_agent: The user agent of the client making the request.
|
||||
ip_address: The IP address of the client making the request.
|
||||
|
||||
Returns:
|
||||
The user ID associated with this response.
|
||||
|
||||
Raises:
|
||||
MappingException if there was a problem mapping the response to a user.
|
||||
RedirectException: some mapping providers may raise this if they need
|
||||
to redirect to an interstitial page.
|
||||
"""
|
||||
|
||||
remote_user_id = self._user_mapping_provider.get_remote_user_id(
|
||||
saml2_auth, client_redirect_url
|
||||
)
|
||||
|
||||
if not remote_user_id:
|
||||
raise Exception("Failed to extract remote user id from SAML response")
|
||||
raise MappingException(
|
||||
"Failed to extract remote user id from SAML response"
|
||||
)
|
||||
|
||||
with (await self._mapping_lock.queue(self._auth_provider_id)):
|
||||
# first of all, check if we already have a mapping for this user
|
||||
@@ -235,7 +286,7 @@ class SamlHandler:
|
||||
)
|
||||
if registered_user_id is not None:
|
||||
logger.info("Found existing mapping %s", registered_user_id)
|
||||
return registered_user_id, current_session
|
||||
return registered_user_id
|
||||
|
||||
# backwards-compatibility hack: see if there is an existing user with a
|
||||
# suitable mapping from the uid
|
||||
@@ -260,7 +311,7 @@ class SamlHandler:
|
||||
await self._datastore.record_user_external_id(
|
||||
self._auth_provider_id, remote_user_id, registered_user_id
|
||||
)
|
||||
return registered_user_id, current_session
|
||||
return registered_user_id
|
||||
|
||||
# Map saml response to user attributes using the configured mapping provider
|
||||
for i in range(1000):
|
||||
@@ -277,7 +328,7 @@ class SamlHandler:
|
||||
|
||||
localpart = attribute_dict.get("mxid_localpart")
|
||||
if not localpart:
|
||||
raise Exception(
|
||||
raise MappingException(
|
||||
"Error parsing SAML2 response: SAML mapping provider plugin "
|
||||
"did not return a mxid_localpart value"
|
||||
)
|
||||
@@ -294,8 +345,8 @@ class SamlHandler:
|
||||
else:
|
||||
# Unable to generate a username in 1000 iterations
|
||||
# Break and return error to the user
|
||||
raise SynapseError(
|
||||
500, "Unable to generate a Matrix ID from the SAML response"
|
||||
raise MappingException(
|
||||
"Unable to generate a Matrix ID from the SAML response"
|
||||
)
|
||||
|
||||
logger.info("Mapped SAML user to local part %s", localpart)
|
||||
@@ -310,7 +361,7 @@ class SamlHandler:
|
||||
await self._datastore.record_user_external_id(
|
||||
self._auth_provider_id, remote_user_id, registered_user_id
|
||||
)
|
||||
return registered_user_id, current_session
|
||||
return registered_user_id
|
||||
|
||||
def expire_sessions(self):
|
||||
expire_before = self._clock.time_msec() - self._saml2_session_lifetime
|
||||
@@ -323,11 +374,11 @@ class SamlHandler:
|
||||
del self._outstanding_requests_dict[reqid]
|
||||
|
||||
|
||||
def _check_attribute_requirement(ava: dict, req: SamlAttributeRequirement):
|
||||
def _check_attribute_requirement(ava: dict, req: SamlAttributeRequirement) -> bool:
|
||||
values = ava.get(req.attribute, [])
|
||||
for v in values:
|
||||
if v == req.value:
|
||||
return
|
||||
return True
|
||||
|
||||
logger.info(
|
||||
"SAML2 attribute %s did not match required value '%s' (was '%s')",
|
||||
@@ -335,7 +386,7 @@ def _check_attribute_requirement(ava: dict, req: SamlAttributeRequirement):
|
||||
req.value,
|
||||
values,
|
||||
)
|
||||
raise AuthError(403, "You are not authorized to log in here.")
|
||||
return False
|
||||
|
||||
|
||||
DOT_REPLACE_PATTERN = re.compile(
|
||||
@@ -390,7 +441,7 @@ class DefaultSamlMappingProvider:
|
||||
return saml_response.ava["uid"][0]
|
||||
except KeyError:
|
||||
logger.warning("SAML2 response lacks a 'uid' attestation")
|
||||
raise SynapseError(400, "'uid' not in SAML2 response")
|
||||
raise MappingException("'uid' not in SAML2 response")
|
||||
|
||||
def saml_response_to_user_attributes(
|
||||
self,
|
||||
|
||||
@@ -32,7 +32,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class SearchHandler(BaseHandler):
|
||||
def __init__(self, hs):
|
||||
super(SearchHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
self._event_serializer = hs.get_event_client_serializer()
|
||||
self.storage = hs.get_storage()
|
||||
self.state_store = self.storage.state
|
||||
@@ -362,13 +362,13 @@ class SearchHandler(BaseHandler):
|
||||
self.storage, user.to_string(), res["events_after"]
|
||||
)
|
||||
|
||||
res["start"] = now_token.copy_and_replace(
|
||||
res["start"] = await now_token.copy_and_replace(
|
||||
"room_key", res["start"]
|
||||
).to_string()
|
||||
).to_string(self.store)
|
||||
|
||||
res["end"] = now_token.copy_and_replace(
|
||||
res["end"] = await now_token.copy_and_replace(
|
||||
"room_key", res["end"]
|
||||
).to_string()
|
||||
).to_string(self.store)
|
||||
|
||||
if include_profile:
|
||||
senders = {
|
||||
|
||||
@@ -27,7 +27,7 @@ class SetPasswordHandler(BaseHandler):
|
||||
"""Handler which deals with changing user account passwords"""
|
||||
|
||||
def __init__(self, hs):
|
||||
super(SetPasswordHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
self._auth_handler = hs.get_auth_handler()
|
||||
self._device_handler = hs.get_device_handler()
|
||||
self._password_policy_handler = hs.get_password_policy_handler()
|
||||
|
||||
+35
-41
@@ -89,14 +89,12 @@ class TimelineBatch:
|
||||
events = attr.ib(type=List[EventBase])
|
||||
limited = attr.ib(bool)
|
||||
|
||||
def __nonzero__(self) -> bool:
|
||||
def __bool__(self) -> bool:
|
||||
"""Make the result appear empty if there are no updates. This is used
|
||||
to tell if room needs to be part of the sync result.
|
||||
"""
|
||||
return bool(self.events)
|
||||
|
||||
__bool__ = __nonzero__ # python3
|
||||
|
||||
|
||||
# We can't freeze this class, because we need to update it after it's instantiated to
|
||||
# update its unread count. This is because we calculate the unread count for a room only
|
||||
@@ -114,7 +112,7 @@ class JoinedSyncResult:
|
||||
summary = attr.ib(type=Optional[JsonDict])
|
||||
unread_count = attr.ib(type=int)
|
||||
|
||||
def __nonzero__(self) -> bool:
|
||||
def __bool__(self) -> bool:
|
||||
"""Make the result appear empty if there are no updates. This is used
|
||||
to tell if room needs to be part of the sync result.
|
||||
"""
|
||||
@@ -127,8 +125,6 @@ class JoinedSyncResult:
|
||||
# else in the result, we don't need to send it.
|
||||
)
|
||||
|
||||
__bool__ = __nonzero__ # python3
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True)
|
||||
class ArchivedSyncResult:
|
||||
@@ -137,26 +133,22 @@ class ArchivedSyncResult:
|
||||
state = attr.ib(type=StateMap[EventBase])
|
||||
account_data = attr.ib(type=List[JsonDict])
|
||||
|
||||
def __nonzero__(self) -> bool:
|
||||
def __bool__(self) -> bool:
|
||||
"""Make the result appear empty if there are no updates. This is used
|
||||
to tell if room needs to be part of the sync result.
|
||||
"""
|
||||
return bool(self.timeline or self.state or self.account_data)
|
||||
|
||||
__bool__ = __nonzero__ # python3
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True)
|
||||
class InvitedSyncResult:
|
||||
room_id = attr.ib(type=str)
|
||||
invite = attr.ib(type=EventBase)
|
||||
|
||||
def __nonzero__(self) -> bool:
|
||||
def __bool__(self) -> bool:
|
||||
"""Invited rooms should always be reported to the client"""
|
||||
return True
|
||||
|
||||
__bool__ = __nonzero__ # python3
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True)
|
||||
class GroupsSyncResult:
|
||||
@@ -164,11 +156,9 @@ class GroupsSyncResult:
|
||||
invite = attr.ib(type=JsonDict)
|
||||
leave = attr.ib(type=JsonDict)
|
||||
|
||||
def __nonzero__(self) -> bool:
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.join or self.invite or self.leave)
|
||||
|
||||
__bool__ = __nonzero__ # python3
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True)
|
||||
class DeviceLists:
|
||||
@@ -181,13 +171,11 @@ class DeviceLists:
|
||||
changed = attr.ib(type=Collection[str])
|
||||
left = attr.ib(type=Collection[str])
|
||||
|
||||
def __nonzero__(self) -> bool:
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.changed or self.left)
|
||||
|
||||
__bool__ = __nonzero__ # python3
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(slots=True)
|
||||
class _RoomChanges:
|
||||
"""The set of room entries to include in the sync, plus the set of joined
|
||||
and left room IDs since last sync.
|
||||
@@ -227,7 +215,7 @@ class SyncResult:
|
||||
device_one_time_keys_count = attr.ib(type=JsonDict)
|
||||
groups = attr.ib(type=Optional[GroupsSyncResult])
|
||||
|
||||
def __nonzero__(self) -> bool:
|
||||
def __bool__(self) -> bool:
|
||||
"""Make the result appear empty if there are no updates. This is used
|
||||
to tell if the notifier needs to wait for more events when polling for
|
||||
events.
|
||||
@@ -243,8 +231,6 @@ class SyncResult:
|
||||
or self.groups
|
||||
)
|
||||
|
||||
__bool__ = __nonzero__ # python3
|
||||
|
||||
|
||||
class SyncHandler:
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
@@ -378,7 +364,7 @@ class SyncHandler:
|
||||
sync_config = sync_result_builder.sync_config
|
||||
|
||||
with Measure(self.clock, "ephemeral_by_room"):
|
||||
typing_key = since_token.typing_key if since_token else "0"
|
||||
typing_key = since_token.typing_key if since_token else 0
|
||||
|
||||
room_ids = sync_result_builder.joined_room_ids
|
||||
|
||||
@@ -402,7 +388,7 @@ class SyncHandler:
|
||||
event_copy = {k: v for (k, v) in event.items() if k != "room_id"}
|
||||
ephemeral_by_room.setdefault(room_id, []).append(event_copy)
|
||||
|
||||
receipt_key = since_token.receipt_key if since_token else "0"
|
||||
receipt_key = since_token.receipt_key if since_token else 0
|
||||
|
||||
receipt_source = self.event_sources.sources["receipt"]
|
||||
receipts, receipt_key = await receipt_source.get_new_events(
|
||||
@@ -981,7 +967,7 @@ class SyncHandler:
|
||||
raise NotImplementedError()
|
||||
else:
|
||||
joined_room_ids = await self.get_rooms_for_user_at(
|
||||
user_id, now_token.room_stream_id
|
||||
user_id, now_token.room_key
|
||||
)
|
||||
sync_result_builder = SyncResultBuilder(
|
||||
sync_config,
|
||||
@@ -1310,12 +1296,11 @@ class SyncHandler:
|
||||
presence_source = self.event_sources.sources["presence"]
|
||||
|
||||
since_token = sync_result_builder.since_token
|
||||
presence_key = None
|
||||
include_offline = False
|
||||
if since_token and not sync_result_builder.full_state:
|
||||
presence_key = since_token.presence_key
|
||||
include_offline = True
|
||||
else:
|
||||
presence_key = None
|
||||
include_offline = False
|
||||
|
||||
presence, presence_key = await presence_source.get_new_events(
|
||||
user=user,
|
||||
@@ -1323,6 +1308,7 @@ class SyncHandler:
|
||||
is_guest=sync_config.is_guest,
|
||||
include_offline=include_offline,
|
||||
)
|
||||
assert presence_key
|
||||
sync_result_builder.now_token = now_token.copy_and_replace(
|
||||
"presence_key", presence_key
|
||||
)
|
||||
@@ -1485,7 +1471,7 @@ class SyncHandler:
|
||||
if rooms_changed:
|
||||
return True
|
||||
|
||||
stream_id = RoomStreamToken.parse_stream_token(since_token.room_key).stream
|
||||
stream_id = since_token.room_key.stream
|
||||
for room_id in sync_result_builder.joined_room_ids:
|
||||
if self.store.has_room_changed_since(room_id, stream_id):
|
||||
return True
|
||||
@@ -1609,16 +1595,24 @@ class SyncHandler:
|
||||
|
||||
if leave_events:
|
||||
leave_event = leave_events[-1]
|
||||
leave_stream_token = await self.store.get_stream_token_for_event(
|
||||
leave_position = await self.store.get_position_for_event(
|
||||
leave_event.event_id
|
||||
)
|
||||
leave_token = since_token.copy_and_replace(
|
||||
"room_key", leave_stream_token
|
||||
)
|
||||
|
||||
if since_token and since_token.is_after(leave_token):
|
||||
# If the leave event happened before the since token then we
|
||||
# bail.
|
||||
if since_token and not leave_position.persisted_after(
|
||||
since_token.room_key
|
||||
):
|
||||
continue
|
||||
|
||||
# We can safely convert the position of the leave event into a
|
||||
# stream token as it'll only be used in the context of this
|
||||
# room. (c.f. the docstring of `to_room_stream_token`).
|
||||
leave_token = since_token.copy_and_replace(
|
||||
"room_key", leave_position.to_room_stream_token()
|
||||
)
|
||||
|
||||
# If this is an out of band message, like a remote invite
|
||||
# rejection, we include it in the recents batch. Otherwise, we
|
||||
# let _load_filtered_recents handle fetching the correct
|
||||
@@ -1751,7 +1745,7 @@ class SyncHandler:
|
||||
continue
|
||||
|
||||
leave_token = now_token.copy_and_replace(
|
||||
"room_key", "s%d" % (event.stream_ordering,)
|
||||
"room_key", RoomStreamToken(None, event.stream_ordering)
|
||||
)
|
||||
room_entries.append(
|
||||
RoomSyncResultBuilder(
|
||||
@@ -1930,7 +1924,7 @@ class SyncHandler:
|
||||
raise Exception("Unrecognized rtype: %r", room_builder.rtype)
|
||||
|
||||
async def get_rooms_for_user_at(
|
||||
self, user_id: str, stream_ordering: int
|
||||
self, user_id: str, room_key: RoomStreamToken
|
||||
) -> FrozenSet[str]:
|
||||
"""Get set of joined rooms for a user at the given stream ordering.
|
||||
|
||||
@@ -1956,15 +1950,15 @@ class SyncHandler:
|
||||
# If the membership's stream ordering is after the given stream
|
||||
# ordering, we need to go and work out if the user was in the room
|
||||
# before.
|
||||
for room_id, membership_stream_ordering in joined_rooms:
|
||||
if membership_stream_ordering <= stream_ordering:
|
||||
for room_id, event_pos in joined_rooms:
|
||||
if not event_pos.persisted_after(room_key):
|
||||
joined_room_ids.add(room_id)
|
||||
continue
|
||||
|
||||
logger.info("User joined room after current token: %s", room_id)
|
||||
|
||||
extrems = await self.store.get_forward_extremeties_for_room(
|
||||
room_id, stream_ordering
|
||||
room_id, event_pos.stream
|
||||
)
|
||||
users_in_room = await self.state.get_current_users_in_room(room_id, extrems)
|
||||
if user_id in users_in_room:
|
||||
@@ -2038,7 +2032,7 @@ def _calculate_state(
|
||||
return {event_id_to_key[e]: e for e in state_ids}
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(slots=True)
|
||||
class SyncResultBuilder:
|
||||
"""Used to help build up a new SyncResult for a user
|
||||
|
||||
@@ -2074,7 +2068,7 @@ class SyncResultBuilder:
|
||||
to_device = attr.ib(type=List[JsonDict], default=attr.Factory(list))
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(slots=True)
|
||||
class RoomSyncResultBuilder:
|
||||
"""Stores information needed to create either a `JoinedSyncResult` or
|
||||
`ArchivedSyncResult`.
|
||||
|
||||
@@ -37,7 +37,7 @@ class UserDirectoryHandler(StateDeltasHandler):
|
||||
"""
|
||||
|
||||
def __init__(self, hs):
|
||||
super(UserDirectoryHandler, self).__init__(hs)
|
||||
super().__init__(hs)
|
||||
|
||||
self.store = hs.get_datastore()
|
||||
self.state = hs.get_state_handler()
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
import re
|
||||
|
||||
from twisted.internet import task
|
||||
from twisted.internet.defer import CancelledError
|
||||
from twisted.python import failure
|
||||
from twisted.web.client import FileBodyProducer
|
||||
|
||||
from synapse.api.errors import SynapseError
|
||||
@@ -26,19 +24,8 @@ from synapse.api.errors import SynapseError
|
||||
class RequestTimedOutError(SynapseError):
|
||||
"""Exception representing timeout of an outbound request"""
|
||||
|
||||
def __init__(self):
|
||||
super(RequestTimedOutError, self).__init__(504, "Timed out")
|
||||
|
||||
|
||||
def cancelled_to_request_timed_out_error(value, timeout):
|
||||
"""Turns CancelledErrors into RequestTimedOutErrors.
|
||||
|
||||
For use with async.add_timeout_to_deferred
|
||||
"""
|
||||
if isinstance(value, failure.Failure):
|
||||
value.trap(CancelledError)
|
||||
raise RequestTimedOutError()
|
||||
return value
|
||||
def __init__(self, msg):
|
||||
super().__init__(504, msg)
|
||||
|
||||
|
||||
ACCESS_TOKEN_RE = re.compile(r"(\?.*access(_|%5[Ff])token=)[^&]*(.*)$")
|
||||
|
||||
+158
-71
@@ -13,10 +13,21 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import urllib
|
||||
from io import BytesIO
|
||||
from typing import (
|
||||
Any,
|
||||
BinaryIO,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
import treq
|
||||
from canonicaljson import encode_canonical_json
|
||||
@@ -26,7 +37,7 @@ from zope.interface import implementer, provider
|
||||
|
||||
from OpenSSL import SSL
|
||||
from OpenSSL.SSL import VERIFY_NONE
|
||||
from twisted.internet import defer, protocol, ssl
|
||||
from twisted.internet import defer, error as twisted_error, protocol, ssl
|
||||
from twisted.internet.interfaces import (
|
||||
IReactorPluggableNameResolver,
|
||||
IResolutionReceiver,
|
||||
@@ -34,16 +45,18 @@ from twisted.internet.interfaces import (
|
||||
from twisted.internet.task import Cooperator
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.web._newclient import ResponseDone
|
||||
from twisted.web.client import Agent, HTTPConnectionPool, readBody
|
||||
from twisted.web.client import (
|
||||
Agent,
|
||||
HTTPConnectionPool,
|
||||
ResponseNeverReceived,
|
||||
readBody,
|
||||
)
|
||||
from twisted.web.http import PotentialDataLoss
|
||||
from twisted.web.http_headers import Headers
|
||||
from twisted.web.iweb import IResponse
|
||||
|
||||
from synapse.api.errors import Codes, HttpResponseException, SynapseError
|
||||
from synapse.http import (
|
||||
QuieterFileBodyProducer,
|
||||
cancelled_to_request_timed_out_error,
|
||||
redact_uri,
|
||||
)
|
||||
from synapse.http import QuieterFileBodyProducer, RequestTimedOutError, redact_uri
|
||||
from synapse.http.proxyagent import ProxyAgent
|
||||
from synapse.logging.context import make_deferred_yieldable
|
||||
from synapse.logging.opentracing import set_tag, start_active_span, tags
|
||||
@@ -57,6 +70,19 @@ incoming_responses_counter = Counter(
|
||||
"synapse_http_client_responses", "", ["method", "code"]
|
||||
)
|
||||
|
||||
# the type of the headers list, to be passed to the t.w.h.Headers.
|
||||
# Actually we can mix str and bytes keys, but Mapping treats 'key' as invariant so
|
||||
# we simplify.
|
||||
RawHeaders = Union[Mapping[str, "RawHeaderValue"], Mapping[bytes, "RawHeaderValue"]]
|
||||
|
||||
# the value actually has to be a List, but List is invariant so we can't specify that
|
||||
# the entries can either be Lists or bytes.
|
||||
RawHeaderValue = Sequence[Union[str, bytes]]
|
||||
|
||||
# the type of the query params, to be passed into `urlencode`
|
||||
QueryParamValue = Union[str, bytes, Iterable[Union[str, bytes]]]
|
||||
QueryParams = Union[Mapping[str, QueryParamValue], Mapping[bytes, QueryParamValue]]
|
||||
|
||||
|
||||
def check_against_blacklist(ip_address, ip_whitelist, ip_blacklist):
|
||||
"""
|
||||
@@ -285,16 +311,27 @@ class SimpleHttpClient:
|
||||
ip_blacklist=self._ip_blacklist,
|
||||
)
|
||||
|
||||
async def request(self, method, uri, data=None, headers=None):
|
||||
async def request(
|
||||
self,
|
||||
method: str,
|
||||
uri: str,
|
||||
data: Optional[bytes] = None,
|
||||
headers: Optional[Headers] = None,
|
||||
) -> IResponse:
|
||||
"""
|
||||
Args:
|
||||
method (str): HTTP method to use.
|
||||
uri (str): URI to query.
|
||||
data (bytes): Data to send in the request body, if applicable.
|
||||
headers (t.w.http_headers.Headers): Request headers.
|
||||
method: HTTP method to use.
|
||||
uri: URI to query.
|
||||
data: Data to send in the request body, if applicable.
|
||||
headers: Request headers.
|
||||
|
||||
Returns:
|
||||
Response object, once the headers have been read.
|
||||
|
||||
Raises:
|
||||
RequestTimedOutError if the request times out before the headers are read
|
||||
|
||||
"""
|
||||
# A small wrapper around self.agent.request() so we can easily attach
|
||||
# counters to it
|
||||
outgoing_requests_counter.labels(method).inc()
|
||||
|
||||
# log request but strip `access_token` (AS requests for example include this)
|
||||
@@ -323,13 +360,17 @@ class SimpleHttpClient:
|
||||
data=body_producer,
|
||||
headers=headers,
|
||||
**self._extra_treq_args
|
||||
)
|
||||
) # type: defer.Deferred
|
||||
|
||||
# we use our own timeout mechanism rather than treq's as a workaround
|
||||
# for https://twistedmatrix.com/trac/ticket/9534.
|
||||
request_deferred = timeout_deferred(
|
||||
request_deferred,
|
||||
60,
|
||||
self.hs.get_reactor(),
|
||||
cancelled_to_request_timed_out_error,
|
||||
request_deferred, 60, self.hs.get_reactor(),
|
||||
)
|
||||
|
||||
# turn timeouts into RequestTimedOutErrors
|
||||
request_deferred.addErrback(_timeout_to_request_timed_out_error)
|
||||
|
||||
response = await make_deferred_yieldable(request_deferred)
|
||||
|
||||
incoming_responses_counter.labels(method, response.code).inc()
|
||||
@@ -353,18 +394,26 @@ class SimpleHttpClient:
|
||||
set_tag("error_reason", e.args[0])
|
||||
raise
|
||||
|
||||
async def post_urlencoded_get_json(self, uri, args={}, headers=None):
|
||||
async def post_urlencoded_get_json(
|
||||
self,
|
||||
uri: str,
|
||||
args: Mapping[str, Union[str, List[str]]] = {},
|
||||
headers: Optional[RawHeaders] = None,
|
||||
) -> Any:
|
||||
"""
|
||||
Args:
|
||||
uri (str):
|
||||
args (dict[str, str|List[str]]): query params
|
||||
headers (dict[str|bytes, List[str|bytes]]|None): If not None, a map from
|
||||
header name to a list of values for that header
|
||||
uri: uri to query
|
||||
args: parameters to be url-encoded in the body
|
||||
headers: a map from header name to a list of values for that header
|
||||
|
||||
Returns:
|
||||
object: parsed json
|
||||
parsed json
|
||||
|
||||
Raises:
|
||||
RequestTimedOutError: if there is a timeout before the response headers
|
||||
are received. Note there is currently no timeout on reading the response
|
||||
body.
|
||||
|
||||
HttpResponseException: On a non-2xx HTTP response.
|
||||
|
||||
ValueError: if the response was not JSON
|
||||
@@ -398,19 +447,24 @@ class SimpleHttpClient:
|
||||
response.code, response.phrase.decode("ascii", errors="replace"), body
|
||||
)
|
||||
|
||||
async def post_json_get_json(self, uri, post_json, headers=None):
|
||||
async def post_json_get_json(
|
||||
self, uri: str, post_json: Any, headers: Optional[RawHeaders] = None
|
||||
) -> Any:
|
||||
"""
|
||||
|
||||
Args:
|
||||
uri (str):
|
||||
post_json (object):
|
||||
headers (dict[str|bytes, List[str|bytes]]|None): If not None, a map from
|
||||
header name to a list of values for that header
|
||||
uri: URI to query.
|
||||
post_json: request body, to be encoded as json
|
||||
headers: a map from header name to a list of values for that header
|
||||
|
||||
Returns:
|
||||
object: parsed json
|
||||
parsed json
|
||||
|
||||
Raises:
|
||||
RequestTimedOutError: if there is a timeout before the response headers
|
||||
are received. Note there is currently no timeout on reading the response
|
||||
body.
|
||||
|
||||
HttpResponseException: On a non-2xx HTTP response.
|
||||
|
||||
ValueError: if the response was not JSON
|
||||
@@ -440,21 +494,22 @@ class SimpleHttpClient:
|
||||
response.code, response.phrase.decode("ascii", errors="replace"), body
|
||||
)
|
||||
|
||||
async def get_json(self, uri, args={}, headers=None):
|
||||
""" Gets some json from the given URI.
|
||||
async def get_json(
|
||||
self, uri: str, args: QueryParams = {}, headers: Optional[RawHeaders] = None,
|
||||
) -> Any:
|
||||
"""Gets some json from the given URI.
|
||||
|
||||
Args:
|
||||
uri (str): The URI to request, not including query parameters
|
||||
args (dict): A dictionary used to create query strings, defaults to
|
||||
None.
|
||||
**Note**: The value of each key is assumed to be an iterable
|
||||
and *not* a string.
|
||||
headers (dict[str|bytes, List[str|bytes]]|None): If not None, a map from
|
||||
header name to a list of values for that header
|
||||
uri: The URI to request, not including query parameters
|
||||
args: A dictionary used to create query string
|
||||
headers: a map from header name to a list of values for that header
|
||||
Returns:
|
||||
Succeeds when we get *any* 2xx HTTP response, with the
|
||||
HTTP body as JSON.
|
||||
Succeeds when we get a 2xx HTTP response, with the HTTP body as JSON.
|
||||
Raises:
|
||||
RequestTimedOutError: if there is a timeout before the response headers
|
||||
are received. Note there is currently no timeout on reading the response
|
||||
body.
|
||||
|
||||
HttpResponseException On a non-2xx HTTP response.
|
||||
|
||||
ValueError: if the response was not JSON
|
||||
@@ -466,22 +521,27 @@ class SimpleHttpClient:
|
||||
body = await self.get_raw(uri, args, headers=headers)
|
||||
return json_decoder.decode(body.decode("utf-8"))
|
||||
|
||||
async def put_json(self, uri, json_body, args={}, headers=None):
|
||||
""" Puts some json to the given URI.
|
||||
async def put_json(
|
||||
self,
|
||||
uri: str,
|
||||
json_body: Any,
|
||||
args: QueryParams = {},
|
||||
headers: RawHeaders = None,
|
||||
) -> Any:
|
||||
"""Puts some json to the given URI.
|
||||
|
||||
Args:
|
||||
uri (str): The URI to request, not including query parameters
|
||||
json_body (dict): The JSON to put in the HTTP body,
|
||||
args (dict): A dictionary used to create query strings, defaults to
|
||||
None.
|
||||
**Note**: The value of each key is assumed to be an iterable
|
||||
and *not* a string.
|
||||
headers (dict[str|bytes, List[str|bytes]]|None): If not None, a map from
|
||||
header name to a list of values for that header
|
||||
uri: The URI to request, not including query parameters
|
||||
json_body: The JSON to put in the HTTP body,
|
||||
args: A dictionary used to create query strings
|
||||
headers: a map from header name to a list of values for that header
|
||||
Returns:
|
||||
Succeeds when we get *any* 2xx HTTP response, with the
|
||||
HTTP body as JSON.
|
||||
Succeeds when we get a 2xx HTTP response, with the HTTP body as JSON.
|
||||
Raises:
|
||||
RequestTimedOutError: if there is a timeout before the response headers
|
||||
are received. Note there is currently no timeout on reading the response
|
||||
body.
|
||||
|
||||
HttpResponseException On a non-2xx HTTP response.
|
||||
|
||||
ValueError: if the response was not JSON
|
||||
@@ -513,21 +573,23 @@ class SimpleHttpClient:
|
||||
response.code, response.phrase.decode("ascii", errors="replace"), body
|
||||
)
|
||||
|
||||
async def get_raw(self, uri, args={}, headers=None):
|
||||
""" Gets raw text from the given URI.
|
||||
async def get_raw(
|
||||
self, uri: str, args: QueryParams = {}, headers: Optional[RawHeaders] = None
|
||||
) -> bytes:
|
||||
"""Gets raw text from the given URI.
|
||||
|
||||
Args:
|
||||
uri (str): The URI to request, not including query parameters
|
||||
args (dict): A dictionary used to create query strings, defaults to
|
||||
None.
|
||||
**Note**: The value of each key is assumed to be an iterable
|
||||
and *not* a string.
|
||||
headers (dict[str|bytes, List[str|bytes]]|None): If not None, a map from
|
||||
header name to a list of values for that header
|
||||
uri: The URI to request, not including query parameters
|
||||
args: A dictionary used to create query strings
|
||||
headers: a map from header name to a list of values for that header
|
||||
Returns:
|
||||
Succeeds when we get *any* 2xx HTTP response, with the
|
||||
Succeeds when we get a 2xx HTTP response, with the
|
||||
HTTP body as bytes.
|
||||
Raises:
|
||||
RequestTimedOutError: if there is a timeout before the response headers
|
||||
are received. Note there is currently no timeout on reading the response
|
||||
body.
|
||||
|
||||
HttpResponseException on a non-2xx HTTP response.
|
||||
"""
|
||||
if len(args):
|
||||
@@ -552,16 +614,29 @@ class SimpleHttpClient:
|
||||
# XXX: FIXME: This is horribly copy-pasted from matrixfederationclient.
|
||||
# The two should be factored out.
|
||||
|
||||
async def get_file(self, url, output_stream, max_size=None, headers=None):
|
||||
async def get_file(
|
||||
self,
|
||||
url: str,
|
||||
output_stream: BinaryIO,
|
||||
max_size: Optional[int] = None,
|
||||
headers: Optional[RawHeaders] = None,
|
||||
) -> Tuple[int, Dict[bytes, List[bytes]], str, int]:
|
||||
"""GETs a file from a given URL
|
||||
Args:
|
||||
url (str): The URL to GET
|
||||
output_stream (file): File to write the response body to.
|
||||
headers (dict[str|bytes, List[str|bytes]]|None): If not None, a map from
|
||||
header name to a list of values for that header
|
||||
url: The URL to GET
|
||||
output_stream: File to write the response body to.
|
||||
headers: A map from header name to a list of values for that header
|
||||
Returns:
|
||||
A (int,dict,string,int) tuple of the file length, dict of the response
|
||||
A tuple of the file length, dict of the response
|
||||
headers, absolute URI of the response and HTTP response code.
|
||||
|
||||
Raises:
|
||||
RequestTimedOutError: if there is a timeout before the response headers
|
||||
are received. Note there is currently no timeout on reading the response
|
||||
body.
|
||||
|
||||
SynapseError: if the response is not a 2xx, the remote file is too large, or
|
||||
another exception happens during the download.
|
||||
"""
|
||||
|
||||
actual_headers = {b"User-Agent": [self.user_agent]}
|
||||
@@ -609,6 +684,18 @@ class SimpleHttpClient:
|
||||
)
|
||||
|
||||
|
||||
def _timeout_to_request_timed_out_error(f: Failure):
|
||||
if f.check(twisted_error.TimeoutError, twisted_error.ConnectingCancelledError):
|
||||
# The TCP connection has its own timeout (set by the 'connectTimeout' param
|
||||
# on the Agent), which raises twisted_error.TimeoutError exception.
|
||||
raise RequestTimedOutError("Timeout connecting to remote server")
|
||||
elif f.check(defer.TimeoutError, ResponseNeverReceived):
|
||||
# this one means that we hit our overall timeout on the request
|
||||
raise RequestTimedOutError("Timeout waiting for response from remote server")
|
||||
|
||||
return f
|
||||
|
||||
|
||||
# XXX: FIXME: This is horribly copy-pasted from matrixfederationclient.
|
||||
# The two should be factored out.
|
||||
|
||||
|
||||
@@ -311,7 +311,7 @@ def _parse_cache_control(headers: Headers) -> Dict[bytes, Optional[bytes]]:
|
||||
return cache_controls
|
||||
|
||||
|
||||
@attr.s()
|
||||
@attr.s(slots=True)
|
||||
class _FetchWellKnownFailure(Exception):
|
||||
# True if we didn't get a non-5xx HTTP response, i.e. this may or may not be
|
||||
# a temporary failure.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user