mirror of
https://github.com/element-hq/synapse.git
synced 2025-12-13 01:50:46 +00:00
Compare commits
3 Commits
v0.19.1
...
erikj/pypy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c11bb956c | ||
|
|
58de897c77 | ||
|
|
06602461e7 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -24,10 +24,10 @@ homeserver*.yaml
|
|||||||
.coverage
|
.coverage
|
||||||
htmlcov
|
htmlcov
|
||||||
|
|
||||||
demo/*/*.db
|
demo/*.db
|
||||||
demo/*/*.log
|
demo/*.log
|
||||||
demo/*/*.log.*
|
demo/*.log.*
|
||||||
demo/*/*.pid
|
demo/*.pid
|
||||||
demo/media_store.*
|
demo/media_store.*
|
||||||
demo/etc
|
demo/etc
|
||||||
|
|
||||||
|
|||||||
17
.travis.yml
17
.travis.yml
@@ -1,17 +0,0 @@
|
|||||||
sudo: false
|
|
||||||
language: python
|
|
||||||
python: 2.7
|
|
||||||
|
|
||||||
# tell travis to cache ~/.cache/pip
|
|
||||||
cache: pip
|
|
||||||
|
|
||||||
env:
|
|
||||||
- TOX_ENV=packaging
|
|
||||||
- TOX_ENV=pep8
|
|
||||||
- TOX_ENV=py27
|
|
||||||
|
|
||||||
install:
|
|
||||||
- pip install tox
|
|
||||||
|
|
||||||
script:
|
|
||||||
- tox -e $TOX_ENV
|
|
||||||
809
CHANGES.rst
809
CHANGES.rst
@@ -1,804 +1,3 @@
|
|||||||
Changes in synapse v0.19.1 (2017-02-09)
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
* Fix bug where state was incorrectly reset in a room when synapse received an
|
|
||||||
event over federation that did not pass auth checks (PR #1892)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.19.0 (2017-02-04)
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
No changes since RC 4.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.19.0-rc4 (2017-02-02)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
* Bump cache sizes for common membership queries (PR #1879)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.19.0-rc3 (2017-02-02)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
* Fix email push in pusher worker (PR #1875)
|
|
||||||
* Make presence.get_new_events a bit faster (PR #1876)
|
|
||||||
* Make /keys/changes a bit more performant (PR #1877)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.19.0-rc2 (2017-02-02)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
* Include newly joined users in /keys/changes API (PR #1872)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.19.0-rc1 (2017-02-02)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Features:
|
|
||||||
|
|
||||||
* Add support for specifying multiple bind addresses (PR #1709, #1712, #1795,
|
|
||||||
#1835). Thanks to @kyrias!
|
|
||||||
* Add /account/3pid/delete endpoint (PR #1714)
|
|
||||||
* Add config option to configure the Riot URL used in notification emails (PR
|
|
||||||
#1811). Thanks to @aperezdc!
|
|
||||||
* Add username and password config options for turn server (PR #1832). Thanks
|
|
||||||
to @xsteadfastx!
|
|
||||||
* Implement device lists updates over federation (PR #1857, #1861, #1864)
|
|
||||||
* Implement /keys/changes (PR #1869, #1872)
|
|
||||||
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Improve IPv6 support (PR #1696). Thanks to @kyrias and @glyph!
|
|
||||||
* Log which files we saved attachments to in the media_repository (PR #1791)
|
|
||||||
* Linearize updates to membership via PUT /state/ to better handle multiple
|
|
||||||
joins (PR #1787)
|
|
||||||
* Limit number of entries to prefill from cache on startup (PR #1792)
|
|
||||||
* Remove full_twisted_stacktraces option (PR #1802)
|
|
||||||
* Measure size of some caches by sum of the size of cached values (PR #1815)
|
|
||||||
* Measure metrics of string_cache (PR #1821)
|
|
||||||
* Reduce logging verbosity (PR #1822, #1823, #1824)
|
|
||||||
* Don't clobber a displayname or avatar_url if provided by an m.room.member
|
|
||||||
event (PR #1852)
|
|
||||||
* Better handle 401/404 response for federation /send/ (PR #1866, #1871)
|
|
||||||
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
|
|
||||||
* Fix ability to change password to a non-ascii one (PR #1711)
|
|
||||||
* Fix push getting stuck due to looking at the wrong view of state (PR #1820)
|
|
||||||
* Fix email address comparison to be case insensitive (PR #1827)
|
|
||||||
* Fix occasional inconsistencies of room membership (PR #1836, #1840)
|
|
||||||
|
|
||||||
|
|
||||||
Performance:
|
|
||||||
|
|
||||||
* Don't block messages sending on bumping presence (PR #1789)
|
|
||||||
* Change device_inbox stream index to include user (PR #1793)
|
|
||||||
* Optimise state resolution (PR #1818)
|
|
||||||
* Use DB cache of joined users for presence (PR #1862)
|
|
||||||
* Add an index to make membership queries faster (PR #1867)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.7 (2017-01-09)
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
No changes from v0.18.7-rc2
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.7-rc2 (2017-01-07)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix error in rc1's discarding invalid inbound traffic logic that was
|
|
||||||
incorrectly discarding missing events
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.7-rc1 (2017-01-06)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix error in #PR 1764 to actually fix the nightmare #1753 bug.
|
|
||||||
* Improve deadlock logging further
|
|
||||||
* Discard inbound federation traffic from invalid domains, to immunise
|
|
||||||
against #1753
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.6 (2017-01-06)
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix bug when checking if a guest user is allowed to join a room (PR #1772)
|
|
||||||
Thanks to Patrik Oldsberg for diagnosing and the fix!
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.6-rc3 (2017-01-05)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix bug where we failed to send ban events to the banned server (PR #1758)
|
|
||||||
* Fix bug where we sent event that didn't originate on this server to
|
|
||||||
other servers (PR #1764)
|
|
||||||
* Fix bug where processing an event from a remote server took a long time
|
|
||||||
because we were making long HTTP requests (PR #1765, PR #1744)
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Improve logging for debugging deadlocks (PR #1766, PR #1767)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.6-rc2 (2016-12-30)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix memory leak in twisted by initialising logging correctly (PR #1731)
|
|
||||||
* Fix bug where fetching missing events took an unacceptable amount of time in
|
|
||||||
large rooms (PR #1734)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.6-rc1 (2016-12-29)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Make sure that outbound connections are closed (PR #1725)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.5 (2016-12-16)
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix federation /backfill returning events it shouldn't (PR #1700)
|
|
||||||
* Fix crash in url preview (PR #1701)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.5-rc3 (2016-12-13)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Features:
|
|
||||||
|
|
||||||
* Add support for E2E for guests (PR #1653)
|
|
||||||
* Add new API appservice specific public room list (PR #1676)
|
|
||||||
* Add new room membership APIs (PR #1680)
|
|
||||||
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Enable guest access for private rooms by default (PR #653)
|
|
||||||
* Limit the number of events that can be created on a given room concurrently
|
|
||||||
(PR #1620)
|
|
||||||
* Log the args that we have on UI auth completion (PR #1649)
|
|
||||||
* Stop generating refresh_tokens (PR #1654)
|
|
||||||
* Stop putting a time caveat on access tokens (PR #1656)
|
|
||||||
* Remove unspecced GET endpoints for e2e keys (PR #1694)
|
|
||||||
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix handling of 500 and 429's over federation (PR #1650)
|
|
||||||
* Fix Content-Type header parsing (PR #1660)
|
|
||||||
* Fix error when previewing sites that include unicode, thanks to kyrias (PR
|
|
||||||
#1664)
|
|
||||||
* Fix some cases where we drop read receipts (PR #1678)
|
|
||||||
* Fix bug where calls to ``/sync`` didn't correctly timeout (PR #1683)
|
|
||||||
* Fix bug where E2E key query would fail if a single remote host failed (PR
|
|
||||||
#1686)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.5-rc2 (2016-11-24)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Don't send old events over federation, fixes bug in -rc1.
|
|
||||||
|
|
||||||
Changes in synapse v0.18.5-rc1 (2016-11-24)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Features:
|
|
||||||
|
|
||||||
* Implement "event_fields" in filters (PR #1638)
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Use external ldap auth pacakge (PR #1628)
|
|
||||||
* Split out federation transaction sending to a worker (PR #1635)
|
|
||||||
* Fail with a coherent error message if `/sync?filter=` is invalid (PR #1636)
|
|
||||||
* More efficient notif count queries (PR #1644)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.4 (2016-11-22)
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Add workaround for buggy clients that the fail to register (PR #1632)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.4-rc1 (2016-11-14)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Various database efficiency improvements (PR #1188, #1192)
|
|
||||||
* Update default config to blacklist more internal IPs, thanks to Euan Kemp (PR
|
|
||||||
#1198)
|
|
||||||
* Allow specifying duration in minutes in config, thanks to Daniel Dent (PR
|
|
||||||
#1625)
|
|
||||||
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix media repo to set CORs headers on responses (PR #1190)
|
|
||||||
* Fix registration to not error on non-ascii passwords (PR #1191)
|
|
||||||
* Fix create event code to limit the number of prev_events (PR #1615)
|
|
||||||
* Fix bug in transaction ID deduplication (PR #1624)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.3 (2016-11-08)
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
SECURITY UPDATE
|
|
||||||
|
|
||||||
Explicitly require authentication when using LDAP3. This is the default on
|
|
||||||
versions of ``ldap3`` above 1.0, but some distributions will package an older
|
|
||||||
version.
|
|
||||||
|
|
||||||
If you are using LDAP3 login and have a version of ``ldap3`` older than 1.0 it
|
|
||||||
is **CRITICAL to updgrade**.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.2 (2016-11-01)
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
No changes since v0.18.2-rc5
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.2-rc5 (2016-10-28)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix prometheus process metrics in worker processes (PR #1184)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.2-rc4 (2016-10-27)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix ``user_threepids`` schema delta, which in some instances prevented
|
|
||||||
startup after upgrade (PR #1183)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.2-rc3 (2016-10-27)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Allow clients to supply access tokens as headers (PR #1098)
|
|
||||||
* Clarify error codes for GET /filter/, thanks to Alexander Maznev (PR #1164)
|
|
||||||
* Make password reset email field case insensitive (PR #1170)
|
|
||||||
* Reduce redundant database work in email pusher (PR #1174)
|
|
||||||
* Allow configurable rate limiting per AS (PR #1175)
|
|
||||||
* Check whether to ratelimit sooner to avoid work (PR #1176)
|
|
||||||
* Standardise prometheus metrics (PR #1177)
|
|
||||||
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix incredibly slow back pagination query (PR #1178)
|
|
||||||
* Fix infinite typing bug (PR #1179)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.2-rc2 (2016-10-25)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
(This release did not include the changes advertised and was identical to RC1)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.2-rc1 (2016-10-17)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Remove redundant event_auth index (PR #1113)
|
|
||||||
* Reduce DB hits for replication (PR #1141)
|
|
||||||
* Implement pluggable password auth (PR #1155)
|
|
||||||
* Remove rate limiting from app service senders and fix get_or_create_user
|
|
||||||
requester, thanks to Patrik Oldsberg (PR #1157)
|
|
||||||
* window.postmessage for Interactive Auth fallback (PR #1159)
|
|
||||||
* Use sys.executable instead of hardcoded python, thanks to Pedro Larroy
|
|
||||||
(PR #1162)
|
|
||||||
* Add config option for adding additional TLS fingerprints (PR #1167)
|
|
||||||
* User-interactive auth on delete device (PR #1168)
|
|
||||||
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix not being allowed to set your own state_key, thanks to Patrik Oldsberg
|
|
||||||
(PR #1150)
|
|
||||||
* Fix interactive auth to return 401 from for incorrect password (PR #1160,
|
|
||||||
#1166)
|
|
||||||
* Fix email push notifs being dropped (PR #1169)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.1 (2016-10-05)
|
|
||||||
======================================
|
|
||||||
|
|
||||||
No changes since v0.18.1-rc1
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.1-rc1 (2016-09-30)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Features:
|
|
||||||
|
|
||||||
* Add total_room_count_estimate to ``/publicRooms`` (PR #1133)
|
|
||||||
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Time out typing over federation (PR #1140)
|
|
||||||
* Restructure LDAP authentication (PR #1153)
|
|
||||||
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix 3pid invites when server is already in the room (PR #1136)
|
|
||||||
* Fix upgrading with SQLite taking lots of CPU for a few days
|
|
||||||
after upgrade (PR #1144)
|
|
||||||
* Fix upgrading from very old database versions (PR #1145)
|
|
||||||
* Fix port script to work with recently added tables (PR #1146)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.0 (2016-09-19)
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
The release includes major changes to the state storage database schemas, which
|
|
||||||
significantly reduce database size. Synapse will attempt to upgrade the current
|
|
||||||
data in the background. Servers with large SQLite database may experience
|
|
||||||
degradation of performance while this upgrade is in progress, therefore you may
|
|
||||||
want to consider migrating to using Postgres before upgrading very large SQLite
|
|
||||||
databases
|
|
||||||
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Make public room search case insensitive (PR #1127)
|
|
||||||
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix and clean up publicRooms pagination (PR #1129)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.18.0-rc1 (2016-09-16)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Features:
|
|
||||||
|
|
||||||
* Add ``only=highlight`` on ``/notifications`` (PR #1081)
|
|
||||||
* Add server param to /publicRooms (PR #1082)
|
|
||||||
* Allow clients to ask for the whole of a single state event (PR #1094)
|
|
||||||
* Add is_direct param to /createRoom (PR #1108)
|
|
||||||
* Add pagination support to publicRooms (PR #1121)
|
|
||||||
* Add very basic filter API to /publicRooms (PR #1126)
|
|
||||||
* Add basic direct to device messaging support for E2E (PR #1074, #1084, #1104,
|
|
||||||
#1111)
|
|
||||||
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Move to storing state_groups_state as deltas, greatly reducing DB size (PR
|
|
||||||
#1065)
|
|
||||||
* Reduce amount of state pulled out of the DB during common requests (PR #1069)
|
|
||||||
* Allow PDF to be rendered from media repo (PR #1071)
|
|
||||||
* Reindex state_groups_state after pruning (PR #1085)
|
|
||||||
* Clobber EDUs in send queue (PR #1095)
|
|
||||||
* Conform better to the CAS protocol specification (PR #1100)
|
|
||||||
* Limit how often we ask for keys from dead servers (PR #1114)
|
|
||||||
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix /notifications API when used with ``from`` param (PR #1080)
|
|
||||||
* Fix backfill when cannot find an event. (PR #1107)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.17.3 (2016-09-09)
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
This release fixes a major bug that stopped servers from handling rooms with
|
|
||||||
over 1000 members.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.17.2 (2016-09-08)
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
This release contains security bug fixes. Please upgrade.
|
|
||||||
|
|
||||||
|
|
||||||
No changes since v0.17.2-rc1
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.17.2-rc1 (2016-09-05)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Features:
|
|
||||||
|
|
||||||
* Start adding store-and-forward direct-to-device messaging (PR #1046, #1050,
|
|
||||||
#1062, #1066)
|
|
||||||
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Avoid pulling the full state of a room out so often (PR #1047, #1049, #1063,
|
|
||||||
#1068)
|
|
||||||
* Don't notify for online to online presence transitions. (PR #1054)
|
|
||||||
* Occasionally persist unpersisted presence updates (PR #1055)
|
|
||||||
* Allow application services to have an optional 'url' (PR #1056)
|
|
||||||
* Clean up old sent transactions from DB (PR #1059)
|
|
||||||
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix None check in backfill (PR #1043)
|
|
||||||
* Fix membership changes to be idempotent (PR #1067)
|
|
||||||
* Fix bug in get_pdu where it would sometimes return events with incorrect
|
|
||||||
signature
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.17.1 (2016-08-24)
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Delete old received_transactions rows (PR #1038)
|
|
||||||
* Pass through user-supplied content in /join/$room_id (PR #1039)
|
|
||||||
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix bug with backfill (PR #1040)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.17.1-rc1 (2016-08-22)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Features:
|
|
||||||
|
|
||||||
* Add notification API (PR #1028)
|
|
||||||
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Don't print stack traces when failing to get remote keys (PR #996)
|
|
||||||
* Various federation /event/ perf improvements (PR #998)
|
|
||||||
* Only process one local membership event per room at a time (PR #1005)
|
|
||||||
* Move default display name push rule (PR #1011, #1023)
|
|
||||||
* Fix up preview URL API. Add tests. (PR #1015)
|
|
||||||
* Set ``Content-Security-Policy`` on media repo (PR #1021)
|
|
||||||
* Make notify_interested_services faster (PR #1022)
|
|
||||||
* Add usage stats to prometheus monitoring (PR #1037)
|
|
||||||
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix token login (PR #993)
|
|
||||||
* Fix CAS login (PR #994, #995)
|
|
||||||
* Fix /sync to not clobber status_msg (PR #997)
|
|
||||||
* Fix redacted state events to include prev_content (PR #1003)
|
|
||||||
* Fix some bugs in the auth/ldap handler (PR #1007)
|
|
||||||
* Fix backfill request to limit URI length, so that remotes don't reject the
|
|
||||||
requests due to path length limits (PR #1012)
|
|
||||||
* Fix AS push code to not send duplicate events (PR #1025)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.17.0 (2016-08-08)
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
This release contains significant security bug fixes regarding authenticating
|
|
||||||
events received over federation. PLEASE UPGRADE.
|
|
||||||
|
|
||||||
This release changes the LDAP configuration format in a backwards incompatible
|
|
||||||
way, see PR #843 for details.
|
|
||||||
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Add federation /version API (PR #990)
|
|
||||||
* Make psutil dependency optional (PR #992)
|
|
||||||
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix URL preview API to exclude HTML comments in description (PR #988)
|
|
||||||
* Fix error handling of remote joins (PR #991)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.17.0-rc4 (2016-08-05)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Change the way we summarize URLs when previewing (PR #973)
|
|
||||||
* Add new ``/state_ids/`` federation API (PR #979)
|
|
||||||
* Speed up processing of ``/state/`` response (PR #986)
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix event persistence when event has already been partially persisted
|
|
||||||
(PR #975, #983, #985)
|
|
||||||
* Fix port script to also copy across backfilled events (PR #982)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.17.0-rc3 (2016-08-02)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Forbid non-ASes from registering users whose names begin with '_' (PR #958)
|
|
||||||
* Add some basic admin API docs (PR #963)
|
|
||||||
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Send the correct host header when fetching keys (PR #941)
|
|
||||||
* Fix joining a room that has missing auth events (PR #964)
|
|
||||||
* Fix various push bugs (PR #966, #970)
|
|
||||||
* Fix adding emails on registration (PR #968)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.17.0-rc2 (2016-08-02)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
(This release did not include the changes advertised and was identical to RC1)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.17.0-rc1 (2016-07-28)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
This release changes the LDAP configuration format in a backwards incompatible
|
|
||||||
way, see PR #843 for details.
|
|
||||||
|
|
||||||
|
|
||||||
Features:
|
|
||||||
|
|
||||||
* Add purge_media_cache admin API (PR #902)
|
|
||||||
* Add deactivate account admin API (PR #903)
|
|
||||||
* Add optional pepper to password hashing (PR #907, #910 by KentShikama)
|
|
||||||
* Add an admin option to shared secret registration (breaks backwards compat)
|
|
||||||
(PR #909)
|
|
||||||
* Add purge local room history API (PR #911, #923, #924)
|
|
||||||
* Add requestToken endpoints (PR #915)
|
|
||||||
* Add an /account/deactivate endpoint (PR #921)
|
|
||||||
* Add filter param to /messages. Add 'contains_url' to filter. (PR #922)
|
|
||||||
* Add device_id support to /login (PR #929)
|
|
||||||
* Add device_id support to /v2/register flow. (PR #937, #942)
|
|
||||||
* Add GET /devices endpoint (PR #939, #944)
|
|
||||||
* Add GET /device/{deviceId} (PR #943)
|
|
||||||
* Add update and delete APIs for devices (PR #949)
|
|
||||||
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Rewrite LDAP Authentication against ldap3 (PR #843 by mweinelt)
|
|
||||||
* Linearize some federation endpoints based on (origin, room_id) (PR #879)
|
|
||||||
* Remove the legacy v0 content upload API. (PR #888)
|
|
||||||
* Use similar naming we use in email notifs for push (PR #894)
|
|
||||||
* Optionally include password hash in createUser endpoint (PR #905 by
|
|
||||||
KentShikama)
|
|
||||||
* Use a query that postgresql optimises better for get_events_around (PR #906)
|
|
||||||
* Fall back to 'username' if 'user' is not given for appservice registration.
|
|
||||||
(PR #927 by Half-Shot)
|
|
||||||
* Add metrics for psutil derived memory usage (PR #936)
|
|
||||||
* Record device_id in client_ips (PR #938)
|
|
||||||
* Send the correct host header when fetching keys (PR #941)
|
|
||||||
* Log the hostname the reCAPTCHA was completed on (PR #946)
|
|
||||||
* Make the device id on e2e key upload optional (PR #956)
|
|
||||||
* Add r0.2.0 to the "supported versions" list (PR #960)
|
|
||||||
* Don't include name of room for invites in push (PR #961)
|
|
||||||
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix substitution failure in mail template (PR #887)
|
|
||||||
* Put most recent 20 messages in email notif (PR #892)
|
|
||||||
* Ensure that the guest user is in the database when upgrading accounts
|
|
||||||
(PR #914)
|
|
||||||
* Fix various edge cases in auth handling (PR #919)
|
|
||||||
* Fix 500 ISE when sending alias event without a state_key (PR #925)
|
|
||||||
* Fix bug where we stored rejections in the state_group, persist all
|
|
||||||
rejections (PR #948)
|
|
||||||
* Fix lack of check of if the user is banned when handling 3pid invites
|
|
||||||
(PR #952)
|
|
||||||
* Fix a couple of bugs in the transaction and keyring code (PR #954, #955)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.16.1-r1 (2016-07-08)
|
|
||||||
==========================================
|
|
||||||
|
|
||||||
THIS IS A CRITICAL SECURITY UPDATE.
|
|
||||||
|
|
||||||
This fixes a bug which allowed users' accounts to be accessed by unauthorised
|
|
||||||
users.
|
|
||||||
|
|
||||||
Changes in synapse v0.16.1 (2016-06-20)
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix assorted bugs in ``/preview_url`` (PR #872)
|
|
||||||
* Fix TypeError when setting unicode passwords (PR #873)
|
|
||||||
|
|
||||||
|
|
||||||
Performance improvements:
|
|
||||||
|
|
||||||
* Turn ``use_frozen_events`` off by default (PR #877)
|
|
||||||
* Disable responding with canonical json for federation (PR #878)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.16.1-rc1 (2016-06-15)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Features: None
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Log requester for ``/publicRoom`` endpoints when possible (PR #856)
|
|
||||||
* 502 on ``/thumbnail`` when can't connect to remote server (PR #862)
|
|
||||||
* Linearize fetching of gaps on incoming events (PR #871)
|
|
||||||
|
|
||||||
|
|
||||||
Bugs fixes:
|
|
||||||
|
|
||||||
* Fix bug where rooms where marked as published by default (PR #857)
|
|
||||||
* Fix bug where joining room with an event with invalid sender (PR #868)
|
|
||||||
* Fix bug where backfilled events were sent down sync streams (PR #869)
|
|
||||||
* Fix bug where outgoing connections could wedge indefinitely, causing push
|
|
||||||
notifications to be unreliable (PR #870)
|
|
||||||
|
|
||||||
|
|
||||||
Performance improvements:
|
|
||||||
|
|
||||||
* Improve ``/publicRooms`` performance(PR #859)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.16.0 (2016-06-09)
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
NB: As of v0.14 all AS config files must have an ID field.
|
|
||||||
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Don't make rooms published by default (PR #857)
|
|
||||||
|
|
||||||
Changes in synapse v0.16.0-rc2 (2016-06-08)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Features:
|
|
||||||
|
|
||||||
* Add configuration option for tuning GC via ``gc.set_threshold`` (PR #849)
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Record metrics about GC (PR #771, #847, #852)
|
|
||||||
* Add metric counter for number of persisted events (PR #841)
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix 'From' header in email notifications (PR #843)
|
|
||||||
* Fix presence where timeouts were not being fired for the first 8h after
|
|
||||||
restarts (PR #842)
|
|
||||||
* Fix bug where synapse sent malformed transactions to AS's when retrying
|
|
||||||
transactions (Commits 310197b, 8437906)
|
|
||||||
|
|
||||||
Performance improvements:
|
|
||||||
|
|
||||||
* Remove event fetching from DB threads (PR #835)
|
|
||||||
* Change the way we cache events (PR #836)
|
|
||||||
* Add events to cache when we persist them (PR #840)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.16.0-rc1 (2016-06-03)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Version 0.15 was not released. See v0.15.0-rc1 below for additional changes.
|
|
||||||
|
|
||||||
Features:
|
|
||||||
|
|
||||||
* Add email notifications for missed messages (PR #759, #786, #799, #810, #815,
|
|
||||||
#821)
|
|
||||||
* Add a ``url_preview_ip_range_whitelist`` config param (PR #760)
|
|
||||||
* Add /report endpoint (PR #762)
|
|
||||||
* Add basic ignore user API (PR #763)
|
|
||||||
* Add an openidish mechanism for proving that you own a given user_id (PR #765)
|
|
||||||
* Allow clients to specify a server_name to avoid 'No known servers' (PR #794)
|
|
||||||
* Add secondary_directory_servers option to fetch room list from other servers
|
|
||||||
(PR #808, #813)
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Report per request metrics for all of the things using request_handler (PR
|
|
||||||
#756)
|
|
||||||
* Correctly handle ``NULL`` password hashes from the database (PR #775)
|
|
||||||
* Allow receipts for events we haven't seen in the db (PR #784)
|
|
||||||
* Make synctl read a cache factor from config file (PR #785)
|
|
||||||
* Increment badge count per missed convo, not per msg (PR #793)
|
|
||||||
* Special case m.room.third_party_invite event auth to match invites (PR #814)
|
|
||||||
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix typo in event_auth servlet path (PR #757)
|
|
||||||
* Fix password reset (PR #758)
|
|
||||||
|
|
||||||
|
|
||||||
Performance improvements:
|
|
||||||
|
|
||||||
* Reduce database inserts when sending transactions (PR #767)
|
|
||||||
* Queue events by room for persistence (PR #768)
|
|
||||||
* Add cache to ``get_user_by_id`` (PR #772)
|
|
||||||
* Add and use ``get_domain_from_id`` (PR #773)
|
|
||||||
* Use tree cache for ``get_linearized_receipts_for_room`` (PR #779)
|
|
||||||
* Remove unused indices (PR #782)
|
|
||||||
* Add caches to ``bulk_get_push_rules*`` (PR #804)
|
|
||||||
* Cache ``get_event_reference_hashes`` (PR #806)
|
|
||||||
* Add ``get_users_with_read_receipts_in_room`` cache (PR #809)
|
|
||||||
* Use state to calculate ``get_users_in_room`` (PR #811)
|
|
||||||
* Load push rules in storage layer so that they get cached (PR #825)
|
|
||||||
* Make ``get_joined_hosts_for_room`` use get_users_in_room (PR #828)
|
|
||||||
* Poke notifier on next reactor tick (PR #829)
|
|
||||||
* Change CacheMetrics to be quicker (PR #830)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.15.0-rc1 (2016-04-26)
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
Features:
|
|
||||||
|
|
||||||
* Add login support for Javascript Web Tokens, thanks to Niklas Riekenbrauck
|
|
||||||
(PR #671,#687)
|
|
||||||
* Add URL previewing support (PR #688)
|
|
||||||
* Add login support for LDAP, thanks to Christoph Witzany (PR #701)
|
|
||||||
* Add GET endpoint for pushers (PR #716)
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
* Never notify for member events (PR #667)
|
|
||||||
* Deduplicate identical ``/sync`` requests (PR #668)
|
|
||||||
* Require user to have left room to forget room (PR #673)
|
|
||||||
* Use DNS cache if within TTL (PR #677)
|
|
||||||
* Let users see their own leave events (PR #699)
|
|
||||||
* Deduplicate membership changes (PR #700)
|
|
||||||
* Increase performance of pusher code (PR #705)
|
|
||||||
* Respond with error status 504 if failed to talk to remote server (PR #731)
|
|
||||||
* Increase search performance on postgres (PR #745)
|
|
||||||
|
|
||||||
Bug fixes:
|
|
||||||
|
|
||||||
* Fix bug where disabling all notifications still resulted in push (PR #678)
|
|
||||||
* Fix bug where users couldn't reject remote invites if remote refused (PR #691)
|
|
||||||
* Fix bug where synapse attempted to backfill from itself (PR #693)
|
|
||||||
* Fix bug where profile information was not correctly added when joining remote
|
|
||||||
rooms (PR #703)
|
|
||||||
* Fix bug where register API required incorrect key name for AS registration
|
|
||||||
(PR #727)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.14.0 (2016-03-30)
|
Changes in synapse v0.14.0 (2016-03-30)
|
||||||
=======================================
|
=======================================
|
||||||
|
|
||||||
@@ -1312,7 +511,7 @@ Configuration:
|
|||||||
|
|
||||||
* Add support for changing the bind host of the metrics listener via the
|
* Add support for changing the bind host of the metrics listener via the
|
||||||
``metrics_bind_host`` option.
|
``metrics_bind_host`` option.
|
||||||
|
|
||||||
|
|
||||||
Changes in synapse v0.9.0-r5 (2015-05-21)
|
Changes in synapse v0.9.0-r5 (2015-05-21)
|
||||||
=========================================
|
=========================================
|
||||||
@@ -1654,7 +853,7 @@ See UPGRADE for information about changes to the client server API, including
|
|||||||
breaking backwards compatibility with VoIP calls and registration API.
|
breaking backwards compatibility with VoIP calls and registration API.
|
||||||
|
|
||||||
Homeserver:
|
Homeserver:
|
||||||
* When a user changes their displayname or avatar the server will now update
|
* When a user changes their displayname or avatar the server will now update
|
||||||
all their join states to reflect this.
|
all their join states to reflect this.
|
||||||
* The server now adds "age" key to events to indicate how old they are. This
|
* The server now adds "age" key to events to indicate how old they are. This
|
||||||
is clock independent, so at no point does any server or webclient have to
|
is clock independent, so at no point does any server or webclient have to
|
||||||
@@ -1712,7 +911,7 @@ Changes in synapse 0.2.2 (2014-09-06)
|
|||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
Homeserver:
|
Homeserver:
|
||||||
* When the server returns state events it now also includes the previous
|
* When the server returns state events it now also includes the previous
|
||||||
content.
|
content.
|
||||||
* Add support for inviting people when creating a new room.
|
* Add support for inviting people when creating a new room.
|
||||||
* Make the homeserver inform the room via `m.room.aliases` when a new alias
|
* Make the homeserver inform the room via `m.room.aliases` when a new alias
|
||||||
@@ -1724,7 +923,7 @@ Webclient:
|
|||||||
* Handle `m.room.aliases` events.
|
* Handle `m.room.aliases` events.
|
||||||
* Asynchronously send messages and show a local echo.
|
* Asynchronously send messages and show a local echo.
|
||||||
* Inform the UI when a message failed to send.
|
* Inform the UI when a message failed to send.
|
||||||
* Only autoscroll on receiving a new message if the user was already at the
|
* Only autoscroll on receiving a new message if the user was already at the
|
||||||
bottom of the screen.
|
bottom of the screen.
|
||||||
* Add support for ban/kick reasons.
|
* Add support for ban/kick reasons.
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,8 @@ recursive-include synapse/storage/schema *.sql
|
|||||||
recursive-include synapse/storage/schema *.py
|
recursive-include synapse/storage/schema *.py
|
||||||
|
|
||||||
recursive-include docs *
|
recursive-include docs *
|
||||||
recursive-include res *
|
|
||||||
recursive-include scripts *
|
recursive-include scripts *
|
||||||
recursive-include scripts-dev *
|
recursive-include scripts-dev *
|
||||||
recursive-include synapse *.pyi
|
|
||||||
recursive-include tests *.py
|
recursive-include tests *.py
|
||||||
|
|
||||||
recursive-include synapse/static *.css
|
recursive-include synapse/static *.css
|
||||||
@@ -24,7 +22,5 @@ recursive-include synapse/static *.js
|
|||||||
|
|
||||||
exclude jenkins.sh
|
exclude jenkins.sh
|
||||||
exclude jenkins*.sh
|
exclude jenkins*.sh
|
||||||
exclude jenkins*
|
|
||||||
recursive-exclude jenkins *.sh
|
|
||||||
|
|
||||||
prune demo/etc
|
prune demo/etc
|
||||||
|
|||||||
703
README.rst
703
README.rst
@@ -11,8 +11,8 @@ VoIP. The basics you need to know to get up and running are:
|
|||||||
like ``#matrix:matrix.org`` or ``#test:localhost:8448``.
|
like ``#matrix:matrix.org`` or ``#test:localhost:8448``.
|
||||||
|
|
||||||
- Matrix user IDs look like ``@matthew:matrix.org`` (although in the future
|
- Matrix user IDs look like ``@matthew:matrix.org`` (although in the future
|
||||||
you will normally refer to yourself and others using a third party identifier
|
you will normally refer to yourself and others using a 3PID: email
|
||||||
(3PID): email address, phone number, etc rather than manipulating Matrix user IDs)
|
address, phone number, etc rather than manipulating Matrix user IDs)
|
||||||
|
|
||||||
The overall architecture is::
|
The overall architecture is::
|
||||||
|
|
||||||
@@ -20,13 +20,12 @@ The overall architecture is::
|
|||||||
https://somewhere.org/_matrix https://elsewhere.net/_matrix
|
https://somewhere.org/_matrix https://elsewhere.net/_matrix
|
||||||
|
|
||||||
``#matrix:matrix.org`` is the official support room for Matrix, and can be
|
``#matrix:matrix.org`` is the official support room for Matrix, and can be
|
||||||
accessed by any client from https://matrix.org/docs/projects/try-matrix-now or
|
accessed by any client from https://matrix.org/blog/try-matrix-now or via IRC
|
||||||
via IRC bridge at irc://irc.freenode.net/matrix.
|
bridge at irc://irc.freenode.net/matrix.
|
||||||
|
|
||||||
Synapse is currently in rapid development, but as of version 0.5 we believe it
|
Synapse is currently in rapid development, but as of version 0.5 we believe it
|
||||||
is sufficiently stable to be run as an internet-facing service for real usage!
|
is sufficiently stable to be run as an internet-facing service for real usage!
|
||||||
|
|
||||||
|
|
||||||
About Matrix
|
About Matrix
|
||||||
============
|
============
|
||||||
|
|
||||||
@@ -53,30 +52,39 @@ generation of fully open and interoperable messaging and VoIP apps for the
|
|||||||
internet.
|
internet.
|
||||||
|
|
||||||
Synapse is a reference "homeserver" implementation of Matrix from the core
|
Synapse is a reference "homeserver" implementation of Matrix from the core
|
||||||
development team at matrix.org, written in Python/Twisted. It is intended to
|
development team at matrix.org, written in Python/Twisted for clarity and
|
||||||
showcase the concept of Matrix and let folks see the spec in the context of a
|
simplicity. It is intended to showcase the concept of Matrix and let folks see
|
||||||
codebase and let you run your own homeserver and generally help bootstrap the
|
the spec in the context of a codebase and let you run your own homeserver and
|
||||||
ecosystem.
|
generally help bootstrap the ecosystem.
|
||||||
|
|
||||||
In Matrix, every user runs one or more Matrix clients, which connect through to
|
In Matrix, every user runs one or more Matrix clients, which connect through to
|
||||||
a Matrix homeserver. The homeserver stores all their personal chat history and
|
a Matrix homeserver which stores all their personal chat history and user
|
||||||
user account information - much as a mail client connects through to an
|
account information - much as a mail client connects through to an IMAP/SMTP
|
||||||
IMAP/SMTP server. Just like email, you can either run your own Matrix
|
server. Just like email, you can either run your own Matrix homeserver and
|
||||||
homeserver and control and own your own communications and history or use one
|
control and own your own communications and history or use one hosted by
|
||||||
hosted by someone else (e.g. matrix.org) - there is no single point of control
|
someone else (e.g. matrix.org) - there is no single point of control or
|
||||||
or mandatory service provider in Matrix, unlike WhatsApp, Facebook, Hangouts,
|
mandatory service provider in Matrix, unlike WhatsApp, Facebook, Hangouts, etc.
|
||||||
etc.
|
|
||||||
|
Synapse ships with two basic demo Matrix clients: webclient (a basic group chat
|
||||||
|
web client demo implemented in AngularJS) and cmdclient (a basic Python
|
||||||
|
command line utility which lets you easily see what the JSON APIs are up to).
|
||||||
|
|
||||||
|
Meanwhile, iOS and Android SDKs and clients are available from:
|
||||||
|
|
||||||
|
- https://github.com/matrix-org/matrix-ios-sdk
|
||||||
|
- https://github.com/matrix-org/matrix-ios-kit
|
||||||
|
- https://github.com/matrix-org/matrix-ios-console
|
||||||
|
- https://github.com/matrix-org/matrix-android-sdk
|
||||||
|
|
||||||
We'd like to invite you to join #matrix:matrix.org (via
|
We'd like to invite you to join #matrix:matrix.org (via
|
||||||
https://matrix.org/docs/projects/try-matrix-now), run a homeserver, take a look
|
https://matrix.org/blog/try-matrix-now), run a homeserver, take a look at the
|
||||||
at the `Matrix spec <https://matrix.org/docs/spec>`_, and experiment with the
|
Matrix spec at https://matrix.org/docs/spec and API docs at
|
||||||
`APIs <https://matrix.org/docs/api>`_ and `Client SDKs
|
https://matrix.org/docs/api, experiment with the APIs and the demo clients, and
|
||||||
<http://matrix.org/docs/projects/try-matrix-now.html#client-sdks>`_.
|
report any bugs via https://matrix.org/jira.
|
||||||
|
|
||||||
Thanks for using Matrix!
|
Thanks for using Matrix!
|
||||||
|
|
||||||
[1] End-to-end encryption is currently in beta: `blog post <https://matrix.org/blog/2016/11/21/matrixs-olm-end-to-end-encryption-security-assessment-released-and-implemented-cross-platform-on-riot-at-last>`_.
|
[1] End-to-end encryption is currently in development - see https://matrix.org/git/olm
|
||||||
|
|
||||||
|
|
||||||
Synapse Installation
|
Synapse Installation
|
||||||
====================
|
====================
|
||||||
@@ -86,14 +94,9 @@ Synapse is the reference python/twisted Matrix homeserver implementation.
|
|||||||
System requirements:
|
System requirements:
|
||||||
- POSIX-compliant system (tested on Linux & OS X)
|
- POSIX-compliant system (tested on Linux & OS X)
|
||||||
- Python 2.7
|
- Python 2.7
|
||||||
- At least 1GB of free RAM if you want to join large public rooms like #matrix:matrix.org
|
- At least 512 MB RAM.
|
||||||
|
|
||||||
Installing from source
|
Synapse is written in python but some of the libraries is uses are written in
|
||||||
----------------------
|
|
||||||
(Prebuilt packages are available for some platforms - see `Platform-Specific
|
|
||||||
Instructions`_.)
|
|
||||||
|
|
||||||
Synapse is written in python but some of the libraries it uses are written in
|
|
||||||
C. So before we can install synapse itself we need a working C compiler and the
|
C. So before we can install synapse itself we need a working C compiler and the
|
||||||
header files for python C extensions.
|
header files for python C extensions.
|
||||||
|
|
||||||
@@ -101,7 +104,7 @@ Installing prerequisites on Ubuntu or Debian::
|
|||||||
|
|
||||||
sudo apt-get install build-essential python2.7-dev libffi-dev \
|
sudo apt-get install build-essential python2.7-dev libffi-dev \
|
||||||
python-pip python-setuptools sqlite3 \
|
python-pip python-setuptools sqlite3 \
|
||||||
libssl-dev python-virtualenv libjpeg-dev libxslt1-dev
|
libssl-dev python-virtualenv libjpeg-dev
|
||||||
|
|
||||||
Installing prerequisites on ArchLinux::
|
Installing prerequisites on ArchLinux::
|
||||||
|
|
||||||
@@ -120,7 +123,6 @@ Installing prerequisites on Mac OS X::
|
|||||||
xcode-select --install
|
xcode-select --install
|
||||||
sudo easy_install pip
|
sudo easy_install pip
|
||||||
sudo pip install virtualenv
|
sudo pip install virtualenv
|
||||||
brew install pkg-config libffi
|
|
||||||
|
|
||||||
Installing prerequisites on Raspbian::
|
Installing prerequisites on Raspbian::
|
||||||
|
|
||||||
@@ -131,17 +133,6 @@ Installing prerequisites on Raspbian::
|
|||||||
sudo pip install --upgrade ndg-httpsclient
|
sudo pip install --upgrade ndg-httpsclient
|
||||||
sudo pip install --upgrade virtualenv
|
sudo pip install --upgrade virtualenv
|
||||||
|
|
||||||
Installing prerequisites on openSUSE::
|
|
||||||
|
|
||||||
sudo zypper in -t pattern devel_basis
|
|
||||||
sudo zypper in python-pip python-setuptools sqlite3 python-virtualenv \
|
|
||||||
python-devel libffi-devel libopenssl-devel libjpeg62-devel
|
|
||||||
|
|
||||||
Installing prerequisites on OpenBSD::
|
|
||||||
|
|
||||||
doas pkg_add python libffi py-pip py-setuptools sqlite3 py-virtualenv \
|
|
||||||
libxslt
|
|
||||||
|
|
||||||
To install the synapse homeserver run::
|
To install the synapse homeserver run::
|
||||||
|
|
||||||
virtualenv -p python2.7 ~/.synapse
|
virtualenv -p python2.7 ~/.synapse
|
||||||
@@ -153,74 +144,38 @@ This installs synapse, along with the libraries it uses, into a virtual
|
|||||||
environment under ``~/.synapse``. Feel free to pick a different directory
|
environment under ``~/.synapse``. Feel free to pick a different directory
|
||||||
if you prefer.
|
if you prefer.
|
||||||
|
|
||||||
In case of problems, please see the _`Troubleshooting` section below.
|
In case of problems, please see the _Troubleshooting section below.
|
||||||
|
|
||||||
Alternatively, Silvio Fricke has contributed a Dockerfile to automate the
|
Alternatively, Silvio Fricke has contributed a Dockerfile to automate the
|
||||||
above in Docker at https://registry.hub.docker.com/u/silviof/docker-matrix/.
|
above in Docker at https://registry.hub.docker.com/u/silviof/docker-matrix/.
|
||||||
|
|
||||||
Also, Martin Giess has created an auto-deployment process with vagrant/ansible,
|
Also, Martin Giess has created an auto-deployment process with vagrant/ansible,
|
||||||
tested with VirtualBox/AWS/DigitalOcean - see https://github.com/EMnify/matrix-synapse-auto-deploy
|
tested with VirtualBox/AWS/DigitalOcean - see https://github.com/EMnify/matrix-synapse-auto-deploy
|
||||||
for details.
|
for details.
|
||||||
|
|
||||||
Configuring synapse
|
To set up your homeserver, run (in your virtualenv, as before)::
|
||||||
-------------------
|
|
||||||
|
|
||||||
Before you can start Synapse, you will need to generate a configuration
|
|
||||||
file. To do this, run (in your virtualenv, as before)::
|
|
||||||
|
|
||||||
cd ~/.synapse
|
cd ~/.synapse
|
||||||
python -m synapse.app.homeserver \
|
python -m synapse.app.homeserver \
|
||||||
--server-name my.domain.name \
|
--server-name machine.my.domain.name \
|
||||||
--config-path homeserver.yaml \
|
--config-path homeserver.yaml \
|
||||||
--generate-config \
|
--generate-config \
|
||||||
--report-stats=[yes|no]
|
--report-stats=[yes|no]
|
||||||
|
|
||||||
... substituting an appropriate value for ``--server-name``. The server name
|
...substituting your host and domain name as appropriate.
|
||||||
determines the "domain" part of user-ids for users on your server: these will
|
|
||||||
all be of the format ``@user:my.domain.name``. It also determines how other
|
|
||||||
matrix servers will reach yours for `Federation`_. For a test configuration,
|
|
||||||
set this to the hostname of your server. For a more production-ready setup, you
|
|
||||||
will probably want to specify your domain (``example.com``) rather than a
|
|
||||||
matrix-specific hostname here (in the same way that your email address is
|
|
||||||
probably ``user@example.com`` rather than ``user@email.example.com``) - but
|
|
||||||
doing so may require more advanced setup - see `Setting up
|
|
||||||
Federation`_. Beware that the server name cannot be changed later.
|
|
||||||
|
|
||||||
This command will generate you a config file that you can then customise, but it will
|
This will generate you a config file that you can then customise, but it will
|
||||||
also generate a set of keys for you. These keys will allow your Home Server to
|
also generate a set of keys for you. These keys will allow your Home Server to
|
||||||
identify itself to other Home Servers, so don't lose or delete them. It would be
|
identify itself to other Home Servers, so don't lose or delete them. It would be
|
||||||
wise to back them up somewhere safe. (If, for whatever reason, you do need to
|
wise to back them up somewhere safe. If, for whatever reason, you do need to
|
||||||
change your Home Server's keys, you may find that other Home Servers have the
|
change your Home Server's keys, you may find that other Home Servers have the
|
||||||
old key cached. If you update the signing key, you should change the name of the
|
old key cached. If you update the signing key, you should change the name of the
|
||||||
key in the ``<server name>.signing.key`` file (the second word) to something
|
key in the <server name>.signing.key file (the second word) to something different.
|
||||||
different. See `the spec`__ for more information on key management.)
|
|
||||||
|
|
||||||
.. __: `key_management`_
|
By default, registration of new users is disabled. You can either enable
|
||||||
|
registration in the config by specifying ``enable_registration: true``
|
||||||
The default configuration exposes two HTTP ports: 8008 and 8448. Port 8008 is
|
(it is then recommended to also set up CAPTCHA - see docs/CAPTCHA_SETUP), or
|
||||||
configured without TLS; it is not recommended this be exposed outside your
|
you can use the command line to register new users::
|
||||||
local network. Port 8448 is configured to use TLS with a self-signed
|
|
||||||
certificate. This is fine for testing with but, to avoid your clients
|
|
||||||
complaining about the certificate, you will almost certainly want to use
|
|
||||||
another certificate for production purposes. (Note that a self-signed
|
|
||||||
certificate is fine for `Federation`_). You can do so by changing
|
|
||||||
``tls_certificate_path``, ``tls_private_key_path`` and ``tls_dh_params_path``
|
|
||||||
in ``homeserver.yaml``; alternatively, you can use a reverse-proxy, but be sure
|
|
||||||
to read `Using a reverse proxy with Synapse`_ when doing so.
|
|
||||||
|
|
||||||
Apart from port 8448 using TLS, both ports are the same in the default
|
|
||||||
configuration.
|
|
||||||
|
|
||||||
Registering a user
|
|
||||||
------------------
|
|
||||||
|
|
||||||
You will need at least one user on your server in order to use a Matrix
|
|
||||||
client. Users can be registered either `via a Matrix client`__, or via a
|
|
||||||
commandline script.
|
|
||||||
|
|
||||||
.. __: `client-user-reg`_
|
|
||||||
|
|
||||||
To get started, it is easiest to use the command line to register new users::
|
|
||||||
|
|
||||||
$ source ~/.synapse/bin/activate
|
$ source ~/.synapse/bin/activate
|
||||||
$ synctl start # if not already running
|
$ synctl start # if not already running
|
||||||
@@ -230,19 +185,8 @@ To get started, it is easiest to use the command line to register new users::
|
|||||||
Confirm password:
|
Confirm password:
|
||||||
Success!
|
Success!
|
||||||
|
|
||||||
This process uses a setting ``registration_shared_secret`` in
|
|
||||||
``homeserver.yaml``, which is shared between Synapse itself and the
|
|
||||||
``register_new_matrix_user`` script. It doesn't matter what it is (a random
|
|
||||||
value is generated by ``--generate-config``), but it should be kept secret, as
|
|
||||||
anyone with knowledge of it can register users on your server even if
|
|
||||||
``enable_registration`` is ``false``.
|
|
||||||
|
|
||||||
Setting up a TURN server
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
For reliable VoIP calls to be routed via this homeserver, you MUST configure
|
For reliable VoIP calls to be routed via this homeserver, you MUST configure
|
||||||
a TURN server. See `<docs/turn-howto.rst>`_ for details.
|
a TURN server. See docs/turn-howto.rst for details.
|
||||||
|
|
||||||
|
|
||||||
Running Synapse
|
Running Synapse
|
||||||
===============
|
===============
|
||||||
@@ -254,66 +198,29 @@ run (e.g. ``~/.synapse``), and::
|
|||||||
source ./bin/activate
|
source ./bin/activate
|
||||||
synctl start
|
synctl start
|
||||||
|
|
||||||
|
Using PostgreSQL
|
||||||
|
================
|
||||||
|
|
||||||
Connecting to Synapse from a client
|
As of Synapse 0.9, `PostgreSQL <http://www.postgresql.org>`_ is supported as an
|
||||||
===================================
|
alternative to the `SQLite <http://sqlite.org/>`_ database that Synapse has
|
||||||
|
traditionally used for convenience and simplicity.
|
||||||
|
|
||||||
The easiest way to try out your new Synapse installation is by connecting to it
|
The advantages of Postgres include:
|
||||||
from a web client. The easiest option is probably the one at
|
|
||||||
http://riot.im/app. You will need to specify a "Custom server" when you log on
|
|
||||||
or register: set this to ``https://localhost:8448`` - remember to specify the
|
|
||||||
port (``:8448``) unless you changed the configuration. (Leave the identity
|
|
||||||
server as the default - see `Identity servers`_.)
|
|
||||||
|
|
||||||
If all goes well you should at least be able to log in, create a room, and
|
* significant performance improvements due to the superior threading and
|
||||||
start sending messages.
|
caching model, smarter query optimiser
|
||||||
|
* allowing the DB to be run on separate hardware
|
||||||
|
* allowing basic active/backup high-availability with a "hot spare" synapse
|
||||||
|
pointing at the same DB master, as well as enabling DB replication in
|
||||||
|
synapse itself.
|
||||||
|
|
||||||
(The homeserver runs a web client by default at https://localhost:8448/, though
|
The only disadvantage is that the code is relatively new as of April 2015 and
|
||||||
as of the time of writing it is somewhat outdated and not really recommended -
|
may have a few regressions relative to SQLite.
|
||||||
https://github.com/matrix-org/synapse/issues/1527).
|
|
||||||
|
|
||||||
.. _`client-user-reg`:
|
For information on how to install and use PostgreSQL, please see
|
||||||
|
`docs/postgres.rst <docs/postgres.rst>`_.
|
||||||
|
|
||||||
Registering a new user from a client
|
Platform Specific Instructions
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
By default, registration of new users via Matrix clients is disabled. To enable
|
|
||||||
it, specify ``enable_registration: true`` in ``homeserver.yaml``. (It is then
|
|
||||||
recommended to also set up CAPTCHA - see `<docs/CAPTCHA_SETUP.rst>`_.)
|
|
||||||
|
|
||||||
Once ``enable_registration`` is set to ``true``, it is possible to register a
|
|
||||||
user via `riot.im <https://riot.im/app/#/register>`_ or other Matrix clients.
|
|
||||||
|
|
||||||
Your new user name will be formed partly from the ``server_name`` (see
|
|
||||||
`Configuring synapse`_), and partly from a localpart you specify when you
|
|
||||||
create the account. Your name will take the form of::
|
|
||||||
|
|
||||||
@localpart:my.domain.name
|
|
||||||
|
|
||||||
(pronounced "at localpart on my dot domain dot name").
|
|
||||||
|
|
||||||
As when logging in, you will need to specify a "Custom server". Specify your
|
|
||||||
desired ``localpart`` in the 'User name' box.
|
|
||||||
|
|
||||||
|
|
||||||
Security Note
|
|
||||||
=============
|
|
||||||
|
|
||||||
Matrix serves raw user generated data in some APIs - specifically the `content
|
|
||||||
repository endpoints <http://matrix.org/docs/spec/client_server/latest.html#get-matrix-media-r0-download-servername-mediaid>`_.
|
|
||||||
|
|
||||||
Whilst we have tried to mitigate against possible XSS attacks (e.g.
|
|
||||||
https://github.com/matrix-org/synapse/pull/1021) we recommend running
|
|
||||||
matrix homeservers on a dedicated domain name, to limit any malicious user generated
|
|
||||||
content served to web browsers a matrix API from being able to attack webapps hosted
|
|
||||||
on the same domain. This is particularly true of sharing a matrix webclient and
|
|
||||||
server on the same domain.
|
|
||||||
|
|
||||||
See https://github.com/vector-im/vector-web/issues/1977 and
|
|
||||||
https://developer.github.com/changes/2014-04-25-user-content-security for more details.
|
|
||||||
|
|
||||||
|
|
||||||
Platform-Specific Instructions
|
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
Debian
|
Debian
|
||||||
@@ -321,7 +228,7 @@ Debian
|
|||||||
|
|
||||||
Matrix provides official Debian packages via apt from http://matrix.org/packages/debian/.
|
Matrix provides official Debian packages via apt from http://matrix.org/packages/debian/.
|
||||||
Note that these packages do not include a client - choose one from
|
Note that these packages do not include a client - choose one from
|
||||||
https://matrix.org/docs/projects/try-matrix-now/ (or build your own with one of our SDKs :)
|
https://matrix.org/blog/try-matrix-now/ (or build your own with one of our SDKs :)
|
||||||
|
|
||||||
Fedora
|
Fedora
|
||||||
------
|
------
|
||||||
@@ -375,32 +282,6 @@ Synapse can be installed via FreeBSD Ports or Packages contributed by Brendan Mo
|
|||||||
- Ports: ``cd /usr/ports/net/py-matrix-synapse && make install clean``
|
- Ports: ``cd /usr/ports/net/py-matrix-synapse && make install clean``
|
||||||
- Packages: ``pkg install py27-matrix-synapse``
|
- Packages: ``pkg install py27-matrix-synapse``
|
||||||
|
|
||||||
|
|
||||||
OpenBSD
|
|
||||||
-------
|
|
||||||
|
|
||||||
There is currently no port for OpenBSD. Additionally, OpenBSD's security
|
|
||||||
settings require a slightly more difficult installation process.
|
|
||||||
|
|
||||||
1) Create a new directory in ``/usr/local`` called ``_synapse``. Also, create a
|
|
||||||
new user called ``_synapse`` and set that directory as the new user's home.
|
|
||||||
This is required because, by default, OpenBSD only allows binaries which need
|
|
||||||
write and execute permissions on the same memory space to be run from
|
|
||||||
``/usr/local``.
|
|
||||||
2) ``su`` to the new ``_synapse`` user and change to their home directory.
|
|
||||||
3) Create a new virtualenv: ``virtualenv -p python2.7 ~/.synapse``
|
|
||||||
4) Source the virtualenv configuration located at
|
|
||||||
``/usr/local/_synapse/.synapse/bin/activate``. This is done in ``ksh`` by
|
|
||||||
using the ``.`` command, rather than ``bash``'s ``source``.
|
|
||||||
5) Optionally, use ``pip`` to install ``lxml``, which Synapse needs to parse
|
|
||||||
webpages for their titles.
|
|
||||||
6) Use ``pip`` to install this repository: ``pip install
|
|
||||||
https://github.com/matrix-org/synapse/tarball/master``
|
|
||||||
7) Optionally, change ``_synapse``'s shell to ``/bin/false`` to reduce the
|
|
||||||
chance of a compromised Synapse server being used to take over your box.
|
|
||||||
|
|
||||||
After this, you may proceed with the rest of the install directions.
|
|
||||||
|
|
||||||
NixOS
|
NixOS
|
||||||
-----
|
-----
|
||||||
|
|
||||||
@@ -440,7 +321,6 @@ Troubleshooting:
|
|||||||
you do, you may need to create a symlink to ``libsodium.a`` so ``ld`` can find
|
you do, you may need to create a symlink to ``libsodium.a`` so ``ld`` can find
|
||||||
it: ``ln -s /usr/local/lib/libsodium.a /usr/lib/libsodium.a``
|
it: ``ln -s /usr/local/lib/libsodium.a /usr/lib/libsodium.a``
|
||||||
|
|
||||||
|
|
||||||
Troubleshooting
|
Troubleshooting
|
||||||
===============
|
===============
|
||||||
|
|
||||||
@@ -514,292 +394,9 @@ you will need to explicitly call Python2.7 - either running as::
|
|||||||
|
|
||||||
...or by editing synctl with the correct python executable.
|
...or by editing synctl with the correct python executable.
|
||||||
|
|
||||||
|
|
||||||
Upgrading an existing Synapse
|
|
||||||
=============================
|
|
||||||
|
|
||||||
The instructions for upgrading synapse are in `UPGRADE.rst`_.
|
|
||||||
Please check these instructions as upgrading may require extra steps for some
|
|
||||||
versions of synapse.
|
|
||||||
|
|
||||||
.. _UPGRADE.rst: UPGRADE.rst
|
|
||||||
|
|
||||||
.. _federation:
|
|
||||||
|
|
||||||
Setting up Federation
|
|
||||||
=====================
|
|
||||||
|
|
||||||
Federation is the process by which users on different servers can participate
|
|
||||||
in the same room. For this to work, those other servers must be able to contact
|
|
||||||
yours to send messages.
|
|
||||||
|
|
||||||
As explained in `Configuring synapse`_, the ``server_name`` in your
|
|
||||||
``homeserver.yaml`` file determines the way that other servers will reach
|
|
||||||
yours. By default, they will treat it as a hostname and try to connect to
|
|
||||||
port 8448. This is easy to set up and will work with the default configuration,
|
|
||||||
provided you set the ``server_name`` to match your machine's public DNS
|
|
||||||
hostname.
|
|
||||||
|
|
||||||
For a more flexible configuration, you can set up a DNS SRV record. This allows
|
|
||||||
you to run your server on a machine that might not have the same name as your
|
|
||||||
domain name. For example, you might want to run your server at
|
|
||||||
``synapse.example.com``, but have your Matrix user-ids look like
|
|
||||||
``@user:example.com``. (A SRV record also allows you to change the port from
|
|
||||||
the default 8448. However, if you are thinking of using a reverse-proxy, be
|
|
||||||
sure to read `Reverse-proxying the federation port`_ first.)
|
|
||||||
|
|
||||||
To use a SRV record, first create your SRV record and publish it in DNS. This
|
|
||||||
should have the format ``_matrix._tcp.<yourdomain.com> <ttl> IN SRV 10 0 <port>
|
|
||||||
<synapse.server.name>``. The DNS record should then look something like::
|
|
||||||
|
|
||||||
$ dig -t srv _matrix._tcp.example.com
|
|
||||||
_matrix._tcp.example.com. 3600 IN SRV 10 0 8448 synapse.example.com.
|
|
||||||
|
|
||||||
You can then configure your homeserver to use ``<yourdomain.com>`` as the domain in
|
|
||||||
its user-ids, by setting ``server_name``::
|
|
||||||
|
|
||||||
python -m synapse.app.homeserver \
|
|
||||||
--server-name <yourdomain.com> \
|
|
||||||
--config-path homeserver.yaml \
|
|
||||||
--generate-config
|
|
||||||
python -m synapse.app.homeserver --config-path homeserver.yaml
|
|
||||||
|
|
||||||
If you've already generated the config file, you need to edit the ``server_name``
|
|
||||||
in your ``homeserver.yaml`` file. If you've already started Synapse and a
|
|
||||||
database has been created, you will have to recreate the database.
|
|
||||||
|
|
||||||
If all goes well, you should be able to `connect to your server with a client`__,
|
|
||||||
and then join a room via federation. (Try ``#matrix-dev:matrix.org`` as a first
|
|
||||||
step. "Matrix HQ"'s sheer size and activity level tends to make even the
|
|
||||||
largest boxes pause for thought.)
|
|
||||||
|
|
||||||
.. __: `Connecting to Synapse from a client`_
|
|
||||||
|
|
||||||
Troubleshooting
|
|
||||||
---------------
|
|
||||||
The typical failure mode with federation is that when you try to join a room,
|
|
||||||
it is rejected with "401: Unauthorized". Generally this means that other
|
|
||||||
servers in the room couldn't access yours. (Joining a room over federation is a
|
|
||||||
complicated dance which requires connections in both directions).
|
|
||||||
|
|
||||||
So, things to check are:
|
|
||||||
|
|
||||||
* If you are trying to use a reverse-proxy, read `Reverse-proxying the
|
|
||||||
federation port`_.
|
|
||||||
* If you are not using a SRV record, check that your ``server_name`` (the part
|
|
||||||
of your user-id after the ``:``) matches your hostname, and that port 8448 on
|
|
||||||
that hostname is reachable from outside your network.
|
|
||||||
* If you *are* using a SRV record, check that it matches your ``server_name``
|
|
||||||
(it should be ``_matrix._tcp.<server_name>``), and that the port and hostname
|
|
||||||
it specifies are reachable from outside your network.
|
|
||||||
|
|
||||||
Running a Demo Federation of Synapses
|
|
||||||
-------------------------------------
|
|
||||||
|
|
||||||
If you want to get up and running quickly with a trio of homeservers in a
|
|
||||||
private federation, there is a script in the ``demo`` directory. This is mainly
|
|
||||||
useful just for development purposes. See `<demo/README>`_.
|
|
||||||
|
|
||||||
|
|
||||||
Using PostgreSQL
|
|
||||||
================
|
|
||||||
|
|
||||||
As of Synapse 0.9, `PostgreSQL <http://www.postgresql.org>`_ is supported as an
|
|
||||||
alternative to the `SQLite <http://sqlite.org/>`_ database that Synapse has
|
|
||||||
traditionally used for convenience and simplicity.
|
|
||||||
|
|
||||||
The advantages of Postgres include:
|
|
||||||
|
|
||||||
* significant performance improvements due to the superior threading and
|
|
||||||
caching model, smarter query optimiser
|
|
||||||
* allowing the DB to be run on separate hardware
|
|
||||||
* allowing basic active/backup high-availability with a "hot spare" synapse
|
|
||||||
pointing at the same DB master, as well as enabling DB replication in
|
|
||||||
synapse itself.
|
|
||||||
|
|
||||||
For information on how to install and use PostgreSQL, please see
|
|
||||||
`docs/postgres.rst <docs/postgres.rst>`_.
|
|
||||||
|
|
||||||
|
|
||||||
.. _reverse-proxy:
|
|
||||||
|
|
||||||
Using a reverse proxy with Synapse
|
|
||||||
==================================
|
|
||||||
|
|
||||||
It is possible to put a reverse proxy such as
|
|
||||||
`nginx <https://nginx.org/en/docs/http/ngx_http_proxy_module.html>`_,
|
|
||||||
`Apache <https://httpd.apache.org/docs/current/mod/mod_proxy_http.html>`_ or
|
|
||||||
`HAProxy <http://www.haproxy.org/>`_ in front of Synapse. One advantage of
|
|
||||||
doing so is that it means that you can expose the default https port (443) to
|
|
||||||
Matrix clients without needing to run Synapse with root privileges.
|
|
||||||
|
|
||||||
The most important thing to know here is that Matrix clients and other Matrix
|
|
||||||
servers do not necessarily need to connect to your server via the same
|
|
||||||
port. Indeed, clients will use port 443 by default, whereas servers default to
|
|
||||||
port 8448. Where these are different, we refer to the 'client port' and the
|
|
||||||
'federation port'.
|
|
||||||
|
|
||||||
The next most important thing to know is that using a reverse-proxy on the
|
|
||||||
federation port has a number of pitfalls. It is possible, but be sure to read
|
|
||||||
`Reverse-proxying the federation port`_.
|
|
||||||
|
|
||||||
The recommended setup is therefore to configure your reverse-proxy on port 443
|
|
||||||
for client connections, but to also expose port 8448 for server-server
|
|
||||||
connections. All the Matrix endpoints begin ``/_matrix``, so an example nginx
|
|
||||||
configuration might look like::
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
listen [::]:443 ssl;
|
|
||||||
server_name matrix.example.com;
|
|
||||||
|
|
||||||
location /_matrix {
|
|
||||||
proxy_pass http://localhost:8008;
|
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
You will also want to set ``bind_addresses: ['127.0.0.1']`` and ``x_forwarded: true``
|
|
||||||
for port 8008 in ``homeserver.yaml`` to ensure that client IP addresses are
|
|
||||||
recorded correctly.
|
|
||||||
|
|
||||||
Having done so, you can then use ``https://matrix.example.com`` (instead of
|
|
||||||
``https://matrix.example.com:8448``) as the "Custom server" when `Connecting to
|
|
||||||
Synapse from a client`_.
|
|
||||||
|
|
||||||
Reverse-proxying the federation port
|
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
There are two issues to consider before using a reverse-proxy on the federation
|
|
||||||
port:
|
|
||||||
|
|
||||||
* Due to the way SSL certificates are managed in the Matrix federation protocol
|
|
||||||
(see `spec`__), Synapse needs to be configured with the path to the SSL
|
|
||||||
certificate, *even if you do not terminate SSL at Synapse*.
|
|
||||||
|
|
||||||
.. __: `key_management`_
|
|
||||||
|
|
||||||
* Synapse does not currently support SNI on the federation protocol
|
|
||||||
(`bug #1491 <https://github.com/matrix-org/synapse/issues/1491>`_), which
|
|
||||||
means that using name-based virtual hosting is unreliable.
|
|
||||||
|
|
||||||
Furthermore, a number of the normal reasons for using a reverse-proxy do not
|
|
||||||
apply:
|
|
||||||
|
|
||||||
* Other servers will connect on port 8448 by default, so there is no need to
|
|
||||||
listen on port 443 (for federation, at least), which avoids the need for root
|
|
||||||
privileges and virtual hosting.
|
|
||||||
|
|
||||||
* A self-signed SSL certificate is fine for federation, so there is no need to
|
|
||||||
automate renewals. (The certificate generated by ``--generate-config`` is
|
|
||||||
valid for 10 years.)
|
|
||||||
|
|
||||||
If you want to set up a reverse-proxy on the federation port despite these
|
|
||||||
caveats, you will need to do the following:
|
|
||||||
|
|
||||||
* In ``homeserver.yaml``, set ``tls_certificate_path`` to the path to the SSL
|
|
||||||
certificate file used by your reverse-proxy, and set ``no_tls`` to ``True``.
|
|
||||||
(``tls_private_key_path`` will be ignored if ``no_tls`` is ``True``.)
|
|
||||||
|
|
||||||
* In your reverse-proxy configuration:
|
|
||||||
|
|
||||||
* If there are other virtual hosts on the same port, make sure that the
|
|
||||||
*default* one uses the certificate configured above.
|
|
||||||
|
|
||||||
* Forward ``/_matrix`` to Synapse.
|
|
||||||
|
|
||||||
* If your reverse-proxy is not listening on port 8448, publish a SRV record to
|
|
||||||
tell other servers how to find you. See `Setting up Federation`_.
|
|
||||||
|
|
||||||
When updating the SSL certificate, just update the file pointed to by
|
|
||||||
``tls_certificate_path``: there is no need to restart synapse. (You may like to
|
|
||||||
use a symbolic link to help make this process atomic.)
|
|
||||||
|
|
||||||
The most common mistake when setting up federation is not to tell Synapse about
|
|
||||||
your SSL certificate. To check it, you can visit
|
|
||||||
``https://matrix.org/federationtester/api/report?server_name=<your_server_name>``.
|
|
||||||
Unfortunately, there is no UI for this yet, but, you should see
|
|
||||||
``"MatchingTLSFingerprint": true``. If not, check that
|
|
||||||
``Certificates[0].SHA256Fingerprint`` (the fingerprint of the certificate
|
|
||||||
presented by your reverse-proxy) matches ``Keys.tls_fingerprints[0].sha256``
|
|
||||||
(the fingerprint of the certificate Synapse is using).
|
|
||||||
|
|
||||||
|
|
||||||
Identity Servers
|
|
||||||
================
|
|
||||||
|
|
||||||
Identity servers have the job of mapping email addresses and other 3rd Party
|
|
||||||
IDs (3PIDs) to Matrix user IDs, as well as verifying the ownership of 3PIDs
|
|
||||||
before creating that mapping.
|
|
||||||
|
|
||||||
**They are not where accounts or credentials are stored - these live on home
|
|
||||||
servers. Identity Servers are just for mapping 3rd party IDs to matrix IDs.**
|
|
||||||
|
|
||||||
This process is very security-sensitive, as there is obvious risk of spam if it
|
|
||||||
is too easy to sign up for Matrix accounts or harvest 3PID data. In the longer
|
|
||||||
term, we hope to create a decentralised system to manage it (`matrix-doc #712
|
|
||||||
<https://github.com/matrix-org/matrix-doc/issues/712>`_), but in the meantime,
|
|
||||||
the role of managing trusted identity in the Matrix ecosystem is farmed out to
|
|
||||||
a cluster of known trusted ecosystem partners, who run 'Matrix Identity
|
|
||||||
Servers' such as `Sydent <https://github.com/matrix-org/sydent>`_, whose role
|
|
||||||
is purely to authenticate and track 3PID logins and publish end-user public
|
|
||||||
keys.
|
|
||||||
|
|
||||||
You can host your own copy of Sydent, but this will prevent you reaching other
|
|
||||||
users in the Matrix ecosystem via their email address, and prevent them finding
|
|
||||||
you. We therefore recommend that you use one of the centralised identity servers
|
|
||||||
at ``https://matrix.org`` or ``https://vector.im`` for now.
|
|
||||||
|
|
||||||
To reiterate: the Identity server will only be used if you choose to associate
|
|
||||||
an email address with your account, or send an invite to another user via their
|
|
||||||
email address.
|
|
||||||
|
|
||||||
|
|
||||||
URL Previews
|
|
||||||
============
|
|
||||||
|
|
||||||
Synapse 0.15.0 introduces a new API for previewing URLs at
|
|
||||||
``/_matrix/media/r0/preview_url``. This is disabled by default. To turn it on
|
|
||||||
you must enable the ``url_preview_enabled: True`` config parameter and
|
|
||||||
explicitly specify the IP ranges that Synapse is not allowed to spider for
|
|
||||||
previewing in the ``url_preview_ip_range_blacklist`` configuration parameter.
|
|
||||||
This is critical from a security perspective to stop arbitrary Matrix users
|
|
||||||
spidering 'internal' URLs on your network. At the very least we recommend that
|
|
||||||
your loopback and RFC1918 IP addresses are blacklisted.
|
|
||||||
|
|
||||||
This also requires the optional lxml and netaddr python dependencies to be
|
|
||||||
installed.
|
|
||||||
|
|
||||||
|
|
||||||
Password reset
|
|
||||||
==============
|
|
||||||
|
|
||||||
If a user has registered an email address to their account using an identity
|
|
||||||
server, they can request a password-reset token via clients such as Vector.
|
|
||||||
|
|
||||||
A manual password reset can be done via direct database access as follows.
|
|
||||||
|
|
||||||
First calculate the hash of the new password::
|
|
||||||
|
|
||||||
$ source ~/.synapse/bin/activate
|
|
||||||
$ ./scripts/hash_password
|
|
||||||
Password:
|
|
||||||
Confirm password:
|
|
||||||
$2a$12$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
||||||
|
|
||||||
Then update the `users` table in the database::
|
|
||||||
|
|
||||||
UPDATE users SET password_hash='$2a$12$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
|
||||||
WHERE name='@test:test.com';
|
|
||||||
|
|
||||||
|
|
||||||
Synapse Development
|
Synapse Development
|
||||||
===================
|
===================
|
||||||
|
|
||||||
Before setting up a development environment for synapse, make sure you have the
|
|
||||||
system dependencies (such as the python header files) installed - see
|
|
||||||
`Installing from source`_.
|
|
||||||
|
|
||||||
To check out a synapse for development, clone the git repo into a working
|
To check out a synapse for development, clone the git repo into a working
|
||||||
directory of your choice::
|
directory of your choice::
|
||||||
|
|
||||||
@@ -811,8 +408,8 @@ to install using pip and a virtualenv::
|
|||||||
|
|
||||||
virtualenv env
|
virtualenv env
|
||||||
source env/bin/activate
|
source env/bin/activate
|
||||||
python synapse/python_dependencies.py | xargs pip install
|
python synapse/python_dependencies.py | xargs -n1 pip install
|
||||||
pip install lxml mock
|
pip install setuptools_trial mock
|
||||||
|
|
||||||
This will run a process of downloading and installing all the needed
|
This will run a process of downloading and installing all the needed
|
||||||
dependencies into a virtual env.
|
dependencies into a virtual env.
|
||||||
@@ -820,7 +417,7 @@ dependencies into a virtual env.
|
|||||||
Once this is done, you may wish to run Synapse's unit tests, to
|
Once this is done, you may wish to run Synapse's unit tests, to
|
||||||
check that everything is installed as it should be::
|
check that everything is installed as it should be::
|
||||||
|
|
||||||
PYTHONPATH="." trial tests
|
python setup.py test
|
||||||
|
|
||||||
This should end with a 'PASSED' result::
|
This should end with a 'PASSED' result::
|
||||||
|
|
||||||
@@ -829,6 +426,165 @@ This should end with a 'PASSED' result::
|
|||||||
PASSED (successes=143)
|
PASSED (successes=143)
|
||||||
|
|
||||||
|
|
||||||
|
Upgrading an existing Synapse
|
||||||
|
=============================
|
||||||
|
|
||||||
|
The instructions for upgrading synapse are in `UPGRADE.rst`_.
|
||||||
|
Please check these instructions as upgrading may require extra steps for some
|
||||||
|
versions of synapse.
|
||||||
|
|
||||||
|
.. _UPGRADE.rst: UPGRADE.rst
|
||||||
|
|
||||||
|
Setting up Federation
|
||||||
|
=====================
|
||||||
|
|
||||||
|
In order for other homeservers to send messages to your server, it will need to
|
||||||
|
be publicly visible on the internet, and they will need to know its host name.
|
||||||
|
You have two choices here, which will influence the form of your Matrix user
|
||||||
|
IDs:
|
||||||
|
|
||||||
|
1) Use the machine's own hostname as available on public DNS in the form of
|
||||||
|
its A or AAAA records. This is easier to set up initially, perhaps for
|
||||||
|
testing, but lacks the flexibility of SRV.
|
||||||
|
|
||||||
|
2) Set up a SRV record for your domain name. This requires you create a SRV
|
||||||
|
record in DNS, but gives the flexibility to run the server on your own
|
||||||
|
choice of TCP port, on a machine that might not be the same name as the
|
||||||
|
domain name.
|
||||||
|
|
||||||
|
For the first form, simply pass the required hostname (of the machine) as the
|
||||||
|
--server-name parameter::
|
||||||
|
|
||||||
|
python -m synapse.app.homeserver \
|
||||||
|
--server-name machine.my.domain.name \
|
||||||
|
--config-path homeserver.yaml \
|
||||||
|
--generate-config
|
||||||
|
python -m synapse.app.homeserver --config-path homeserver.yaml
|
||||||
|
|
||||||
|
Alternatively, you can run ``synctl start`` to guide you through the process.
|
||||||
|
|
||||||
|
For the second form, first create your SRV record and publish it in DNS. This
|
||||||
|
needs to be named _matrix._tcp.YOURDOMAIN, and point at at least one hostname
|
||||||
|
and port where the server is running. (At the current time synapse does not
|
||||||
|
support clustering multiple servers into a single logical homeserver). The DNS
|
||||||
|
record would then look something like::
|
||||||
|
|
||||||
|
$ dig -t srv _matrix._tcp.machine.my.domain.name
|
||||||
|
_matrix._tcp IN SRV 10 0 8448 machine.my.domain.name.
|
||||||
|
|
||||||
|
|
||||||
|
At this point, you should then run the homeserver with the hostname of this
|
||||||
|
SRV record, as that is the name other machines will expect it to have::
|
||||||
|
|
||||||
|
python -m synapse.app.homeserver \
|
||||||
|
--server-name YOURDOMAIN \
|
||||||
|
--config-path homeserver.yaml \
|
||||||
|
--generate-config
|
||||||
|
python -m synapse.app.homeserver --config-path homeserver.yaml
|
||||||
|
|
||||||
|
|
||||||
|
If you've already generated the config file, you need to edit the "server_name"
|
||||||
|
in you ```homeserver.yaml``` file. If you've already started Synapse and a
|
||||||
|
database has been created, you will have to recreate the database.
|
||||||
|
|
||||||
|
You may additionally want to pass one or more "-v" options, in order to
|
||||||
|
increase the verbosity of logging output; at least for initial testing.
|
||||||
|
|
||||||
|
Running a Demo Federation of Synapses
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
If you want to get up and running quickly with a trio of homeservers in a
|
||||||
|
private federation (``localhost:8080``, ``localhost:8081`` and
|
||||||
|
``localhost:8082``) which you can then access through the webclient running at
|
||||||
|
http://localhost:8080. Simply run::
|
||||||
|
|
||||||
|
demo/start.sh
|
||||||
|
|
||||||
|
This is mainly useful just for development purposes.
|
||||||
|
|
||||||
|
Running The Demo Web Client
|
||||||
|
===========================
|
||||||
|
|
||||||
|
The homeserver runs a web client by default at https://localhost:8448/.
|
||||||
|
|
||||||
|
If this is the first time you have used the client from that browser (it uses
|
||||||
|
HTML5 local storage to remember its config), you will need to log in to your
|
||||||
|
account. If you don't yet have an account, because you've just started the
|
||||||
|
homeserver for the first time, then you'll need to register one.
|
||||||
|
|
||||||
|
|
||||||
|
Registering A New Account
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Your new user name will be formed partly from the hostname your server is
|
||||||
|
running as, and partly from a localpart you specify when you create the
|
||||||
|
account. Your name will take the form of::
|
||||||
|
|
||||||
|
@localpart:my.domain.here
|
||||||
|
(pronounced "at localpart on my dot domain dot here")
|
||||||
|
|
||||||
|
Specify your desired localpart in the topmost box of the "Register for an
|
||||||
|
account" form, and click the "Register" button. Hostnames can contain ports if
|
||||||
|
required due to lack of SRV records (e.g. @matthew:localhost:8448 on an
|
||||||
|
internal synapse sandbox running on localhost).
|
||||||
|
|
||||||
|
If registration fails, you may need to enable it in the homeserver (see
|
||||||
|
`Synapse Installation`_ above)
|
||||||
|
|
||||||
|
|
||||||
|
Logging In To An Existing Account
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
Just enter the ``@localpart:my.domain.here`` Matrix user ID and password into
|
||||||
|
the form and click the Login button.
|
||||||
|
|
||||||
|
Identity Servers
|
||||||
|
================
|
||||||
|
|
||||||
|
The job of authenticating 3PIDs and tracking which 3PIDs are associated with a
|
||||||
|
given Matrix user is very security-sensitive, as there is obvious risk of spam
|
||||||
|
if it is too easy to sign up for Matrix accounts or harvest 3PID data.
|
||||||
|
Meanwhile the job of publishing the end-to-end encryption public keys for
|
||||||
|
Matrix users is also very security-sensitive for similar reasons.
|
||||||
|
|
||||||
|
Therefore the role of managing trusted identity in the Matrix ecosystem is
|
||||||
|
farmed out to a cluster of known trusted ecosystem partners, who run 'Matrix
|
||||||
|
Identity Servers' such as ``sydent``, whose role is purely to authenticate and
|
||||||
|
track 3PID logins and publish end-user public keys.
|
||||||
|
|
||||||
|
It's currently early days for identity servers as Matrix is not yet using 3PIDs
|
||||||
|
as the primary means of identity and E2E encryption is not complete. As such,
|
||||||
|
we are running a single identity server (https://matrix.org) at the current
|
||||||
|
time.
|
||||||
|
|
||||||
|
Password reset
|
||||||
|
==============
|
||||||
|
|
||||||
|
If a user has registered an email address to their account using an identity
|
||||||
|
server, they can request a password-reset token via clients such as Vector.
|
||||||
|
|
||||||
|
A manual password reset can be done via direct database access as follows.
|
||||||
|
|
||||||
|
First calculate the hash of the new password:
|
||||||
|
|
||||||
|
$ source ~/.synapse/bin/activate
|
||||||
|
$ ./scripts/hash_password
|
||||||
|
Password:
|
||||||
|
Confirm password:
|
||||||
|
$2a$12$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
|
||||||
|
Then update the `users` table in the database:
|
||||||
|
|
||||||
|
UPDATE users SET password_hash='$2a$12$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
||||||
|
WHERE name='@test:test.com';
|
||||||
|
|
||||||
|
Where's the spec?!
|
||||||
|
==================
|
||||||
|
|
||||||
|
The source of the matrix spec lives at https://github.com/matrix-org/matrix-doc.
|
||||||
|
A recent HTML snapshot of this lives at http://matrix.org/docs/spec
|
||||||
|
|
||||||
|
|
||||||
Building Internal API Documentation
|
Building Internal API Documentation
|
||||||
===================================
|
===================================
|
||||||
|
|
||||||
@@ -843,7 +599,8 @@ Building internal API documentation::
|
|||||||
python setup.py build_sphinx
|
python setup.py build_sphinx
|
||||||
|
|
||||||
|
|
||||||
Help!! Synapse eats all my RAM!
|
|
||||||
|
Halp!! Synapse eats all my RAM!
|
||||||
===============================
|
===============================
|
||||||
|
|
||||||
Synapse's architecture is quite RAM hungry currently - we deliberately
|
Synapse's architecture is quite RAM hungry currently - we deliberately
|
||||||
@@ -858,5 +615,3 @@ around a ~700MB footprint. You can dial it down further to 0.02 if
|
|||||||
desired, which targets roughly ~512MB. Conversely you can dial it up if
|
desired, which targets roughly ~512MB. Conversely you can dial it up if
|
||||||
you need performance for lots of users and have a box with a lot of RAM.
|
you need performance for lots of users and have a box with a lot of RAM.
|
||||||
|
|
||||||
|
|
||||||
.. _`key_management`: https://matrix.org/docs/spec/server_server/unstable.html#retrieving-server-keys
|
|
||||||
|
|||||||
10
UPGRADE.rst
10
UPGRADE.rst
@@ -27,15 +27,7 @@ running:
|
|||||||
# Pull the latest version of the master branch.
|
# Pull the latest version of the master branch.
|
||||||
git pull
|
git pull
|
||||||
# Update the versions of synapse's python dependencies.
|
# Update the versions of synapse's python dependencies.
|
||||||
python synapse/python_dependencies.py | xargs -n1 pip install --upgrade
|
python synapse/python_dependencies.py | xargs -n1 pip install
|
||||||
|
|
||||||
|
|
||||||
Upgrading to v0.15.0
|
|
||||||
====================
|
|
||||||
|
|
||||||
If you want to use the new URL previewing API (/_matrix/media/r0/preview_url)
|
|
||||||
then you have to explicitly enable it in the config and update your dependencies
|
|
||||||
dependencies. See README.rst for details.
|
|
||||||
|
|
||||||
|
|
||||||
Upgrading to v0.11.0
|
Upgrading to v0.11.0
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ Description=Synapse Matrix homeserver
|
|||||||
Type=simple
|
Type=simple
|
||||||
User=synapse
|
User=synapse
|
||||||
Group=synapse
|
Group=synapse
|
||||||
EnvironmentFile=-/etc/sysconfig/synapse
|
|
||||||
WorkingDirectory=/var/lib/synapse
|
WorkingDirectory=/var/lib/synapse
|
||||||
ExecStart=/usr/bin/python2.7 -m synapse.app.homeserver --config-path=/etc/synapse/homeserver.yaml --log-config=/etc/synapse/log_config.yaml
|
ExecStart=/usr/bin/python2.7 -m synapse.app.homeserver --config-path=/etc/synapse/homeserver.yaml --log-config=/etc/synapse/log_config.yaml
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ https://developers.google.com/recaptcha/
|
|||||||
|
|
||||||
Setting ReCaptcha Keys
|
Setting ReCaptcha Keys
|
||||||
----------------------
|
----------------------
|
||||||
The keys are a config option on the home server config. If they are not
|
The keys are a config option on the home server config. If they are not
|
||||||
visible, you can generate them via --generate-config. Set the following value::
|
visible, you can generate them via --generate-config. Set the following value:
|
||||||
|
|
||||||
recaptcha_public_key: YOUR_PUBLIC_KEY
|
recaptcha_public_key: YOUR_PUBLIC_KEY
|
||||||
recaptcha_private_key: YOUR_PRIVATE_KEY
|
recaptcha_private_key: YOUR_PRIVATE_KEY
|
||||||
|
|
||||||
In addition, you MUST enable captchas via::
|
In addition, you MUST enable captchas via:
|
||||||
|
|
||||||
enable_registration_captcha: true
|
enable_registration_captcha: true
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ Configuring IP used for auth
|
|||||||
The ReCaptcha API requires that the IP address of the user who solved the
|
The ReCaptcha API requires that the IP address of the user who solved the
|
||||||
captcha is sent. If the client is connecting through a proxy or load balancer,
|
captcha is sent. If the client is connecting through a proxy or load balancer,
|
||||||
it may be required to use the X-Forwarded-For (XFF) header instead of the origin
|
it may be required to use the X-Forwarded-For (XFF) header instead of the origin
|
||||||
IP address. This can be configured as an option on the home server like so::
|
IP address. This can be configured as an option on the home server like so:
|
||||||
|
|
||||||
captcha_ip_origin_is_x_forwarded: true
|
captcha_ip_origin_is_x_forwarded: true
|
||||||
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
Admin APIs
|
|
||||||
==========
|
|
||||||
|
|
||||||
This directory includes documentation for the various synapse specific admin
|
|
||||||
APIs available.
|
|
||||||
|
|
||||||
Only users that are server admins can use these APIs. A user can be marked as a
|
|
||||||
server admin by updating the database directly, e.g.:
|
|
||||||
|
|
||||||
``UPDATE users SET admin = 1 WHERE name = '@foo:bar.com'``
|
|
||||||
|
|
||||||
Restarting may be required for the changes to register.
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
Purge History API
|
|
||||||
=================
|
|
||||||
|
|
||||||
The purge history API allows server admins to purge historic events from their
|
|
||||||
database, reclaiming disk space.
|
|
||||||
|
|
||||||
Depending on the amount of history being purged a call to the API may take
|
|
||||||
several minutes or longer. During this period users will not be able to
|
|
||||||
paginate further back in the room from the point being purged from.
|
|
||||||
|
|
||||||
The API is simply:
|
|
||||||
|
|
||||||
``POST /_matrix/client/r0/admin/purge_history/<room_id>/<event_id>``
|
|
||||||
|
|
||||||
including an ``access_token`` of a server admin.
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
Purge Remote Media API
|
|
||||||
======================
|
|
||||||
|
|
||||||
The purge remote media API allows server admins to purge old cached remote
|
|
||||||
media.
|
|
||||||
|
|
||||||
The API is::
|
|
||||||
|
|
||||||
POST /_matrix/client/r0/admin/purge_media_cache?before_ts=<unix_timestamp_in_ms>&access_token=<access_token>
|
|
||||||
|
|
||||||
{}
|
|
||||||
|
|
||||||
Which will remove all cached media that was last accessed before
|
|
||||||
``<unix_timestamp_in_ms>``.
|
|
||||||
|
|
||||||
If the user re-requests purged remote media, synapse will re-request the media
|
|
||||||
from the originating server.
|
|
||||||
@@ -32,4 +32,5 @@ The format of the AS configuration file is as follows:
|
|||||||
|
|
||||||
See the spec_ for further details on how application services work.
|
See the spec_ for further details on how application services work.
|
||||||
|
|
||||||
.. _spec: https://matrix.org/docs/spec/application_service/unstable.html
|
.. _spec: https://github.com/matrix-org/matrix-doc/blob/master/specification/25_application_service_api.rst#application-service-api
|
||||||
|
|
||||||
|
|||||||
@@ -43,10 +43,7 @@ Basically, PEP8
|
|||||||
together, or want to deliberately extend or preserve vertical/horizontal
|
together, or want to deliberately extend or preserve vertical/horizontal
|
||||||
space)
|
space)
|
||||||
|
|
||||||
Comments should follow the `google code style <http://google.github.io/styleguide/pyguide.html?showone=Comments#Comments>`_.
|
Comments should follow the google code style. This is so that we can generate
|
||||||
This is so that we can generate documentation with
|
documentation with sphinx (http://sphinxcontrib-napoleon.readthedocs.org/en/latest/)
|
||||||
`sphinx <http://sphinxcontrib-napoleon.readthedocs.org/en/latest/>`_. See the
|
|
||||||
`examples <http://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html>`_
|
|
||||||
in the sphinx documentation.
|
|
||||||
|
|
||||||
Code should pass pep8 --max-line-length=100 without any warnings.
|
Code should pass pep8 --max-line-length=100 without any warnings.
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
What do I do about "Unexpected logging context" debug log-lines everywhere?
|
|
||||||
|
|
||||||
<Mjark> The logging context lives in thread local storage
|
|
||||||
<Mjark> Sometimes it gets out of sync with what it should actually be, usually because something scheduled something to run on the reactor without preserving the logging context.
|
|
||||||
<Matthew> what is the impact of it getting out of sync? and how and when should we preserve log context?
|
|
||||||
<Mjark> The impact is that some of the CPU and database metrics will be under-reported, and some log lines will be mis-attributed.
|
|
||||||
<Mjark> It should happen auto-magically in all the APIs that do IO or otherwise defer to the reactor.
|
|
||||||
<Erik> Mjark: the other place is if we branch, e.g. using defer.gatherResults
|
|
||||||
|
|
||||||
Unanswered: how and when should we preserve log context?
|
|
||||||
@@ -15,45 +15,36 @@ How to monitor Synapse metrics using Prometheus
|
|||||||
|
|
||||||
Restart synapse
|
Restart synapse
|
||||||
|
|
||||||
3: Add a prometheus target for synapse. It needs to set the ``metrics_path``
|
3: Check out synapse-prometheus-config
|
||||||
to a non-default value::
|
https://github.com/matrix-org/synapse-prometheus-config
|
||||||
|
|
||||||
- job_name: "synapse"
|
4: Add ``synapse.html`` and ``synapse.rules``
|
||||||
metrics_path: "/_synapse/metrics"
|
The ``.html`` file needs to appear in prometheus's ``consoles`` directory,
|
||||||
static_configs:
|
and the ``.rules`` file needs to be invoked somewhere in the main config
|
||||||
- targets:
|
file. A symlink to each from the git checkout into the prometheus directory
|
||||||
"my.server.here:9092"
|
might be easiest to ensure ``git pull`` keeps it updated.
|
||||||
|
|
||||||
Standard Metric Names
|
5: Add a prometheus target for synapse
|
||||||
---------------------
|
This is easiest if prometheus runs on the same machine as synapse, as it can
|
||||||
|
then just use localhost::
|
||||||
|
|
||||||
As of synapse version 0.18.2, the format of the process-wide metrics has been
|
global: {
|
||||||
changed to fit prometheus standard naming conventions. Additionally the units
|
rule_file: "synapse.rules"
|
||||||
have been changed to seconds, from miliseconds.
|
}
|
||||||
|
|
||||||
================================== =============================
|
job: {
|
||||||
New name Old name
|
name: "synapse"
|
||||||
---------------------------------- -----------------------------
|
|
||||||
process_cpu_user_seconds_total process_resource_utime / 1000
|
|
||||||
process_cpu_system_seconds_total process_resource_stime / 1000
|
|
||||||
process_open_fds (no 'type' label) process_fds
|
|
||||||
================================== =============================
|
|
||||||
|
|
||||||
The python-specific counts of garbage collector performance have been renamed.
|
target_group: {
|
||||||
|
target: "http://localhost:9092/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
=========================== ======================
|
6: Start prometheus::
|
||||||
New name Old name
|
|
||||||
--------------------------- ----------------------
|
|
||||||
python_gc_time reactor_gc_time
|
|
||||||
python_gc_unreachable_total reactor_gc_unreachable
|
|
||||||
python_gc_counts reactor_gc_counts
|
|
||||||
=========================== ======================
|
|
||||||
|
|
||||||
The twisted-specific reactor metrics have been renamed.
|
./prometheus -config.file=prometheus.conf
|
||||||
|
|
||||||
==================================== =====================
|
7: Wait a few seconds for it to start and perform the first scrape,
|
||||||
New name Old name
|
then visit the console:
|
||||||
------------------------------------ ---------------------
|
|
||||||
python_twisted_reactor_pending_calls reactor_pending_calls
|
http://server-where-prometheus-runs:9090/consoles/synapse.html
|
||||||
python_twisted_reactor_tick_time reactor_tick_time
|
|
||||||
==================================== =====================
|
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
Replication Architecture
|
|
||||||
========================
|
|
||||||
|
|
||||||
Motivation
|
|
||||||
----------
|
|
||||||
|
|
||||||
We'd like to be able to split some of the work that synapse does into multiple
|
|
||||||
python processes. In theory multiple synapse processes could share a single
|
|
||||||
postgresql database and we'd scale up by running more synapse processes.
|
|
||||||
However much of synapse assumes that only one process is interacting with the
|
|
||||||
database, both for assigning unique identifiers when inserting into tables,
|
|
||||||
notifying components about new updates, and for invalidating its caches.
|
|
||||||
|
|
||||||
So running multiple copies of the current code isn't an option. One way to
|
|
||||||
run multiple processes would be to have a single writer process and multiple
|
|
||||||
reader processes connected to the same database. In order to do this we'd need
|
|
||||||
a way for the reader process to invalidate its in-memory caches when an update
|
|
||||||
happens on the writer. One way to do this is for the writer to present an
|
|
||||||
append-only log of updates which the readers can consume to invalidate their
|
|
||||||
caches and to push updates to listening clients or pushers.
|
|
||||||
|
|
||||||
Synapse already stores much of its data as an append-only log so that it can
|
|
||||||
correctly respond to /sync requests so the amount of code changes needed to
|
|
||||||
expose the append-only log to the readers should be fairly minimal.
|
|
||||||
|
|
||||||
Architecture
|
|
||||||
------------
|
|
||||||
|
|
||||||
The Replication API
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Synapse will optionally expose a long poll HTTP API for extracting updates. The
|
|
||||||
API will have a similar shape to /sync in that clients provide tokens
|
|
||||||
indicating where in the log they have reached and a timeout. The synapse server
|
|
||||||
then either responds with updates immediately if it already has updates or it
|
|
||||||
waits until the timeout for more updates. If the timeout expires and nothing
|
|
||||||
happened then the server returns an empty response.
|
|
||||||
|
|
||||||
However unlike the /sync API this replication API is returning synapse specific
|
|
||||||
data rather than trying to implement a matrix specification. The replication
|
|
||||||
results are returned as arrays of rows where the rows are mostly lifted
|
|
||||||
directly from the database. This avoids unnecessary JSON parsing on the server
|
|
||||||
and hopefully avoids an impedance mismatch between the data returned and the
|
|
||||||
required updates to the datastore.
|
|
||||||
|
|
||||||
This does not replicate all the database tables as many of the database tables
|
|
||||||
are indexes that can be recovered from the contents of other tables.
|
|
||||||
|
|
||||||
The format and parameters for the api are documented in
|
|
||||||
``synapse/replication/resource.py``.
|
|
||||||
|
|
||||||
|
|
||||||
The Slaved DataStore
|
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
There are read-only version of the synapse storage layer in
|
|
||||||
``synapse/replication/slave/storage`` that use the response of the replication
|
|
||||||
API to invalidate their caches.
|
|
||||||
@@ -9,35 +9,31 @@ the Home Server to generate credentials that are valid for use on the TURN
|
|||||||
server through the use of a secret shared between the Home Server and the
|
server through the use of a secret shared between the Home Server and the
|
||||||
TURN server.
|
TURN server.
|
||||||
|
|
||||||
This document describes how to install coturn
|
This document described how to install coturn
|
||||||
(https://github.com/coturn/coturn) which also supports the TURN REST API,
|
(https://code.google.com/p/coturn/) which also supports the TURN REST API,
|
||||||
and integrate it with synapse.
|
and integrate it with synapse.
|
||||||
|
|
||||||
coturn Setup
|
coturn Setup
|
||||||
============
|
============
|
||||||
|
|
||||||
You may be able to setup coturn via your package manager, or set it up manually using the usual ``configure, make, make install`` process.
|
|
||||||
|
|
||||||
1. Check out coturn::
|
1. Check out coturn::
|
||||||
|
svn checkout http://coturn.googlecode.com/svn/trunk/ coturn
|
||||||
git clone https://github.com/coturn/coturn.git coturn
|
|
||||||
cd coturn
|
cd coturn
|
||||||
|
|
||||||
2. Configure it::
|
2. Configure it::
|
||||||
|
|
||||||
./configure
|
./configure
|
||||||
|
|
||||||
You may need to install ``libevent2``: if so, you should do so
|
You may need to install libevent2: if so, you should do so
|
||||||
in the way recommended by your operating system.
|
in the way recommended by your operating system.
|
||||||
You can ignore warnings about lack of database support: a
|
You can ignore warnings about lack of database support: a
|
||||||
database is unnecessary for this purpose.
|
database is unnecessary for this purpose.
|
||||||
|
|
||||||
3. Build and install it::
|
3. Build and install it::
|
||||||
|
|
||||||
make
|
make
|
||||||
make install
|
make install
|
||||||
|
|
||||||
4. Create or edit the config file in ``/etc/turnserver.conf``. The relevant
|
4. Make a config file in /etc/turnserver.conf. You can customise
|
||||||
|
a config file from turnserver.conf.default. The relevant
|
||||||
lines, with example values, are::
|
lines, with example values, are::
|
||||||
|
|
||||||
lt-cred-mech
|
lt-cred-mech
|
||||||
@@ -45,7 +41,7 @@ You may be able to setup coturn via your package manager, or set it up manually
|
|||||||
static-auth-secret=[your secret key here]
|
static-auth-secret=[your secret key here]
|
||||||
realm=turn.myserver.org
|
realm=turn.myserver.org
|
||||||
|
|
||||||
See turnserver.conf for explanations of the options.
|
See turnserver.conf.default for explanations of the options.
|
||||||
One way to generate the static-auth-secret is with pwgen::
|
One way to generate the static-auth-secret is with pwgen::
|
||||||
|
|
||||||
pwgen -s 64 1
|
pwgen -s 64 1
|
||||||
@@ -58,7 +54,6 @@ You may be able to setup coturn via your package manager, or set it up manually
|
|||||||
import your private key and certificate.
|
import your private key and certificate.
|
||||||
|
|
||||||
7. Start the turn server::
|
7. Start the turn server::
|
||||||
|
|
||||||
bin/turnserver -o
|
bin/turnserver -o
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
URL Previews
|
|
||||||
============
|
|
||||||
|
|
||||||
Design notes on a URL previewing service for Matrix:
|
|
||||||
|
|
||||||
Options are:
|
|
||||||
|
|
||||||
1. Have an AS which listens for URLs, downloads them, and inserts an event that describes their metadata.
|
|
||||||
* Pros:
|
|
||||||
* Decouples the implementation entirely from Synapse.
|
|
||||||
* Uses existing Matrix events & content repo to store the metadata.
|
|
||||||
* Cons:
|
|
||||||
* Which AS should provide this service for a room, and why should you trust it?
|
|
||||||
* Doesn't work well with E2E; you'd have to cut the AS into every room
|
|
||||||
* the AS would end up subscribing to every room anyway.
|
|
||||||
|
|
||||||
2. Have a generic preview API (nothing to do with Matrix) that provides a previewing service:
|
|
||||||
* Pros:
|
|
||||||
* Simple and flexible; can be used by any clients at any point
|
|
||||||
* Cons:
|
|
||||||
* If each HS provides one of these independently, all the HSes in a room may needlessly DoS the target URI
|
|
||||||
* We need somewhere to store the URL metadata rather than just using Matrix itself
|
|
||||||
* We can't piggyback on matrix to distribute the metadata between HSes.
|
|
||||||
|
|
||||||
3. Make the synapse of the sending user responsible for spidering the URL and inserting an event asynchronously which describes the metadata.
|
|
||||||
* Pros:
|
|
||||||
* Works transparently for all clients
|
|
||||||
* Piggy-backs nicely on using Matrix for distributing the metadata.
|
|
||||||
* No confusion as to which AS
|
|
||||||
* Cons:
|
|
||||||
* Doesn't work with E2E
|
|
||||||
* We might want to decouple the implementation of the spider from the HS, given spider behaviour can be quite complicated and evolve much more rapidly than the HS. It's more like a bot than a core part of the server.
|
|
||||||
|
|
||||||
4. Make the sending client use the preview API and insert the event itself when successful.
|
|
||||||
* Pros:
|
|
||||||
* Works well with E2E
|
|
||||||
* No custom server functionality
|
|
||||||
* Lets the client customise the preview that they send (like on FB)
|
|
||||||
* Cons:
|
|
||||||
* Entirely specific to the sending client, whereas it'd be nice if /any/ URL was correctly previewed if clients support it.
|
|
||||||
|
|
||||||
5. Have the option of specifying a shared (centralised) previewing service used by a room, to avoid all the different HSes in the room DoSing the target.
|
|
||||||
|
|
||||||
Best solution is probably a combination of both 2 and 4.
|
|
||||||
* Sending clients do their best to create and send a preview at the point of sending the message, perhaps delaying the message until the preview is computed? (This also lets the user validate the preview before sending)
|
|
||||||
* Receiving clients have the option of going and creating their own preview if one doesn't arrive soon enough (or if the original sender didn't create one)
|
|
||||||
|
|
||||||
This is a bit magical though in that the preview could come from two entirely different sources - the sending HS or your local one. However, this can always be exposed to users: "Generate your own URL previews if none are available?"
|
|
||||||
|
|
||||||
This is tantamount also to senders calculating their own thumbnails for sending in advance of the main content - we are trusting the sender not to lie about the content in the thumbnail. Whereas currently thumbnails are calculated by the receiving homeserver to avoid this attack.
|
|
||||||
|
|
||||||
However, this kind of phishing attack does exist whether we let senders pick their thumbnails or not, in that a malicious sender can send normal text messages around the attachment claiming it to be legitimate. We could rely on (future) reputation/abuse management to punish users who phish (be it with bogus metadata or bogus descriptions). Bogus metadata is particularly bad though, especially if it's avoidable.
|
|
||||||
|
|
||||||
As a first cut, let's do #2 and have the receiver hit the API to calculate its own previews (as it does currently for image thumbnails). We can then extend/optimise this to option 4 as a special extra if needed.
|
|
||||||
|
|
||||||
API
|
|
||||||
---
|
|
||||||
|
|
||||||
GET /_matrix/media/r0/preview_url?url=http://wherever.com
|
|
||||||
200 OK
|
|
||||||
{
|
|
||||||
"og:type" : "article"
|
|
||||||
"og:url" : "https://twitter.com/matrixdotorg/status/684074366691356672"
|
|
||||||
"og:title" : "Matrix on Twitter"
|
|
||||||
"og:image" : "https://pbs.twimg.com/profile_images/500400952029888512/yI0qtFi7_400x400.png"
|
|
||||||
"og:description" : "“Synapse 0.12 is out! Lots of polishing, performance &amp; bugfixes: /sync API, /r0 prefix, fulltext search, 3PID invites https://t.co/5alhXLLEGP”"
|
|
||||||
"og:site_name" : "Twitter"
|
|
||||||
}
|
|
||||||
|
|
||||||
* Downloads the URL
|
|
||||||
* If HTML, just stores it in RAM and parses it for OG meta tags
|
|
||||||
* Download any media OG meta tags to the media repo, and refer to them in the OG via mxc:// URIs.
|
|
||||||
* If a media filetype we know we can thumbnail: store it on disk, and hand it to the thumbnailer. Generate OG meta tags from the thumbnailer contents.
|
|
||||||
* Otherwise, don't bother downloading further.
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
Scaling synapse via workers
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
Synapse has experimental support for splitting out functionality into
|
|
||||||
multiple separate python processes, helping greatly with scalability. These
|
|
||||||
processes are called 'workers', and are (eventually) intended to scale
|
|
||||||
horizontally independently.
|
|
||||||
|
|
||||||
All processes continue to share the same database instance, and as such, workers
|
|
||||||
only work with postgres based synapse deployments (sharing a single sqlite
|
|
||||||
across multiple processes is a recipe for disaster, plus you should be using
|
|
||||||
postgres anyway if you care about scalability).
|
|
||||||
|
|
||||||
The workers communicate with the master synapse process via a synapse-specific
|
|
||||||
HTTP protocol called 'replication' - analogous to MySQL or Postgres style
|
|
||||||
database replication; feeding a stream of relevant data to the workers so they
|
|
||||||
can be kept in sync with the main synapse process and database state.
|
|
||||||
|
|
||||||
To enable workers, you need to add a replication listener to the master synapse, e.g.::
|
|
||||||
|
|
||||||
listeners:
|
|
||||||
- port: 9092
|
|
||||||
bind_address: '127.0.0.1'
|
|
||||||
type: http
|
|
||||||
tls: false
|
|
||||||
x_forwarded: false
|
|
||||||
resources:
|
|
||||||
- names: [replication]
|
|
||||||
compress: false
|
|
||||||
|
|
||||||
Under **no circumstances** should this replication API listener be exposed to the
|
|
||||||
public internet; it currently implements no authentication whatsoever and is
|
|
||||||
unencrypted HTTP.
|
|
||||||
|
|
||||||
You then create a set of configs for the various worker processes. These should be
|
|
||||||
worker configuration files should be stored in a dedicated subdirectory, to allow
|
|
||||||
synctl to manipulate them.
|
|
||||||
|
|
||||||
The current available worker applications are:
|
|
||||||
* synapse.app.pusher - handles sending push notifications to sygnal and email
|
|
||||||
* synapse.app.synchrotron - handles /sync endpoints. can scales horizontally through multiple instances.
|
|
||||||
* synapse.app.appservice - handles output traffic to Application Services
|
|
||||||
* synapse.app.federation_reader - handles receiving federation traffic (including public_rooms API)
|
|
||||||
* synapse.app.media_repository - handles the media repository.
|
|
||||||
* synapse.app.client_reader - handles client API endpoints like /publicRooms
|
|
||||||
|
|
||||||
Each worker configuration file inherits the configuration of the main homeserver
|
|
||||||
configuration file. You can then override configuration specific to that worker,
|
|
||||||
e.g. the HTTP listener that it provides (if any); logging configuration; etc.
|
|
||||||
You should minimise the number of overrides though to maintain a usable config.
|
|
||||||
|
|
||||||
You must specify the type of worker application (worker_app) and the replication
|
|
||||||
endpoint that it's talking to on the main synapse process (worker_replication_url).
|
|
||||||
|
|
||||||
For instance::
|
|
||||||
|
|
||||||
worker_app: synapse.app.synchrotron
|
|
||||||
|
|
||||||
# The replication listener on the synapse to talk to.
|
|
||||||
worker_replication_url: http://127.0.0.1:9092/_synapse/replication
|
|
||||||
|
|
||||||
worker_listeners:
|
|
||||||
- type: http
|
|
||||||
port: 8083
|
|
||||||
resources:
|
|
||||||
- names:
|
|
||||||
- client
|
|
||||||
|
|
||||||
worker_daemonize: True
|
|
||||||
worker_pid_file: /home/matrix/synapse/synchrotron.pid
|
|
||||||
worker_log_config: /home/matrix/synapse/config/synchrotron_log_config.yaml
|
|
||||||
|
|
||||||
...is a full configuration for a synchrotron worker instance, which will expose a
|
|
||||||
plain HTTP /sync endpoint on port 8083 separately from the /sync endpoint provided
|
|
||||||
by the main synapse.
|
|
||||||
|
|
||||||
Obviously you should configure your loadbalancer to route the /sync endpoint to
|
|
||||||
the synchrotron instance(s) in this instance.
|
|
||||||
|
|
||||||
Finally, to actually run your worker-based synapse, you must pass synctl the -a
|
|
||||||
commandline option to tell it to operate on all the worker configurations found
|
|
||||||
in the given directory, e.g.::
|
|
||||||
|
|
||||||
synctl -a $CONFIG/workers start
|
|
||||||
|
|
||||||
Currently one should always restart all workers when restarting or upgrading
|
|
||||||
synapse, unless you explicitly know it's safe not to. For instance, restarting
|
|
||||||
synapse without restarting all the synchrotrons may result in broken typing
|
|
||||||
notifications.
|
|
||||||
|
|
||||||
To manipulate a specific worker, you pass the -w option to synctl::
|
|
||||||
|
|
||||||
synctl -w $CONFIG/workers/synchrotron.yaml restart
|
|
||||||
|
|
||||||
All of the above is highly experimental and subject to change as Synapse evolves,
|
|
||||||
but documenting it here to help folks needing highly scalable Synapses similar
|
|
||||||
to the one running matrix.org!
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -eux
|
|
||||||
|
|
||||||
: ${WORKSPACE:="$(pwd)"}
|
|
||||||
|
|
||||||
export WORKSPACE
|
|
||||||
export PYTHONDONTWRITEBYTECODE=yep
|
|
||||||
export SYNAPSE_CACHE_FACTOR=1
|
|
||||||
|
|
||||||
./jenkins/prepare_synapse.sh
|
|
||||||
./jenkins/clone.sh sytest https://github.com/matrix-org/sytest.git
|
|
||||||
./jenkins/clone.sh dendron https://github.com/matrix-org/dendron.git
|
|
||||||
./dendron/jenkins/build_dendron.sh
|
|
||||||
./sytest/jenkins/prep_sytest_for_postgres.sh
|
|
||||||
|
|
||||||
./sytest/jenkins/install_and_run.sh \
|
|
||||||
--synapse-directory $WORKSPACE \
|
|
||||||
--dendron $WORKSPACE/dendron/bin/dendron \
|
|
||||||
--pusher \
|
|
||||||
--synchrotron \
|
|
||||||
--federation-reader \
|
|
||||||
--client-reader \
|
|
||||||
--appservice \
|
|
||||||
--federation-sender \
|
|
||||||
@@ -4,14 +4,58 @@ set -eux
|
|||||||
|
|
||||||
: ${WORKSPACE:="$(pwd)"}
|
: ${WORKSPACE:="$(pwd)"}
|
||||||
|
|
||||||
export WORKSPACE
|
|
||||||
export PYTHONDONTWRITEBYTECODE=yep
|
export PYTHONDONTWRITEBYTECODE=yep
|
||||||
export SYNAPSE_CACHE_FACTOR=1
|
export SYNAPSE_CACHE_FACTOR=1
|
||||||
|
|
||||||
./jenkins/prepare_synapse.sh
|
# Output test results as junit xml
|
||||||
./jenkins/clone.sh sytest https://github.com/matrix-org/sytest.git
|
export TRIAL_FLAGS="--reporter=subunit"
|
||||||
|
export TOXSUFFIX="| subunit-1to2 | subunit2junitxml --no-passthrough --output-to=results.xml"
|
||||||
|
# Write coverage reports to a separate file for each process
|
||||||
|
export COVERAGE_OPTS="-p"
|
||||||
|
export DUMP_COVERAGE_COMMAND="coverage help"
|
||||||
|
|
||||||
./sytest/jenkins/prep_sytest_for_postgres.sh
|
# Output flake8 violations to violations.flake8.log
|
||||||
|
# Don't exit with non-0 status code on Jenkins,
|
||||||
|
# so that the build steps continue and a later step can decided whether to
|
||||||
|
# UNSTABLE or FAILURE this build.
|
||||||
|
export PEP8SUFFIX="--output-file=violations.flake8.log || echo flake8 finished with status code \$?"
|
||||||
|
|
||||||
./sytest/jenkins/install_and_run.sh \
|
rm .coverage* || echo "No coverage files to remove"
|
||||||
--synapse-directory $WORKSPACE \
|
|
||||||
|
tox --notest -e py27
|
||||||
|
|
||||||
|
TOX_BIN=$WORKSPACE/.tox/py27/bin
|
||||||
|
$TOX_BIN/pip install psycopg2
|
||||||
|
|
||||||
|
: ${GIT_BRANCH:="origin/$(git rev-parse --abbrev-ref HEAD)"}
|
||||||
|
|
||||||
|
if [[ ! -e .sytest-base ]]; then
|
||||||
|
git clone https://github.com/matrix-org/sytest.git .sytest-base --mirror
|
||||||
|
else
|
||||||
|
(cd .sytest-base; git fetch -p)
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf sytest
|
||||||
|
git clone .sytest-base sytest --shared
|
||||||
|
cd sytest
|
||||||
|
|
||||||
|
git checkout "${GIT_BRANCH}" || (echo >&2 "No ref ${GIT_BRANCH} found, falling back to develop" ; git checkout develop)
|
||||||
|
|
||||||
|
: ${PORT_BASE:=8000}
|
||||||
|
|
||||||
|
./jenkins/prep_sytest_for_postgres.sh
|
||||||
|
|
||||||
|
echo >&2 "Running sytest with PostgreSQL";
|
||||||
|
./jenkins/install_and_run.sh --coverage \
|
||||||
|
--python $TOX_BIN/python \
|
||||||
|
--synapse-directory $WORKSPACE \
|
||||||
|
--port-base $PORT_BASE
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
cp sytest/.coverage.* .
|
||||||
|
|
||||||
|
# Combine the coverage reports
|
||||||
|
echo "Combining:" .coverage.*
|
||||||
|
$TOX_BIN/python -m coverage combine
|
||||||
|
# Output coverage to coverage.xml
|
||||||
|
$TOX_BIN/coverage xml -o coverage.xml
|
||||||
|
|||||||
@@ -4,12 +4,52 @@ set -eux
|
|||||||
|
|
||||||
: ${WORKSPACE:="$(pwd)"}
|
: ${WORKSPACE:="$(pwd)"}
|
||||||
|
|
||||||
export WORKSPACE
|
|
||||||
export PYTHONDONTWRITEBYTECODE=yep
|
export PYTHONDONTWRITEBYTECODE=yep
|
||||||
export SYNAPSE_CACHE_FACTOR=1
|
export SYNAPSE_CACHE_FACTOR=1
|
||||||
|
|
||||||
./jenkins/prepare_synapse.sh
|
# Output test results as junit xml
|
||||||
./jenkins/clone.sh sytest https://github.com/matrix-org/sytest.git
|
export TRIAL_FLAGS="--reporter=subunit"
|
||||||
|
export TOXSUFFIX="| subunit-1to2 | subunit2junitxml --no-passthrough --output-to=results.xml"
|
||||||
|
# Write coverage reports to a separate file for each process
|
||||||
|
export COVERAGE_OPTS="-p"
|
||||||
|
export DUMP_COVERAGE_COMMAND="coverage help"
|
||||||
|
|
||||||
./sytest/jenkins/install_and_run.sh \
|
# Output flake8 violations to violations.flake8.log
|
||||||
--synapse-directory $WORKSPACE \
|
# Don't exit with non-0 status code on Jenkins,
|
||||||
|
# so that the build steps continue and a later step can decided whether to
|
||||||
|
# UNSTABLE or FAILURE this build.
|
||||||
|
export PEP8SUFFIX="--output-file=violations.flake8.log || echo flake8 finished with status code \$?"
|
||||||
|
|
||||||
|
rm .coverage* || echo "No coverage files to remove"
|
||||||
|
|
||||||
|
tox --notest -e py27
|
||||||
|
TOX_BIN=$WORKSPACE/.tox/py27/bin
|
||||||
|
|
||||||
|
: ${GIT_BRANCH:="origin/$(git rev-parse --abbrev-ref HEAD)"}
|
||||||
|
|
||||||
|
if [[ ! -e .sytest-base ]]; then
|
||||||
|
git clone https://github.com/matrix-org/sytest.git .sytest-base --mirror
|
||||||
|
else
|
||||||
|
(cd .sytest-base; git fetch -p)
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf sytest
|
||||||
|
git clone .sytest-base sytest --shared
|
||||||
|
cd sytest
|
||||||
|
|
||||||
|
git checkout "${GIT_BRANCH}" || (echo >&2 "No ref ${GIT_BRANCH} found, falling back to develop" ; git checkout develop)
|
||||||
|
|
||||||
|
: ${PORT_BASE:=8500}
|
||||||
|
./jenkins/install_and_run.sh --coverage \
|
||||||
|
--python $TOX_BIN/python \
|
||||||
|
--synapse-directory $WORKSPACE \
|
||||||
|
--port-base $PORT_BASE
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
cp sytest/.coverage.* .
|
||||||
|
|
||||||
|
# Combine the coverage reports
|
||||||
|
echo "Combining:" .coverage.*
|
||||||
|
$TOX_BIN/python -m coverage combine
|
||||||
|
# Output coverage to coverage.xml
|
||||||
|
$TOX_BIN/coverage xml -o coverage.xml
|
||||||
|
|||||||
@@ -22,9 +22,4 @@ export PEP8SUFFIX="--output-file=violations.flake8.log || echo flake8 finished w
|
|||||||
|
|
||||||
rm .coverage* || echo "No coverage files to remove"
|
rm .coverage* || echo "No coverage files to remove"
|
||||||
|
|
||||||
tox --notest -e py27
|
|
||||||
TOX_BIN=$WORKSPACE/.tox/py27/bin
|
|
||||||
python synapse/python_dependencies.py | xargs -n1 $TOX_BIN/pip install
|
|
||||||
$TOX_BIN/pip install lxml
|
|
||||||
|
|
||||||
tox -e py27
|
tox -e py27
|
||||||
|
|||||||
86
jenkins.sh
Executable file
86
jenkins.sh
Executable file
@@ -0,0 +1,86 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -eux
|
||||||
|
|
||||||
|
: ${WORKSPACE:="$(pwd)"}
|
||||||
|
|
||||||
|
export PYTHONDONTWRITEBYTECODE=yep
|
||||||
|
export SYNAPSE_CACHE_FACTOR=1
|
||||||
|
|
||||||
|
# Output test results as junit xml
|
||||||
|
export TRIAL_FLAGS="--reporter=subunit"
|
||||||
|
export TOXSUFFIX="| subunit-1to2 | subunit2junitxml --no-passthrough --output-to=results.xml"
|
||||||
|
# Write coverage reports to a separate file for each process
|
||||||
|
export COVERAGE_OPTS="-p"
|
||||||
|
export DUMP_COVERAGE_COMMAND="coverage help"
|
||||||
|
|
||||||
|
# Output flake8 violations to violations.flake8.log
|
||||||
|
# Don't exit with non-0 status code on Jenkins,
|
||||||
|
# so that the build steps continue and a later step can decided whether to
|
||||||
|
# UNSTABLE or FAILURE this build.
|
||||||
|
export PEP8SUFFIX="--output-file=violations.flake8.log || echo flake8 finished with status code \$?"
|
||||||
|
|
||||||
|
rm .coverage* || echo "No coverage files to remove"
|
||||||
|
|
||||||
|
tox
|
||||||
|
|
||||||
|
: ${GIT_BRANCH:="origin/$(git rev-parse --abbrev-ref HEAD)"}
|
||||||
|
|
||||||
|
TOX_BIN=$WORKSPACE/.tox/py27/bin
|
||||||
|
|
||||||
|
if [[ ! -e .sytest-base ]]; then
|
||||||
|
git clone https://github.com/matrix-org/sytest.git .sytest-base --mirror
|
||||||
|
else
|
||||||
|
(cd .sytest-base; git fetch -p)
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf sytest
|
||||||
|
git clone .sytest-base sytest --shared
|
||||||
|
cd sytest
|
||||||
|
|
||||||
|
git checkout "${GIT_BRANCH}" || (echo >&2 "No ref ${GIT_BRANCH} found, falling back to develop" ; git checkout develop)
|
||||||
|
|
||||||
|
: ${PERL5LIB:=$WORKSPACE/perl5/lib/perl5}
|
||||||
|
: ${PERL_MB_OPT:=--install_base=$WORKSPACE/perl5}
|
||||||
|
: ${PERL_MM_OPT:=INSTALL_BASE=$WORKSPACE/perl5}
|
||||||
|
export PERL5LIB PERL_MB_OPT PERL_MM_OPT
|
||||||
|
|
||||||
|
./install-deps.pl
|
||||||
|
|
||||||
|
: ${PORT_BASE:=8000}
|
||||||
|
|
||||||
|
echo >&2 "Running sytest with SQLite3";
|
||||||
|
./run-tests.pl --coverage -O tap --synapse-directory $WORKSPACE \
|
||||||
|
--python $TOX_BIN/python --all --port-base $PORT_BASE > results-sqlite3.tap
|
||||||
|
|
||||||
|
RUN_POSTGRES=""
|
||||||
|
|
||||||
|
for port in $(($PORT_BASE + 1)) $(($PORT_BASE + 2)); do
|
||||||
|
if psql synapse_jenkins_$port <<< ""; then
|
||||||
|
RUN_POSTGRES="$RUN_POSTGRES:$port"
|
||||||
|
cat > localhost-$port/database.yaml << EOF
|
||||||
|
name: psycopg2
|
||||||
|
args:
|
||||||
|
database: synapse_jenkins_$port
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Run if both postgresql databases exist
|
||||||
|
if test "$RUN_POSTGRES" = ":$(($PORT_BASE + 1)):$(($PORT_BASE + 2))"; then
|
||||||
|
echo >&2 "Running sytest with PostgreSQL";
|
||||||
|
$TOX_BIN/pip install psycopg2
|
||||||
|
./run-tests.pl --coverage -O tap --synapse-directory $WORKSPACE \
|
||||||
|
--python $TOX_BIN/python --all --port-base $PORT_BASE > results-postgresql.tap
|
||||||
|
else
|
||||||
|
echo >&2 "Skipping running sytest with PostgreSQL, $RUN_POSTGRES"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
cp sytest/.coverage.* .
|
||||||
|
|
||||||
|
# Combine the coverage reports
|
||||||
|
echo "Combining:" .coverage.*
|
||||||
|
$TOX_BIN/python -m coverage combine
|
||||||
|
# Output coverage to coverage.xml
|
||||||
|
$TOX_BIN/coverage xml -o coverage.xml
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#! /bin/bash
|
|
||||||
|
|
||||||
# This clones a project from github into a named subdirectory
|
|
||||||
# If the project has a branch with the same name as this branch
|
|
||||||
# then it will checkout that branch after cloning.
|
|
||||||
# Otherwise it will checkout "origin/develop."
|
|
||||||
# The first argument is the name of the directory to checkout
|
|
||||||
# the branch into.
|
|
||||||
# The second argument is the URL of the remote repository to checkout.
|
|
||||||
# Usually something like https://github.com/matrix-org/sytest.git
|
|
||||||
|
|
||||||
set -eux
|
|
||||||
|
|
||||||
NAME=$1
|
|
||||||
PROJECT=$2
|
|
||||||
BASE=".$NAME-base"
|
|
||||||
|
|
||||||
# Update our mirror.
|
|
||||||
if [ ! -d ".$NAME-base" ]; then
|
|
||||||
# Create a local mirror of the source repository.
|
|
||||||
# This saves us from having to download the entire repository
|
|
||||||
# when this script is next run.
|
|
||||||
git clone "$PROJECT" "$BASE" --mirror
|
|
||||||
else
|
|
||||||
# Fetch any updates from the source repository.
|
|
||||||
(cd "$BASE"; git fetch -p)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove the existing repository so that we have a clean copy
|
|
||||||
rm -rf "$NAME"
|
|
||||||
# Cloning with --shared means that we will share portions of the
|
|
||||||
# .git directory with our local mirror.
|
|
||||||
git clone "$BASE" "$NAME" --shared
|
|
||||||
|
|
||||||
# Jenkins may have supplied us with the name of the branch in the
|
|
||||||
# environment. Otherwise we will have to guess based on the current
|
|
||||||
# commit.
|
|
||||||
: ${GIT_BRANCH:="origin/$(git rev-parse --abbrev-ref HEAD)"}
|
|
||||||
cd "$NAME"
|
|
||||||
# check out the relevant branch
|
|
||||||
git checkout "${GIT_BRANCH}" || (
|
|
||||||
echo >&2 "No ref ${GIT_BRANCH} found, falling back to develop"
|
|
||||||
git checkout "origin/develop"
|
|
||||||
)
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
#! /bin/bash
|
|
||||||
|
|
||||||
cd "`dirname $0`/.."
|
|
||||||
|
|
||||||
TOX_DIR=$WORKSPACE/.tox
|
|
||||||
|
|
||||||
mkdir -p $TOX_DIR
|
|
||||||
|
|
||||||
if ! [ $TOX_DIR -ef .tox ]; then
|
|
||||||
ln -s "$TOX_DIR" .tox
|
|
||||||
fi
|
|
||||||
|
|
||||||
# set up the virtualenv
|
|
||||||
tox -e py27 --notest -v
|
|
||||||
|
|
||||||
TOX_BIN=$TOX_DIR/py27/bin
|
|
||||||
$TOX_BIN/pip install setuptools
|
|
||||||
{ python synapse/python_dependencies.py
|
|
||||||
echo lxml psycopg2
|
|
||||||
} | xargs $TOX_BIN/pip install
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
.header {
|
|
||||||
border-bottom: 4px solid #e4f7ed ! important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notif_link a, .footer a {
|
|
||||||
color: #76CFA6 ! important;
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
body {
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre, code {
|
|
||||||
word-break: break-word;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page {
|
|
||||||
font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
|
|
||||||
font-color: #454545;
|
|
||||||
font-size: 12pt;
|
|
||||||
width: 100%;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#inner {
|
|
||||||
width: 640px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
width: 100%;
|
|
||||||
height: 87px;
|
|
||||||
color: #454545;
|
|
||||||
border-bottom: 4px solid #e5e5e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
text-align: right;
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.salutation {
|
|
||||||
padding-top: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summarytext {
|
|
||||||
}
|
|
||||||
|
|
||||||
.room {
|
|
||||||
width: 100%;
|
|
||||||
color: #454545;
|
|
||||||
border-bottom: 1px solid #e5e5e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.room_header td {
|
|
||||||
padding-top: 38px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 1px solid #e5e5e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.room_name {
|
|
||||||
vertical-align: middle;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.room_header h2 {
|
|
||||||
margin-top: 0px;
|
|
||||||
margin-left: 75px;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.room_avatar {
|
|
||||||
width: 56px;
|
|
||||||
line-height: 0px;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.room_avatar img {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notif {
|
|
||||||
border-bottom: 1px solid #e5e5e5;
|
|
||||||
margin-top: 16px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.historical_message .sender_avatar {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* spell out opacity and historical_message class names for Outlook aka Word */
|
|
||||||
.historical_message .sender_name {
|
|
||||||
color: #e3e3e3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.historical_message .message_time {
|
|
||||||
color: #e3e3e3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.historical_message .message_body {
|
|
||||||
color: #c7c7c7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.historical_message td,
|
|
||||||
.message td {
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender_avatar {
|
|
||||||
width: 56px;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender_avatar img {
|
|
||||||
margin-top: -2px;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender_name {
|
|
||||||
display: inline;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #a2a2a2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message_time {
|
|
||||||
text-align: right;
|
|
||||||
width: 100px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #a2a2a2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message_body {
|
|
||||||
}
|
|
||||||
|
|
||||||
.notif_link td {
|
|
||||||
padding-top: 10px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notif_link a, .footer a {
|
|
||||||
color: #454545;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.debug {
|
|
||||||
font-size: 10px;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
margin-top: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
{% for message in notif.messages %}
|
|
||||||
<tr class="{{ "historical_message" if message.is_historical else "message" }}">
|
|
||||||
<td class="sender_avatar">
|
|
||||||
{% if loop.index0 == 0 or notif.messages[loop.index0 - 1].sender_name != notif.messages[loop.index0].sender_name %}
|
|
||||||
{% if message.sender_avatar_url %}
|
|
||||||
<img alt="" class="sender_avatar" src="{{ message.sender_avatar_url|mxc_to_http(32,32) }}" />
|
|
||||||
{% else %}
|
|
||||||
{% if message.sender_hash % 3 == 0 %}
|
|
||||||
<img class="sender_avatar" src="https://vector.im/beta/img/76cfa6.png" />
|
|
||||||
{% elif message.sender_hash % 3 == 1 %}
|
|
||||||
<img class="sender_avatar" src="https://vector.im/beta/img/50e2c2.png" />
|
|
||||||
{% else %}
|
|
||||||
<img class="sender_avatar" src="https://vector.im/beta/img/f4c371.png" />
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="message_contents">
|
|
||||||
{% if loop.index0 == 0 or notif.messages[loop.index0 - 1].sender_name != notif.messages[loop.index0].sender_name %}
|
|
||||||
<div class="sender_name">{% if message.msgtype == "m.emote" %}*{% endif %} {{ message.sender_name }}</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="message_body">
|
|
||||||
{% if message.msgtype == "m.text" %}
|
|
||||||
{{ message.body_text_html }}
|
|
||||||
{% elif message.msgtype == "m.emote" %}
|
|
||||||
{{ message.body_text_html }}
|
|
||||||
{% elif message.msgtype == "m.notice" %}
|
|
||||||
{{ message.body_text_html }}
|
|
||||||
{% elif message.msgtype == "m.image" %}
|
|
||||||
<img src="{{ message.image_url|mxc_to_http(640, 480, scale) }}" />
|
|
||||||
{% elif message.msgtype == "m.file" %}
|
|
||||||
<span class="filename">{{ message.body_text_plain }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="message_time">{{ message.ts|format_ts("%H:%M") }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
<tr class="notif_link">
|
|
||||||
<td></td>
|
|
||||||
<td>
|
|
||||||
<a href="{{ notif.link }}">View {{ room.title }}</a>
|
|
||||||
</td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{% for message in notif.messages %}
|
|
||||||
{% if message.msgtype == "m.emote" %}* {% endif %}{{ message.sender_name }} ({{ message.ts|format_ts("%H:%M") }})
|
|
||||||
{% if message.msgtype == "m.text" %}
|
|
||||||
{{ message.body_text_plain }}
|
|
||||||
{% elif message.msgtype == "m.emote" %}
|
|
||||||
{{ message.body_text_plain }}
|
|
||||||
{% elif message.msgtype == "m.notice" %}
|
|
||||||
{{ message.body_text_plain }}
|
|
||||||
{% elif message.msgtype == "m.image" %}
|
|
||||||
{{ message.body_text_plain }}
|
|
||||||
{% elif message.msgtype == "m.file" %}
|
|
||||||
{{ message.body_text_plain }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
View {{ room.title }} at {{ notif.link }}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<style type="text/css">
|
|
||||||
{% include 'mail.css' without context %}
|
|
||||||
{% include "mail-%s.css" % app_name ignore missing without context %}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<table id="page">
|
|
||||||
<tr>
|
|
||||||
<td> </td>
|
|
||||||
<td id="inner">
|
|
||||||
<table class="header">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="salutation">Hi {{ user_display_name }},</div>
|
|
||||||
<div class="summarytext">{{ summary_text }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="logo">
|
|
||||||
{% if app_name == "Riot" %}
|
|
||||||
<img src="http://matrix.org/img/riot-logo-email.png" width="83" height="83" alt="[Riot]"/>
|
|
||||||
{% elif app_name == "Vector" %}
|
|
||||||
<img src="http://matrix.org/img/vector-logo-email.png" width="64" height="83" alt="[Vector]"/>
|
|
||||||
{% else %}
|
|
||||||
<img src="http://matrix.org/img/matrix-120x51.png" width="120" height="51" alt="[matrix]"/>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
{% for room in rooms %}
|
|
||||||
{% include 'room.html' with context %}
|
|
||||||
{% endfor %}
|
|
||||||
<div class="footer">
|
|
||||||
<a href="{{ unsubscribe_link }}">Unsubscribe</a>
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
<div class="debug">
|
|
||||||
Sending email at {{ reason.now|format_ts("%c") }} due to activity in room {{ reason.room_name }} because
|
|
||||||
an event was received at {{ reason.received_at|format_ts("%c") }}
|
|
||||||
which is more than {{ "%.1f"|format(reason.delay_before_mail_ms / (60*1000)) }} ({{ reason.delay_before_mail_ms }}) mins ago,
|
|
||||||
{% if reason.last_sent_ts %}
|
|
||||||
and the last time we sent a mail for this room was {{ reason.last_sent_ts|format_ts("%c") }},
|
|
||||||
which is more than {{ "%.1f"|format(reason.throttle_ms / (60*1000)) }} (current throttle_ms) mins ago.
|
|
||||||
{% else %}
|
|
||||||
and we don't have a last time we sent a mail for this room.
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td> </td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
Hi {{ user_display_name }},
|
|
||||||
|
|
||||||
{{ summary_text }}
|
|
||||||
|
|
||||||
{% for room in rooms %}
|
|
||||||
{% include 'room.txt' with context %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
You can disable these notifications at {{ unsubscribe_link }}
|
|
||||||
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<table class="room">
|
|
||||||
<tr class="room_header">
|
|
||||||
<td class="room_avatar">
|
|
||||||
{% if room.avatar_url %}
|
|
||||||
<img alt="" src="{{ room.avatar_url|mxc_to_http(48,48) }}" />
|
|
||||||
{% else %}
|
|
||||||
{% if room.hash % 3 == 0 %}
|
|
||||||
<img alt="" src="https://vector.im/beta/img/76cfa6.png" />
|
|
||||||
{% elif room.hash % 3 == 1 %}
|
|
||||||
<img alt="" src="https://vector.im/beta/img/50e2c2.png" />
|
|
||||||
{% else %}
|
|
||||||
<img alt="" src="https://vector.im/beta/img/f4c371.png" />
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="room_name" colspan="2">
|
|
||||||
{{ room.title }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% if room.invite %}
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>
|
|
||||||
<a href="{{ room.link }}">Join the conversation.</a>
|
|
||||||
</td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
{% else %}
|
|
||||||
{% for notif in room.notifs %}
|
|
||||||
{% include 'notif.html' with context %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</table>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{{ room.title }}
|
|
||||||
|
|
||||||
{% if room.invite %}
|
|
||||||
You've been invited, join at {{ room.link }}
|
|
||||||
{% else %}
|
|
||||||
{% for notif in room.notifs %}
|
|
||||||
{% include 'notif.txt' with context %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
@@ -116,19 +116,17 @@ def get_json(origin_name, origin_key, destination, path):
|
|||||||
authorization_headers = []
|
authorization_headers = []
|
||||||
|
|
||||||
for key, sig in signed_json["signatures"][origin_name].items():
|
for key, sig in signed_json["signatures"][origin_name].items():
|
||||||
header = "X-Matrix origin=%s,key=\"%s\",sig=\"%s\"" % (
|
authorization_headers.append(bytes(
|
||||||
origin_name, key, sig,
|
"X-Matrix origin=%s,key=\"%s\",sig=\"%s\"" % (
|
||||||
)
|
origin_name, key, sig,
|
||||||
authorization_headers.append(bytes(header))
|
)
|
||||||
sys.stderr.write(header)
|
))
|
||||||
sys.stderr.write("\n")
|
|
||||||
|
|
||||||
result = requests.get(
|
result = requests.get(
|
||||||
lookup(destination, path),
|
lookup(destination, path),
|
||||||
headers={"Authorization": authorization_headers[0]},
|
headers={"Authorization": authorization_headers[0]},
|
||||||
verify=False,
|
verify=False,
|
||||||
)
|
)
|
||||||
sys.stderr.write("Status Code: %d\n" % (result.status_code,))
|
|
||||||
return result.json()
|
return result.json()
|
||||||
|
|
||||||
|
|
||||||
@@ -143,7 +141,6 @@ def main():
|
|||||||
)
|
)
|
||||||
|
|
||||||
json.dump(result, sys.stdout)
|
json.dump(result, sys.stdout)
|
||||||
print ""
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
import getpass
|
import getpass
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
bcrypt_rounds=12
|
bcrypt_rounds=12
|
||||||
password_pepper = ""
|
|
||||||
|
|
||||||
def prompt_for_pass():
|
def prompt_for_pass():
|
||||||
password = getpass.getpass("Password: ")
|
password = getpass.getpass("Password: ")
|
||||||
@@ -34,22 +28,12 @@ if __name__ == "__main__":
|
|||||||
default=None,
|
default=None,
|
||||||
help="New password for user. Will prompt if omitted.",
|
help="New password for user. Will prompt if omitted.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"-c", "--config",
|
|
||||||
type=argparse.FileType('r'),
|
|
||||||
help="Path to server config file. Used to read in bcrypt_rounds and password_pepper.",
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if "config" in args and args.config:
|
|
||||||
config = yaml.safe_load(args.config)
|
|
||||||
bcrypt_rounds = config.get("bcrypt_rounds", bcrypt_rounds)
|
|
||||||
password_config = config.get("password_config", {})
|
|
||||||
password_pepper = password_config.get("pepper", password_pepper)
|
|
||||||
password = args.password
|
password = args.password
|
||||||
|
|
||||||
if not password:
|
if not password:
|
||||||
password = prompt_for_pass()
|
password = prompt_for_pass()
|
||||||
|
|
||||||
print bcrypt.hashpw(password + password_pepper, bcrypt.gensalt(bcrypt_rounds))
|
print bcrypt.hashpw(password, bcrypt.gensalt(bcrypt_rounds))
|
||||||
|
|
||||||
|
|||||||
@@ -25,26 +25,18 @@ import urllib2
|
|||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
def request_registration(user, password, server_location, shared_secret, admin=False):
|
def request_registration(user, password, server_location, shared_secret):
|
||||||
mac = hmac.new(
|
mac = hmac.new(
|
||||||
key=shared_secret,
|
key=shared_secret,
|
||||||
|
msg=user,
|
||||||
digestmod=hashlib.sha1,
|
digestmod=hashlib.sha1,
|
||||||
)
|
).hexdigest()
|
||||||
|
|
||||||
mac.update(user)
|
|
||||||
mac.update("\x00")
|
|
||||||
mac.update(password)
|
|
||||||
mac.update("\x00")
|
|
||||||
mac.update("admin" if admin else "notadmin")
|
|
||||||
|
|
||||||
mac = mac.hexdigest()
|
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"user": user,
|
"user": user,
|
||||||
"password": password,
|
"password": password,
|
||||||
"mac": mac,
|
"mac": mac,
|
||||||
"type": "org.matrix.login.shared_secret",
|
"type": "org.matrix.login.shared_secret",
|
||||||
"admin": admin,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server_location = server_location.rstrip("/")
|
server_location = server_location.rstrip("/")
|
||||||
@@ -76,7 +68,7 @@ def request_registration(user, password, server_location, shared_secret, admin=F
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def register_new_user(user, password, server_location, shared_secret, admin):
|
def register_new_user(user, password, server_location, shared_secret):
|
||||||
if not user:
|
if not user:
|
||||||
try:
|
try:
|
||||||
default_user = getpass.getuser()
|
default_user = getpass.getuser()
|
||||||
@@ -107,14 +99,7 @@ def register_new_user(user, password, server_location, shared_secret, admin):
|
|||||||
print "Passwords do not match"
|
print "Passwords do not match"
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if not admin:
|
request_registration(user, password, server_location, shared_secret)
|
||||||
admin = raw_input("Make admin [no]: ")
|
|
||||||
if admin in ("y", "yes", "true"):
|
|
||||||
admin = True
|
|
||||||
else:
|
|
||||||
admin = False
|
|
||||||
|
|
||||||
request_registration(user, password, server_location, shared_secret, bool(admin))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -134,11 +119,6 @@ if __name__ == "__main__":
|
|||||||
default=None,
|
default=None,
|
||||||
help="New password for user. Will prompt if omitted.",
|
help="New password for user. Will prompt if omitted.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"-a", "--admin",
|
|
||||||
action="store_true",
|
|
||||||
help="Register new user as an admin. Will prompt if omitted.",
|
|
||||||
)
|
|
||||||
|
|
||||||
group = parser.add_mutually_exclusive_group(required=True)
|
group = parser.add_mutually_exclusive_group(required=True)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
@@ -171,4 +151,4 @@ if __name__ == "__main__":
|
|||||||
else:
|
else:
|
||||||
secret = args.shared_secret
|
secret = args.shared_secret
|
||||||
|
|
||||||
register_new_user(args.user, args.password, args.server_url, secret, args.admin)
|
register_new_user(args.user, args.password, args.server_url, secret)
|
||||||
|
|||||||
@@ -34,12 +34,11 @@ logger = logging.getLogger("synapse_port_db")
|
|||||||
|
|
||||||
|
|
||||||
BOOLEAN_COLUMNS = {
|
BOOLEAN_COLUMNS = {
|
||||||
"events": ["processed", "outlier", "contains_url"],
|
"events": ["processed", "outlier"],
|
||||||
"rooms": ["is_public"],
|
"rooms": ["is_public"],
|
||||||
"event_edges": ["is_state"],
|
"event_edges": ["is_state"],
|
||||||
"presence_list": ["accepted"],
|
"presence_list": ["accepted"],
|
||||||
"presence_stream": ["currently_active"],
|
"presence_stream": ["currently_active"],
|
||||||
"public_room_list_stream": ["visibility"],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -72,14 +71,6 @@ APPEND_ONLY_TABLES = [
|
|||||||
"event_to_state_groups",
|
"event_to_state_groups",
|
||||||
"rejections",
|
"rejections",
|
||||||
"event_search",
|
"event_search",
|
||||||
"presence_stream",
|
|
||||||
"push_rules_stream",
|
|
||||||
"current_state_resets",
|
|
||||||
"ex_outlier_stream",
|
|
||||||
"cache_invalidation_stream",
|
|
||||||
"public_room_list_stream",
|
|
||||||
"state_group_edges",
|
|
||||||
"stream_ordering_to_exterm",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -101,12 +92,8 @@ class Store(object):
|
|||||||
|
|
||||||
_simple_select_onecol_txn = SQLBaseStore.__dict__["_simple_select_onecol_txn"]
|
_simple_select_onecol_txn = SQLBaseStore.__dict__["_simple_select_onecol_txn"]
|
||||||
_simple_select_onecol = SQLBaseStore.__dict__["_simple_select_onecol"]
|
_simple_select_onecol = SQLBaseStore.__dict__["_simple_select_onecol"]
|
||||||
_simple_select_one = SQLBaseStore.__dict__["_simple_select_one"]
|
|
||||||
_simple_select_one_txn = SQLBaseStore.__dict__["_simple_select_one_txn"]
|
|
||||||
_simple_select_one_onecol = SQLBaseStore.__dict__["_simple_select_one_onecol"]
|
_simple_select_one_onecol = SQLBaseStore.__dict__["_simple_select_one_onecol"]
|
||||||
_simple_select_one_onecol_txn = SQLBaseStore.__dict__[
|
_simple_select_one_onecol_txn = SQLBaseStore.__dict__["_simple_select_one_onecol_txn"]
|
||||||
"_simple_select_one_onecol_txn"
|
|
||||||
]
|
|
||||||
|
|
||||||
_simple_update_one = SQLBaseStore.__dict__["_simple_update_one"]
|
_simple_update_one = SQLBaseStore.__dict__["_simple_update_one"]
|
||||||
_simple_update_one_txn = SQLBaseStore.__dict__["_simple_update_one_txn"]
|
_simple_update_one_txn = SQLBaseStore.__dict__["_simple_update_one_txn"]
|
||||||
@@ -171,40 +158,31 @@ class Porter(object):
|
|||||||
def setup_table(self, table):
|
def setup_table(self, table):
|
||||||
if table in APPEND_ONLY_TABLES:
|
if table in APPEND_ONLY_TABLES:
|
||||||
# It's safe to just carry on inserting.
|
# It's safe to just carry on inserting.
|
||||||
row = yield self.postgres_store._simple_select_one(
|
next_chunk = yield self.postgres_store._simple_select_one_onecol(
|
||||||
table="port_from_sqlite3",
|
table="port_from_sqlite3",
|
||||||
keyvalues={"table_name": table},
|
keyvalues={"table_name": table},
|
||||||
retcols=("forward_rowid", "backward_rowid"),
|
retcol="rowid",
|
||||||
allow_none=True,
|
allow_none=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
total_to_port = None
|
total_to_port = None
|
||||||
if row is None:
|
if next_chunk is None:
|
||||||
if table == "sent_transactions":
|
if table == "sent_transactions":
|
||||||
forward_chunk, already_ported, total_to_port = (
|
next_chunk, already_ported, total_to_port = (
|
||||||
yield self._setup_sent_transactions()
|
yield self._setup_sent_transactions()
|
||||||
)
|
)
|
||||||
backward_chunk = 0
|
|
||||||
else:
|
else:
|
||||||
yield self.postgres_store._simple_insert(
|
yield self.postgres_store._simple_insert(
|
||||||
table="port_from_sqlite3",
|
table="port_from_sqlite3",
|
||||||
values={
|
values={"table_name": table, "rowid": 1}
|
||||||
"table_name": table,
|
|
||||||
"forward_rowid": 1,
|
|
||||||
"backward_rowid": 0,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
forward_chunk = 1
|
next_chunk = 1
|
||||||
backward_chunk = 0
|
|
||||||
already_ported = 0
|
already_ported = 0
|
||||||
else:
|
|
||||||
forward_chunk = row["forward_rowid"]
|
|
||||||
backward_chunk = row["backward_rowid"]
|
|
||||||
|
|
||||||
if total_to_port is None:
|
if total_to_port is None:
|
||||||
already_ported, total_to_port = yield self._get_total_count_to_port(
|
already_ported, total_to_port = yield self._get_total_count_to_port(
|
||||||
table, forward_chunk, backward_chunk
|
table, next_chunk
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
def delete_all(txn):
|
def delete_all(txn):
|
||||||
@@ -218,124 +196,32 @@ class Porter(object):
|
|||||||
|
|
||||||
yield self.postgres_store._simple_insert(
|
yield self.postgres_store._simple_insert(
|
||||||
table="port_from_sqlite3",
|
table="port_from_sqlite3",
|
||||||
values={
|
values={"table_name": table, "rowid": 0}
|
||||||
"table_name": table,
|
|
||||||
"forward_rowid": 1,
|
|
||||||
"backward_rowid": 0,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
forward_chunk = 1
|
next_chunk = 1
|
||||||
backward_chunk = 0
|
|
||||||
|
|
||||||
already_ported, total_to_port = yield self._get_total_count_to_port(
|
already_ported, total_to_port = yield self._get_total_count_to_port(
|
||||||
table, forward_chunk, backward_chunk
|
table, next_chunk
|
||||||
)
|
)
|
||||||
|
|
||||||
defer.returnValue(
|
defer.returnValue((table, already_ported, total_to_port, next_chunk))
|
||||||
(table, already_ported, total_to_port, forward_chunk, backward_chunk)
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def handle_table(self, table, postgres_size, table_size, forward_chunk,
|
def handle_table(self, table, postgres_size, table_size, next_chunk):
|
||||||
backward_chunk):
|
|
||||||
if not table_size:
|
if not table_size:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.progress.add_table(table, postgres_size, table_size)
|
self.progress.add_table(table, postgres_size, table_size)
|
||||||
|
|
||||||
if table == "event_search":
|
select = (
|
||||||
yield self.handle_search_table(
|
|
||||||
postgres_size, table_size, forward_chunk, backward_chunk
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
forward_select = (
|
|
||||||
"SELECT rowid, * FROM %s WHERE rowid >= ? ORDER BY rowid LIMIT ?"
|
"SELECT rowid, * FROM %s WHERE rowid >= ? ORDER BY rowid LIMIT ?"
|
||||||
% (table,)
|
% (table,)
|
||||||
)
|
)
|
||||||
|
|
||||||
backward_select = (
|
|
||||||
"SELECT rowid, * FROM %s WHERE rowid <= ? ORDER BY rowid LIMIT ?"
|
|
||||||
% (table,)
|
|
||||||
)
|
|
||||||
|
|
||||||
do_forward = [True]
|
|
||||||
do_backward = [True]
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
def r(txn):
|
def r(txn):
|
||||||
forward_rows = []
|
txn.execute(select, (next_chunk, self.batch_size,))
|
||||||
backward_rows = []
|
|
||||||
if do_forward[0]:
|
|
||||||
txn.execute(forward_select, (forward_chunk, self.batch_size,))
|
|
||||||
forward_rows = txn.fetchall()
|
|
||||||
if not forward_rows:
|
|
||||||
do_forward[0] = False
|
|
||||||
|
|
||||||
if do_backward[0]:
|
|
||||||
txn.execute(backward_select, (backward_chunk, self.batch_size,))
|
|
||||||
backward_rows = txn.fetchall()
|
|
||||||
if not backward_rows:
|
|
||||||
do_backward[0] = False
|
|
||||||
|
|
||||||
if forward_rows or backward_rows:
|
|
||||||
headers = [column[0] for column in txn.description]
|
|
||||||
else:
|
|
||||||
headers = None
|
|
||||||
|
|
||||||
return headers, forward_rows, backward_rows
|
|
||||||
|
|
||||||
headers, frows, brows = yield self.sqlite_store.runInteraction(
|
|
||||||
"select", r
|
|
||||||
)
|
|
||||||
|
|
||||||
if frows or brows:
|
|
||||||
if frows:
|
|
||||||
forward_chunk = max(row[0] for row in frows) + 1
|
|
||||||
if brows:
|
|
||||||
backward_chunk = min(row[0] for row in brows) - 1
|
|
||||||
|
|
||||||
rows = frows + brows
|
|
||||||
self._convert_rows(table, headers, rows)
|
|
||||||
|
|
||||||
def insert(txn):
|
|
||||||
self.postgres_store.insert_many_txn(
|
|
||||||
txn, table, headers[1:], rows
|
|
||||||
)
|
|
||||||
|
|
||||||
self.postgres_store._simple_update_one_txn(
|
|
||||||
txn,
|
|
||||||
table="port_from_sqlite3",
|
|
||||||
keyvalues={"table_name": table},
|
|
||||||
updatevalues={
|
|
||||||
"forward_rowid": forward_chunk,
|
|
||||||
"backward_rowid": backward_chunk,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
yield self.postgres_store.execute(insert)
|
|
||||||
|
|
||||||
postgres_size += len(rows)
|
|
||||||
|
|
||||||
self.progress.update(table, postgres_size)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def handle_search_table(self, postgres_size, table_size, forward_chunk,
|
|
||||||
backward_chunk):
|
|
||||||
select = (
|
|
||||||
"SELECT es.rowid, es.*, e.origin_server_ts, e.stream_ordering"
|
|
||||||
" FROM event_search as es"
|
|
||||||
" INNER JOIN events AS e USING (event_id, room_id)"
|
|
||||||
" WHERE es.rowid >= ?"
|
|
||||||
" ORDER BY es.rowid LIMIT ?"
|
|
||||||
)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
def r(txn):
|
|
||||||
txn.execute(select, (forward_chunk, self.batch_size,))
|
|
||||||
rows = txn.fetchall()
|
rows = txn.fetchall()
|
||||||
headers = [column[0] for column in txn.description]
|
headers = [column[0] for column in txn.description]
|
||||||
|
|
||||||
@@ -344,51 +230,59 @@ class Porter(object):
|
|||||||
headers, rows = yield self.sqlite_store.runInteraction("select", r)
|
headers, rows = yield self.sqlite_store.runInteraction("select", r)
|
||||||
|
|
||||||
if rows:
|
if rows:
|
||||||
forward_chunk = rows[-1][0] + 1
|
next_chunk = rows[-1][0] + 1
|
||||||
|
|
||||||
# We have to treat event_search differently since it has a
|
if table == "event_search":
|
||||||
# different structure in the two different databases.
|
# We have to treat event_search differently since it has a
|
||||||
def insert(txn):
|
# different structure in the two different databases.
|
||||||
sql = (
|
def insert(txn):
|
||||||
"INSERT INTO event_search (event_id, room_id, key,"
|
sql = (
|
||||||
" sender, vector, origin_server_ts, stream_ordering)"
|
"INSERT INTO event_search (event_id, room_id, key, sender, vector)"
|
||||||
" VALUES (?,?,?,?,to_tsvector('english', ?),?,?)"
|
" VALUES (?,?,?,?,to_tsvector('english', ?))"
|
||||||
)
|
|
||||||
|
|
||||||
rows_dict = [
|
|
||||||
dict(zip(headers, row))
|
|
||||||
for row in rows
|
|
||||||
]
|
|
||||||
|
|
||||||
txn.executemany(sql, [
|
|
||||||
(
|
|
||||||
row["event_id"],
|
|
||||||
row["room_id"],
|
|
||||||
row["key"],
|
|
||||||
row["sender"],
|
|
||||||
row["value"],
|
|
||||||
row["origin_server_ts"],
|
|
||||||
row["stream_ordering"],
|
|
||||||
)
|
)
|
||||||
for row in rows_dict
|
|
||||||
])
|
|
||||||
|
|
||||||
self.postgres_store._simple_update_one_txn(
|
rows_dict = [
|
||||||
txn,
|
dict(zip(headers, row))
|
||||||
table="port_from_sqlite3",
|
for row in rows
|
||||||
keyvalues={"table_name": "event_search"},
|
]
|
||||||
updatevalues={
|
|
||||||
"forward_rowid": forward_chunk,
|
txn.executemany(sql, [
|
||||||
"backward_rowid": backward_chunk,
|
(
|
||||||
},
|
row["event_id"],
|
||||||
)
|
row["room_id"],
|
||||||
|
row["key"],
|
||||||
|
row["sender"],
|
||||||
|
row["value"],
|
||||||
|
)
|
||||||
|
for row in rows_dict
|
||||||
|
])
|
||||||
|
|
||||||
|
self.postgres_store._simple_update_one_txn(
|
||||||
|
txn,
|
||||||
|
table="port_from_sqlite3",
|
||||||
|
keyvalues={"table_name": table},
|
||||||
|
updatevalues={"rowid": next_chunk},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._convert_rows(table, headers, rows)
|
||||||
|
|
||||||
|
def insert(txn):
|
||||||
|
self.postgres_store.insert_many_txn(
|
||||||
|
txn, table, headers[1:], rows
|
||||||
|
)
|
||||||
|
|
||||||
|
self.postgres_store._simple_update_one_txn(
|
||||||
|
txn,
|
||||||
|
table="port_from_sqlite3",
|
||||||
|
keyvalues={"table_name": table},
|
||||||
|
updatevalues={"rowid": next_chunk},
|
||||||
|
)
|
||||||
|
|
||||||
yield self.postgres_store.execute(insert)
|
yield self.postgres_store.execute(insert)
|
||||||
|
|
||||||
postgres_size += len(rows)
|
postgres_size += len(rows)
|
||||||
|
|
||||||
self.progress.update("event_search", postgres_size)
|
self.progress.update(table, postgres_size)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -462,32 +356,10 @@ class Porter(object):
|
|||||||
txn.execute(
|
txn.execute(
|
||||||
"CREATE TABLE port_from_sqlite3 ("
|
"CREATE TABLE port_from_sqlite3 ("
|
||||||
" table_name varchar(100) NOT NULL UNIQUE,"
|
" table_name varchar(100) NOT NULL UNIQUE,"
|
||||||
" forward_rowid bigint NOT NULL,"
|
" rowid bigint NOT NULL"
|
||||||
" backward_rowid bigint NOT NULL"
|
|
||||||
")"
|
")"
|
||||||
)
|
)
|
||||||
|
|
||||||
# The old port script created a table with just a "rowid" column.
|
|
||||||
# We want people to be able to rerun this script from an old port
|
|
||||||
# so that they can pick up any missing events that were not
|
|
||||||
# ported across.
|
|
||||||
def alter_table(txn):
|
|
||||||
txn.execute(
|
|
||||||
"ALTER TABLE IF EXISTS port_from_sqlite3"
|
|
||||||
" RENAME rowid TO forward_rowid"
|
|
||||||
)
|
|
||||||
txn.execute(
|
|
||||||
"ALTER TABLE IF EXISTS port_from_sqlite3"
|
|
||||||
" ADD backward_rowid bigint NOT NULL DEFAULT 0"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
yield self.postgres_store.runInteraction(
|
|
||||||
"alter_table", alter_table
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.info("Failed to create port table: %s", e)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield self.postgres_store.runInteraction(
|
yield self.postgres_store.runInteraction(
|
||||||
"create_port_table", create_port_table
|
"create_port_table", create_port_table
|
||||||
@@ -547,7 +419,7 @@ class Porter(object):
|
|||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _setup_sent_transactions(self):
|
def _setup_sent_transactions(self):
|
||||||
# Only save things from the last day
|
# Only save things from the last day
|
||||||
yesterday = int(time.time() * 1000) - 86400000
|
yesterday = int(time.time()*1000) - 86400000
|
||||||
|
|
||||||
# And save the max transaction id from each destination
|
# And save the max transaction id from each destination
|
||||||
select = (
|
select = (
|
||||||
@@ -603,11 +475,7 @@ class Porter(object):
|
|||||||
|
|
||||||
yield self.postgres_store._simple_insert(
|
yield self.postgres_store._simple_insert(
|
||||||
table="port_from_sqlite3",
|
table="port_from_sqlite3",
|
||||||
values={
|
values={"table_name": "sent_transactions", "rowid": next_chunk}
|
||||||
"table_name": "sent_transactions",
|
|
||||||
"forward_rowid": next_chunk,
|
|
||||||
"backward_rowid": 0,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_sent_table_size(txn):
|
def get_sent_table_size(txn):
|
||||||
@@ -628,18 +496,13 @@ class Porter(object):
|
|||||||
defer.returnValue((next_chunk, inserted_rows, total_count))
|
defer.returnValue((next_chunk, inserted_rows, total_count))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _get_remaining_count_to_port(self, table, forward_chunk, backward_chunk):
|
def _get_remaining_count_to_port(self, table, next_chunk):
|
||||||
frows = yield self.sqlite_store.execute_sql(
|
rows = yield self.sqlite_store.execute_sql(
|
||||||
"SELECT count(*) FROM %s WHERE rowid >= ?" % (table,),
|
"SELECT count(*) FROM %s WHERE rowid >= ?" % (table,),
|
||||||
forward_chunk,
|
next_chunk,
|
||||||
)
|
)
|
||||||
|
|
||||||
brows = yield self.sqlite_store.execute_sql(
|
defer.returnValue(rows[0][0])
|
||||||
"SELECT count(*) FROM %s WHERE rowid <= ?" % (table,),
|
|
||||||
backward_chunk,
|
|
||||||
)
|
|
||||||
|
|
||||||
defer.returnValue(frows[0][0] + brows[0][0])
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _get_already_ported_count(self, table):
|
def _get_already_ported_count(self, table):
|
||||||
@@ -650,10 +513,10 @@ class Porter(object):
|
|||||||
defer.returnValue(rows[0][0])
|
defer.returnValue(rows[0][0])
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _get_total_count_to_port(self, table, forward_chunk, backward_chunk):
|
def _get_total_count_to_port(self, table, next_chunk):
|
||||||
remaining, done = yield defer.gatherResults(
|
remaining, done = yield defer.gatherResults(
|
||||||
[
|
[
|
||||||
self._get_remaining_count_to_port(table, forward_chunk, backward_chunk),
|
self._get_remaining_count_to_port(table, next_chunk),
|
||||||
self._get_already_ported_count(table),
|
self._get_already_ported_count(table),
|
||||||
],
|
],
|
||||||
consumeErrors=True,
|
consumeErrors=True,
|
||||||
@@ -784,7 +647,7 @@ class CursesProgress(Progress):
|
|||||||
color = curses.color_pair(2) if perc == 100 else curses.color_pair(1)
|
color = curses.color_pair(2) if perc == 100 else curses.color_pair(1)
|
||||||
|
|
||||||
self.stdscr.addstr(
|
self.stdscr.addstr(
|
||||||
i + 2, left_margin + max_len - len(table),
|
i+2, left_margin + max_len - len(table),
|
||||||
table,
|
table,
|
||||||
curses.A_BOLD | color,
|
curses.A_BOLD | color,
|
||||||
)
|
)
|
||||||
@@ -792,18 +655,18 @@ class CursesProgress(Progress):
|
|||||||
size = 20
|
size = 20
|
||||||
|
|
||||||
progress = "[%s%s]" % (
|
progress = "[%s%s]" % (
|
||||||
"#" * int(perc * size / 100),
|
"#" * int(perc*size/100),
|
||||||
" " * (size - int(perc * size / 100)),
|
" " * (size - int(perc*size/100)),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.stdscr.addstr(
|
self.stdscr.addstr(
|
||||||
i + 2, left_margin + max_len + middle_space,
|
i+2, left_margin + max_len + middle_space,
|
||||||
"%s %3d%% (%d/%d)" % (progress, perc, data["num_done"], data["total"]),
|
"%s %3d%% (%d/%d)" % (progress, perc, data["num_done"], data["total"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.finished:
|
if self.finished:
|
||||||
self.stdscr.addstr(
|
self.stdscr.addstr(
|
||||||
rows - 1, 0,
|
rows-1, 0,
|
||||||
"Press any key to exit...",
|
"Press any key to exit...",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,5 +16,7 @@ ignore =
|
|||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
max-line-length = 90
|
max-line-length = 90
|
||||||
# W503 requires that binary operators be at the end, not start, of lines. Erik doesn't like it.
|
ignore = W503 ; W503 requires that binary operators be at the end, not start, of lines. Erik doesn't like it.
|
||||||
ignore = W503
|
|
||||||
|
[pep8]
|
||||||
|
max-line-length = 90
|
||||||
|
|||||||
73
setup.py
73
setup.py
@@ -23,45 +23,6 @@ import sys
|
|||||||
here = os.path.abspath(os.path.dirname(__file__))
|
here = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
# Some notes on `setup.py test`:
|
|
||||||
#
|
|
||||||
# Once upon a time we used to try to make `setup.py test` run `tox` to run the
|
|
||||||
# tests. That's a bad idea for three reasons:
|
|
||||||
#
|
|
||||||
# 1: `setup.py test` is supposed to find out whether the tests work in the
|
|
||||||
# *current* environmentt, not whatever tox sets up.
|
|
||||||
# 2: Empirically, trying to install tox during the test run wasn't working ("No
|
|
||||||
# module named virtualenv").
|
|
||||||
# 3: The tox documentation advises against it[1].
|
|
||||||
#
|
|
||||||
# Even further back in time, we used to use setuptools_trial [2]. That has its
|
|
||||||
# own set of issues: for instance, it requires installation of Twisted to build
|
|
||||||
# an sdist (because the recommended mode of usage is to add it to
|
|
||||||
# `setup_requires`). That in turn means that in order to successfully run tox
|
|
||||||
# you have to have the python header files installed for whichever version of
|
|
||||||
# python tox uses (which is python3 on recent ubuntus, for example).
|
|
||||||
#
|
|
||||||
# So, for now at least, we stick with what appears to be the convention among
|
|
||||||
# Twisted projects, and don't attempt to do anything when someone runs
|
|
||||||
# `setup.py test`; instead we direct people to run `trial` directly if they
|
|
||||||
# care.
|
|
||||||
#
|
|
||||||
# [1]: http://tox.readthedocs.io/en/2.5.0/example/basic.html#integration-with-setup-py-test-command
|
|
||||||
# [2]: https://pypi.python.org/pypi/setuptools_trial
|
|
||||||
class TestCommand(Command):
|
|
||||||
user_options = []
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
print ("""Synapse's tests cannot be run via setup.py. To run them, try:
|
|
||||||
PYTHONPATH="." trial tests
|
|
||||||
""")
|
|
||||||
|
|
||||||
def read_file(path_segments):
|
def read_file(path_segments):
|
||||||
"""Read a file from the package. Takes a list of strings to join to
|
"""Read a file from the package. Takes a list of strings to join to
|
||||||
make the path"""
|
make the path"""
|
||||||
@@ -78,6 +39,38 @@ def exec_file(path_segments):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class Tox(Command):
|
||||||
|
user_options = [('tox-args=', 'a', "Arguments to pass to tox")]
|
||||||
|
|
||||||
|
def initialize_options(self):
|
||||||
|
self.tox_args = None
|
||||||
|
|
||||||
|
def finalize_options(self):
|
||||||
|
self.test_args = []
|
||||||
|
self.test_suite = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
#import here, cause outside the eggs aren't loaded
|
||||||
|
try:
|
||||||
|
import tox
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
self.distribution.fetch_build_eggs("tox")
|
||||||
|
import tox
|
||||||
|
except:
|
||||||
|
raise RuntimeError(
|
||||||
|
"The tests need 'tox' to run. Please install 'tox'."
|
||||||
|
)
|
||||||
|
import shlex
|
||||||
|
args = self.tox_args
|
||||||
|
if args:
|
||||||
|
args = shlex.split(self.tox_args)
|
||||||
|
else:
|
||||||
|
args = []
|
||||||
|
errno = tox.cmdline(args=args)
|
||||||
|
sys.exit(errno)
|
||||||
|
|
||||||
|
|
||||||
version = exec_file(("synapse", "__init__.py"))["__version__"]
|
version = exec_file(("synapse", "__init__.py"))["__version__"]
|
||||||
dependencies = exec_file(("synapse", "python_dependencies.py"))
|
dependencies = exec_file(("synapse", "python_dependencies.py"))
|
||||||
long_description = read_file(("README.rst",))
|
long_description = read_file(("README.rst",))
|
||||||
@@ -93,5 +86,5 @@ setup(
|
|||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
scripts=["synctl"] + glob.glob("scripts/*"),
|
scripts=["synctl"] + glob.glob("scripts/*"),
|
||||||
cmdclass={'test': TestCommand},
|
cmdclass={'test': Tox},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,4 +16,4 @@
|
|||||||
""" This is a reference implementation of a Matrix home server.
|
""" This is a reference implementation of a Matrix home server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.19.1"
|
__version__ = "0.14.0"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -85,8 +85,3 @@ class RoomCreationPreset(object):
|
|||||||
PRIVATE_CHAT = "private_chat"
|
PRIVATE_CHAT = "private_chat"
|
||||||
PUBLIC_CHAT = "public_chat"
|
PUBLIC_CHAT = "public_chat"
|
||||||
TRUSTED_PRIVATE_CHAT = "trusted_private_chat"
|
TRUSTED_PRIVATE_CHAT = "trusted_private_chat"
|
||||||
|
|
||||||
|
|
||||||
class ThirdPartyEntityKind(object):
|
|
||||||
USER = "user"
|
|
||||||
LOCATION = "location"
|
|
||||||
|
|||||||
@@ -39,14 +39,11 @@ class Codes(object):
|
|||||||
CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
|
CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
|
||||||
CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
|
CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
|
||||||
MISSING_PARAM = "M_MISSING_PARAM"
|
MISSING_PARAM = "M_MISSING_PARAM"
|
||||||
INVALID_PARAM = "M_INVALID_PARAM"
|
|
||||||
TOO_LARGE = "M_TOO_LARGE"
|
TOO_LARGE = "M_TOO_LARGE"
|
||||||
EXCLUSIVE = "M_EXCLUSIVE"
|
EXCLUSIVE = "M_EXCLUSIVE"
|
||||||
THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED"
|
THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED"
|
||||||
THREEPID_IN_USE = "M_THREEPID_IN_USE"
|
THREEPID_IN_USE = "THREEPID_IN_USE"
|
||||||
THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND"
|
|
||||||
INVALID_USERNAME = "M_INVALID_USERNAME"
|
INVALID_USERNAME = "M_INVALID_USERNAME"
|
||||||
SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED"
|
|
||||||
|
|
||||||
|
|
||||||
class CodeMessageException(RuntimeError):
|
class CodeMessageException(RuntimeError):
|
||||||
|
|||||||
@@ -15,9 +15,7 @@
|
|||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
from synapse.types import UserID, RoomID
|
from synapse.types import UserID, RoomID
|
||||||
|
|
||||||
from twisted.internet import defer
|
import json
|
||||||
|
|
||||||
import ujson as json
|
|
||||||
|
|
||||||
|
|
||||||
class Filtering(object):
|
class Filtering(object):
|
||||||
@@ -26,10 +24,10 @@ class Filtering(object):
|
|||||||
super(Filtering, self).__init__()
|
super(Filtering, self).__init__()
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def get_user_filter(self, user_localpart, filter_id):
|
def get_user_filter(self, user_localpart, filter_id):
|
||||||
result = yield self.store.get_user_filter(user_localpart, filter_id)
|
result = self.store.get_user_filter(user_localpart, filter_id)
|
||||||
defer.returnValue(FilterCollection(result))
|
result.addCallback(FilterCollection)
|
||||||
|
return result
|
||||||
|
|
||||||
def add_user_filter(self, user_localpart, user_filter):
|
def add_user_filter(self, user_localpart, user_filter):
|
||||||
self.check_valid_filter(user_filter)
|
self.check_valid_filter(user_filter)
|
||||||
@@ -71,21 +69,6 @@ class Filtering(object):
|
|||||||
if key in user_filter_json["room"]:
|
if key in user_filter_json["room"]:
|
||||||
self._check_definition(user_filter_json["room"][key])
|
self._check_definition(user_filter_json["room"][key])
|
||||||
|
|
||||||
if "event_fields" in user_filter_json:
|
|
||||||
if type(user_filter_json["event_fields"]) != list:
|
|
||||||
raise SynapseError(400, "event_fields must be a list of strings")
|
|
||||||
for field in user_filter_json["event_fields"]:
|
|
||||||
if not isinstance(field, basestring):
|
|
||||||
raise SynapseError(400, "Event field must be a string")
|
|
||||||
# Don't allow '\\' in event field filters. This makes matching
|
|
||||||
# events a lot easier as we can then use a negative lookbehind
|
|
||||||
# assertion to split '\.' If we allowed \\ then it would
|
|
||||||
# incorrectly split '\\.' See synapse.events.utils.serialize_event
|
|
||||||
if r'\\' in field:
|
|
||||||
raise SynapseError(
|
|
||||||
400, r'The escape character \ cannot itself be escaped'
|
|
||||||
)
|
|
||||||
|
|
||||||
def _check_definition_room_lists(self, definition):
|
def _check_definition_room_lists(self, definition):
|
||||||
"""Check that "rooms" and "not_rooms" are lists of room ids if they
|
"""Check that "rooms" and "not_rooms" are lists of room ids if they
|
||||||
are present
|
are present
|
||||||
@@ -167,7 +150,6 @@ class FilterCollection(object):
|
|||||||
self.include_leave = filter_json.get("room", {}).get(
|
self.include_leave = filter_json.get("room", {}).get(
|
||||||
"include_leave", False
|
"include_leave", False
|
||||||
)
|
)
|
||||||
self.event_fields = filter_json.get("event_fields", [])
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<FilterCollection %s>" % (json.dumps(self._filter_json),)
|
return "<FilterCollection %s>" % (json.dumps(self._filter_json),)
|
||||||
@@ -202,51 +184,11 @@ class FilterCollection(object):
|
|||||||
def filter_room_account_data(self, events):
|
def filter_room_account_data(self, events):
|
||||||
return self._room_account_data.filter(self._room_filter.filter(events))
|
return self._room_account_data.filter(self._room_filter.filter(events))
|
||||||
|
|
||||||
def blocks_all_presence(self):
|
|
||||||
return (
|
|
||||||
self._presence_filter.filters_all_types() or
|
|
||||||
self._presence_filter.filters_all_senders()
|
|
||||||
)
|
|
||||||
|
|
||||||
def blocks_all_room_ephemeral(self):
|
|
||||||
return (
|
|
||||||
self._room_ephemeral_filter.filters_all_types() or
|
|
||||||
self._room_ephemeral_filter.filters_all_senders() or
|
|
||||||
self._room_ephemeral_filter.filters_all_rooms()
|
|
||||||
)
|
|
||||||
|
|
||||||
def blocks_all_room_timeline(self):
|
|
||||||
return (
|
|
||||||
self._room_timeline_filter.filters_all_types() or
|
|
||||||
self._room_timeline_filter.filters_all_senders() or
|
|
||||||
self._room_timeline_filter.filters_all_rooms()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Filter(object):
|
class Filter(object):
|
||||||
def __init__(self, filter_json):
|
def __init__(self, filter_json):
|
||||||
self.filter_json = filter_json
|
self.filter_json = filter_json
|
||||||
|
|
||||||
self.types = self.filter_json.get("types", None)
|
|
||||||
self.not_types = self.filter_json.get("not_types", [])
|
|
||||||
|
|
||||||
self.rooms = self.filter_json.get("rooms", None)
|
|
||||||
self.not_rooms = self.filter_json.get("not_rooms", [])
|
|
||||||
|
|
||||||
self.senders = self.filter_json.get("senders", None)
|
|
||||||
self.not_senders = self.filter_json.get("not_senders", [])
|
|
||||||
|
|
||||||
self.contains_url = self.filter_json.get("contains_url", None)
|
|
||||||
|
|
||||||
def filters_all_types(self):
|
|
||||||
return "*" in self.not_types
|
|
||||||
|
|
||||||
def filters_all_senders(self):
|
|
||||||
return "*" in self.not_senders
|
|
||||||
|
|
||||||
def filters_all_rooms(self):
|
|
||||||
return "*" in self.not_rooms
|
|
||||||
|
|
||||||
def check(self, event):
|
def check(self, event):
|
||||||
"""Checks whether the filter matches the given event.
|
"""Checks whether the filter matches the given event.
|
||||||
|
|
||||||
@@ -265,10 +207,9 @@ class Filter(object):
|
|||||||
event.get("room_id", None),
|
event.get("room_id", None),
|
||||||
sender,
|
sender,
|
||||||
event.get("type", None),
|
event.get("type", None),
|
||||||
"url" in event.get("content", {})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def check_fields(self, room_id, sender, event_type, contains_url):
|
def check_fields(self, room_id, sender, event_type):
|
||||||
"""Checks whether the filter matches the given event fields.
|
"""Checks whether the filter matches the given event fields.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -282,20 +223,15 @@ class Filter(object):
|
|||||||
|
|
||||||
for name, match_func in literal_keys.items():
|
for name, match_func in literal_keys.items():
|
||||||
not_name = "not_%s" % (name,)
|
not_name = "not_%s" % (name,)
|
||||||
disallowed_values = getattr(self, not_name)
|
disallowed_values = self.filter_json.get(not_name, [])
|
||||||
if any(map(match_func, disallowed_values)):
|
if any(map(match_func, disallowed_values)):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
allowed_values = getattr(self, name)
|
allowed_values = self.filter_json.get(name, None)
|
||||||
if allowed_values is not None:
|
if allowed_values is not None:
|
||||||
if not any(map(match_func, allowed_values)):
|
if not any(map(match_func, allowed_values)):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
contains_url_filter = self.filter_json.get("contains_url")
|
|
||||||
if contains_url_filter is not None:
|
|
||||||
if contains_url_filter != contains_url:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def filter_rooms(self, room_ids):
|
def filter_rooms(self, room_ids):
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class Ratelimiter(object):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.message_counts = collections.OrderedDict()
|
self.message_counts = collections.OrderedDict()
|
||||||
|
|
||||||
def send_message(self, user_id, time_now_s, msg_rate_hz, burst_count, update=True):
|
def send_message(self, user_id, time_now_s, msg_rate_hz, burst_count):
|
||||||
"""Can the user send a message?
|
"""Can the user send a message?
|
||||||
Args:
|
Args:
|
||||||
user_id: The user sending a message.
|
user_id: The user sending a message.
|
||||||
@@ -32,15 +32,12 @@ class Ratelimiter(object):
|
|||||||
second.
|
second.
|
||||||
burst_count: How many messages the user can send before being
|
burst_count: How many messages the user can send before being
|
||||||
limited.
|
limited.
|
||||||
update (bool): Whether to update the message rates or not. This is
|
|
||||||
useful to check if a message would be allowed to be sent before
|
|
||||||
its ready to be actually sent.
|
|
||||||
Returns:
|
Returns:
|
||||||
A pair of a bool indicating if they can send a message now and a
|
A pair of a bool indicating if they can send a message now and a
|
||||||
time in seconds of when they can next send a message.
|
time in seconds of when they can next send a message.
|
||||||
"""
|
"""
|
||||||
self.prune_message_counts(time_now_s)
|
self.prune_message_counts(time_now_s)
|
||||||
message_count, time_start, _ignored = self.message_counts.get(
|
message_count, time_start, _ignored = self.message_counts.pop(
|
||||||
user_id, (0., time_now_s, None),
|
user_id, (0., time_now_s, None),
|
||||||
)
|
)
|
||||||
time_delta = time_now_s - time_start
|
time_delta = time_now_s - time_start
|
||||||
@@ -55,10 +52,9 @@ class Ratelimiter(object):
|
|||||||
allowed = True
|
allowed = True
|
||||||
message_count += 1
|
message_count += 1
|
||||||
|
|
||||||
if update:
|
self.message_counts[user_id] = (
|
||||||
self.message_counts[user_id] = (
|
message_count, time_start, msg_rate_hz
|
||||||
message_count, time_start, msg_rate_hz
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if msg_rate_hz > 0:
|
if msg_rate_hz > 0:
|
||||||
time_allowed = (
|
time_allowed = (
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ SERVER_KEY_PREFIX = "/_matrix/key/v1"
|
|||||||
SERVER_KEY_V2_PREFIX = "/_matrix/key/v2"
|
SERVER_KEY_V2_PREFIX = "/_matrix/key/v2"
|
||||||
MEDIA_PREFIX = "/_matrix/media/r0"
|
MEDIA_PREFIX = "/_matrix/media/r0"
|
||||||
LEGACY_MEDIA_PREFIX = "/_matrix/media/v1"
|
LEGACY_MEDIA_PREFIX = "/_matrix/media/v1"
|
||||||
|
APP_SERVICE_PREFIX = "/_matrix/appservice/v1"
|
||||||
|
|||||||
@@ -16,11 +16,13 @@
|
|||||||
import sys
|
import sys
|
||||||
sys.dont_write_bytecode = True
|
sys.dont_write_bytecode = True
|
||||||
|
|
||||||
from synapse import python_dependencies # noqa: E402
|
from synapse.python_dependencies import (
|
||||||
|
check_requirements, MissingRequirementError
|
||||||
|
) # NOQA
|
||||||
|
|
||||||
try:
|
try:
|
||||||
python_dependencies.check_requirements()
|
check_requirements()
|
||||||
except python_dependencies.MissingRequirementError as e:
|
except MissingRequirementError as e:
|
||||||
message = "\n".join([
|
message = "\n".join([
|
||||||
"Missing Requirement: %s" % (e.message,),
|
"Missing Requirement: %s" % (e.message,),
|
||||||
"To install run:",
|
"To install run:",
|
||||||
|
|||||||
@@ -1,220 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2016 OpenMarket Ltd
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import synapse
|
|
||||||
|
|
||||||
from synapse.server import HomeServer
|
|
||||||
from synapse.config._base import ConfigError
|
|
||||||
from synapse.config.logger import setup_logging
|
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
|
||||||
from synapse.http.site import SynapseSite
|
|
||||||
from synapse.metrics.resource import MetricsResource, METRICS_PREFIX
|
|
||||||
from synapse.replication.slave.storage.directory import DirectoryStore
|
|
||||||
from synapse.replication.slave.storage.events import SlavedEventStore
|
|
||||||
from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore
|
|
||||||
from synapse.replication.slave.storage.registration import SlavedRegistrationStore
|
|
||||||
from synapse.storage.engines import create_engine
|
|
||||||
from synapse.util.async import sleep
|
|
||||||
from synapse.util.httpresourcetree import create_resource_tree
|
|
||||||
from synapse.util.logcontext import LoggingContext
|
|
||||||
from synapse.util.manhole import manhole
|
|
||||||
from synapse.util.rlimit import change_resource_limit
|
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
|
|
||||||
from synapse import events
|
|
||||||
|
|
||||||
from twisted.internet import reactor, defer
|
|
||||||
from twisted.web.resource import Resource
|
|
||||||
|
|
||||||
from daemonize import Daemonize
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import gc
|
|
||||||
|
|
||||||
logger = logging.getLogger("synapse.app.appservice")
|
|
||||||
|
|
||||||
|
|
||||||
class AppserviceSlaveStore(
|
|
||||||
DirectoryStore, SlavedEventStore, SlavedApplicationServiceStore,
|
|
||||||
SlavedRegistrationStore,
|
|
||||||
):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AppserviceServer(HomeServer):
|
|
||||||
def get_db_conn(self, run_new_connection=True):
|
|
||||||
# Any param beginning with cp_ is a parameter for adbapi, and should
|
|
||||||
# not be passed to the database engine.
|
|
||||||
db_params = {
|
|
||||||
k: v for k, v in self.db_config.get("args", {}).items()
|
|
||||||
if not k.startswith("cp_")
|
|
||||||
}
|
|
||||||
db_conn = self.database_engine.module.connect(**db_params)
|
|
||||||
|
|
||||||
if run_new_connection:
|
|
||||||
self.database_engine.on_new_connection(db_conn)
|
|
||||||
return db_conn
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
logger.info("Setting up.")
|
|
||||||
self.datastore = AppserviceSlaveStore(self.get_db_conn(), self)
|
|
||||||
logger.info("Finished setting up.")
|
|
||||||
|
|
||||||
def _listen_http(self, listener_config):
|
|
||||||
port = listener_config["port"]
|
|
||||||
bind_addresses = listener_config["bind_addresses"]
|
|
||||||
site_tag = listener_config.get("tag", port)
|
|
||||||
resources = {}
|
|
||||||
for res in listener_config["resources"]:
|
|
||||||
for name in res["names"]:
|
|
||||||
if name == "metrics":
|
|
||||||
resources[METRICS_PREFIX] = MetricsResource(self)
|
|
||||||
|
|
||||||
root_resource = create_resource_tree(resources, Resource())
|
|
||||||
|
|
||||||
for address in bind_addresses:
|
|
||||||
reactor.listenTCP(
|
|
||||||
port,
|
|
||||||
SynapseSite(
|
|
||||||
"synapse.access.http.%s" % (site_tag,),
|
|
||||||
site_tag,
|
|
||||||
listener_config,
|
|
||||||
root_resource,
|
|
||||||
),
|
|
||||||
interface=address
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Synapse appservice now listening on port %d", port)
|
|
||||||
|
|
||||||
def start_listening(self, listeners):
|
|
||||||
for listener in listeners:
|
|
||||||
if listener["type"] == "http":
|
|
||||||
self._listen_http(listener)
|
|
||||||
elif listener["type"] == "manhole":
|
|
||||||
bind_addresses = listener["bind_addresses"]
|
|
||||||
|
|
||||||
for address in bind_addresses:
|
|
||||||
reactor.listenTCP(
|
|
||||||
listener["port"],
|
|
||||||
manhole(
|
|
||||||
username="matrix",
|
|
||||||
password="rabbithole",
|
|
||||||
globals={"hs": self},
|
|
||||||
),
|
|
||||||
interface=address
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warn("Unrecognized listener type: %s", listener["type"])
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def replicate(self):
|
|
||||||
http_client = self.get_simple_http_client()
|
|
||||||
store = self.get_datastore()
|
|
||||||
replication_url = self.config.worker_replication_url
|
|
||||||
appservice_handler = self.get_application_service_handler()
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def replicate(results):
|
|
||||||
stream = results.get("events")
|
|
||||||
if stream:
|
|
||||||
max_stream_id = stream["position"]
|
|
||||||
yield appservice_handler.notify_interested_services(max_stream_id)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
args = store.stream_positions()
|
|
||||||
args["timeout"] = 30000
|
|
||||||
result = yield http_client.get_json(replication_url, args=args)
|
|
||||||
yield store.process_replication(result)
|
|
||||||
replicate(result)
|
|
||||||
except:
|
|
||||||
logger.exception("Error replicating from %r", replication_url)
|
|
||||||
yield sleep(30)
|
|
||||||
|
|
||||||
|
|
||||||
def start(config_options):
|
|
||||||
try:
|
|
||||||
config = HomeServerConfig.load_config(
|
|
||||||
"Synapse appservice", config_options
|
|
||||||
)
|
|
||||||
except ConfigError as e:
|
|
||||||
sys.stderr.write("\n" + e.message + "\n")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
assert config.worker_app == "synapse.app.appservice"
|
|
||||||
|
|
||||||
setup_logging(config.worker_log_config, config.worker_log_file)
|
|
||||||
|
|
||||||
events.USE_FROZEN_DICTS = config.use_frozen_dicts
|
|
||||||
|
|
||||||
database_engine = create_engine(config.database_config)
|
|
||||||
|
|
||||||
if config.notify_appservices:
|
|
||||||
sys.stderr.write(
|
|
||||||
"\nThe appservices must be disabled in the main synapse process"
|
|
||||||
"\nbefore they can be run in a separate worker."
|
|
||||||
"\nPlease add ``notify_appservices: false`` to the main config"
|
|
||||||
"\n"
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Force the pushers to start since they will be disabled in the main config
|
|
||||||
config.notify_appservices = True
|
|
||||||
|
|
||||||
ps = AppserviceServer(
|
|
||||||
config.server_name,
|
|
||||||
db_config=config.database_config,
|
|
||||||
config=config,
|
|
||||||
version_string="Synapse/" + get_version_string(synapse),
|
|
||||||
database_engine=database_engine,
|
|
||||||
)
|
|
||||||
|
|
||||||
ps.setup()
|
|
||||||
ps.start_listening(config.worker_listeners)
|
|
||||||
|
|
||||||
def run():
|
|
||||||
with LoggingContext("run"):
|
|
||||||
logger.info("Running")
|
|
||||||
change_resource_limit(config.soft_file_limit)
|
|
||||||
if config.gc_thresholds:
|
|
||||||
gc.set_threshold(*config.gc_thresholds)
|
|
||||||
reactor.run()
|
|
||||||
|
|
||||||
def start():
|
|
||||||
ps.replicate()
|
|
||||||
ps.get_datastore().start_profiling()
|
|
||||||
ps.get_state_handler().start_caching()
|
|
||||||
|
|
||||||
reactor.callWhenRunning(start)
|
|
||||||
|
|
||||||
if config.worker_daemonize:
|
|
||||||
daemon = Daemonize(
|
|
||||||
app="synapse-appservice",
|
|
||||||
pid=config.worker_pid_file,
|
|
||||||
action=run,
|
|
||||||
auto_close_fds=False,
|
|
||||||
verbose=True,
|
|
||||||
logger=logger,
|
|
||||||
)
|
|
||||||
daemon.start()
|
|
||||||
else:
|
|
||||||
run()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
with LoggingContext("main"):
|
|
||||||
start(sys.argv[1:])
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2016 OpenMarket Ltd
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import synapse
|
|
||||||
|
|
||||||
from synapse.config._base import ConfigError
|
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
|
||||||
from synapse.config.logger import setup_logging
|
|
||||||
from synapse.http.site import SynapseSite
|
|
||||||
from synapse.http.server import JsonResource
|
|
||||||
from synapse.metrics.resource import MetricsResource, METRICS_PREFIX
|
|
||||||
from synapse.replication.slave.storage._base import BaseSlavedStore
|
|
||||||
from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore
|
|
||||||
from synapse.replication.slave.storage.events import SlavedEventStore
|
|
||||||
from synapse.replication.slave.storage.keys import SlavedKeyStore
|
|
||||||
from synapse.replication.slave.storage.room import RoomStore
|
|
||||||
from synapse.replication.slave.storage.directory import DirectoryStore
|
|
||||||
from synapse.replication.slave.storage.registration import SlavedRegistrationStore
|
|
||||||
from synapse.rest.client.v1.room import PublicRoomListRestServlet
|
|
||||||
from synapse.server import HomeServer
|
|
||||||
from synapse.storage.client_ips import ClientIpStore
|
|
||||||
from synapse.storage.engines import create_engine
|
|
||||||
from synapse.util.async import sleep
|
|
||||||
from synapse.util.httpresourcetree import create_resource_tree
|
|
||||||
from synapse.util.logcontext import LoggingContext
|
|
||||||
from synapse.util.manhole import manhole
|
|
||||||
from synapse.util.rlimit import change_resource_limit
|
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
from synapse.crypto import context_factory
|
|
||||||
|
|
||||||
from synapse import events
|
|
||||||
|
|
||||||
|
|
||||||
from twisted.internet import reactor, defer
|
|
||||||
from twisted.web.resource import Resource
|
|
||||||
|
|
||||||
from daemonize import Daemonize
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import gc
|
|
||||||
|
|
||||||
logger = logging.getLogger("synapse.app.client_reader")
|
|
||||||
|
|
||||||
|
|
||||||
class ClientReaderSlavedStore(
|
|
||||||
SlavedEventStore,
|
|
||||||
SlavedKeyStore,
|
|
||||||
RoomStore,
|
|
||||||
DirectoryStore,
|
|
||||||
SlavedApplicationServiceStore,
|
|
||||||
SlavedRegistrationStore,
|
|
||||||
BaseSlavedStore,
|
|
||||||
ClientIpStore, # After BaseSlavedStore because the constructor is different
|
|
||||||
):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ClientReaderServer(HomeServer):
|
|
||||||
def get_db_conn(self, run_new_connection=True):
|
|
||||||
# Any param beginning with cp_ is a parameter for adbapi, and should
|
|
||||||
# not be passed to the database engine.
|
|
||||||
db_params = {
|
|
||||||
k: v for k, v in self.db_config.get("args", {}).items()
|
|
||||||
if not k.startswith("cp_")
|
|
||||||
}
|
|
||||||
db_conn = self.database_engine.module.connect(**db_params)
|
|
||||||
|
|
||||||
if run_new_connection:
|
|
||||||
self.database_engine.on_new_connection(db_conn)
|
|
||||||
return db_conn
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
logger.info("Setting up.")
|
|
||||||
self.datastore = ClientReaderSlavedStore(self.get_db_conn(), self)
|
|
||||||
logger.info("Finished setting up.")
|
|
||||||
|
|
||||||
def _listen_http(self, listener_config):
|
|
||||||
port = listener_config["port"]
|
|
||||||
bind_addresses = listener_config["bind_addresses"]
|
|
||||||
site_tag = listener_config.get("tag", port)
|
|
||||||
resources = {}
|
|
||||||
for res in listener_config["resources"]:
|
|
||||||
for name in res["names"]:
|
|
||||||
if name == "metrics":
|
|
||||||
resources[METRICS_PREFIX] = MetricsResource(self)
|
|
||||||
elif name == "client":
|
|
||||||
resource = JsonResource(self, canonical_json=False)
|
|
||||||
PublicRoomListRestServlet(self).register(resource)
|
|
||||||
resources.update({
|
|
||||||
"/_matrix/client/r0": resource,
|
|
||||||
"/_matrix/client/unstable": resource,
|
|
||||||
"/_matrix/client/v2_alpha": resource,
|
|
||||||
"/_matrix/client/api/v1": resource,
|
|
||||||
})
|
|
||||||
|
|
||||||
root_resource = create_resource_tree(resources, Resource())
|
|
||||||
|
|
||||||
for address in bind_addresses:
|
|
||||||
reactor.listenTCP(
|
|
||||||
port,
|
|
||||||
SynapseSite(
|
|
||||||
"synapse.access.http.%s" % (site_tag,),
|
|
||||||
site_tag,
|
|
||||||
listener_config,
|
|
||||||
root_resource,
|
|
||||||
),
|
|
||||||
interface=address
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Synapse client reader now listening on port %d", port)
|
|
||||||
|
|
||||||
def start_listening(self, listeners):
|
|
||||||
for listener in listeners:
|
|
||||||
if listener["type"] == "http":
|
|
||||||
self._listen_http(listener)
|
|
||||||
elif listener["type"] == "manhole":
|
|
||||||
bind_addresses = listener["bind_addresses"]
|
|
||||||
|
|
||||||
for address in bind_addresses:
|
|
||||||
reactor.listenTCP(
|
|
||||||
listener["port"],
|
|
||||||
manhole(
|
|
||||||
username="matrix",
|
|
||||||
password="rabbithole",
|
|
||||||
globals={"hs": self},
|
|
||||||
),
|
|
||||||
interface=address
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warn("Unrecognized listener type: %s", listener["type"])
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def replicate(self):
|
|
||||||
http_client = self.get_simple_http_client()
|
|
||||||
store = self.get_datastore()
|
|
||||||
replication_url = self.config.worker_replication_url
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
args = store.stream_positions()
|
|
||||||
args["timeout"] = 30000
|
|
||||||
result = yield http_client.get_json(replication_url, args=args)
|
|
||||||
yield store.process_replication(result)
|
|
||||||
except:
|
|
||||||
logger.exception("Error replicating from %r", replication_url)
|
|
||||||
yield sleep(5)
|
|
||||||
|
|
||||||
|
|
||||||
def start(config_options):
|
|
||||||
try:
|
|
||||||
config = HomeServerConfig.load_config(
|
|
||||||
"Synapse client reader", config_options
|
|
||||||
)
|
|
||||||
except ConfigError as e:
|
|
||||||
sys.stderr.write("\n" + e.message + "\n")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
assert config.worker_app == "synapse.app.client_reader"
|
|
||||||
|
|
||||||
setup_logging(config.worker_log_config, config.worker_log_file)
|
|
||||||
|
|
||||||
events.USE_FROZEN_DICTS = config.use_frozen_dicts
|
|
||||||
|
|
||||||
database_engine = create_engine(config.database_config)
|
|
||||||
|
|
||||||
tls_server_context_factory = context_factory.ServerContextFactory(config)
|
|
||||||
|
|
||||||
ss = ClientReaderServer(
|
|
||||||
config.server_name,
|
|
||||||
db_config=config.database_config,
|
|
||||||
tls_server_context_factory=tls_server_context_factory,
|
|
||||||
config=config,
|
|
||||||
version_string="Synapse/" + get_version_string(synapse),
|
|
||||||
database_engine=database_engine,
|
|
||||||
)
|
|
||||||
|
|
||||||
ss.setup()
|
|
||||||
ss.get_handlers()
|
|
||||||
ss.start_listening(config.worker_listeners)
|
|
||||||
|
|
||||||
def run():
|
|
||||||
with LoggingContext("run"):
|
|
||||||
logger.info("Running")
|
|
||||||
change_resource_limit(config.soft_file_limit)
|
|
||||||
if config.gc_thresholds:
|
|
||||||
gc.set_threshold(*config.gc_thresholds)
|
|
||||||
reactor.run()
|
|
||||||
|
|
||||||
def start():
|
|
||||||
ss.get_state_handler().start_caching()
|
|
||||||
ss.get_datastore().start_profiling()
|
|
||||||
ss.replicate()
|
|
||||||
|
|
||||||
reactor.callWhenRunning(start)
|
|
||||||
|
|
||||||
if config.worker_daemonize:
|
|
||||||
daemon = Daemonize(
|
|
||||||
app="synapse-client-reader",
|
|
||||||
pid=config.worker_pid_file,
|
|
||||||
action=run,
|
|
||||||
auto_close_fds=False,
|
|
||||||
verbose=True,
|
|
||||||
logger=logger,
|
|
||||||
)
|
|
||||||
daemon.start()
|
|
||||||
else:
|
|
||||||
run()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
with LoggingContext("main"):
|
|
||||||
start(sys.argv[1:])
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2016 OpenMarket Ltd
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import synapse
|
|
||||||
|
|
||||||
from synapse.config._base import ConfigError
|
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
|
||||||
from synapse.config.logger import setup_logging
|
|
||||||
from synapse.http.site import SynapseSite
|
|
||||||
from synapse.metrics.resource import MetricsResource, METRICS_PREFIX
|
|
||||||
from synapse.replication.slave.storage._base import BaseSlavedStore
|
|
||||||
from synapse.replication.slave.storage.events import SlavedEventStore
|
|
||||||
from synapse.replication.slave.storage.keys import SlavedKeyStore
|
|
||||||
from synapse.replication.slave.storage.room import RoomStore
|
|
||||||
from synapse.replication.slave.storage.transactions import TransactionStore
|
|
||||||
from synapse.replication.slave.storage.directory import DirectoryStore
|
|
||||||
from synapse.server import HomeServer
|
|
||||||
from synapse.storage.engines import create_engine
|
|
||||||
from synapse.util.async import sleep
|
|
||||||
from synapse.util.httpresourcetree import create_resource_tree
|
|
||||||
from synapse.util.logcontext import LoggingContext
|
|
||||||
from synapse.util.manhole import manhole
|
|
||||||
from synapse.util.rlimit import change_resource_limit
|
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
from synapse.api.urls import FEDERATION_PREFIX
|
|
||||||
from synapse.federation.transport.server import TransportLayerServer
|
|
||||||
from synapse.crypto import context_factory
|
|
||||||
|
|
||||||
from synapse import events
|
|
||||||
|
|
||||||
|
|
||||||
from twisted.internet import reactor, defer
|
|
||||||
from twisted.web.resource import Resource
|
|
||||||
|
|
||||||
from daemonize import Daemonize
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import gc
|
|
||||||
|
|
||||||
logger = logging.getLogger("synapse.app.federation_reader")
|
|
||||||
|
|
||||||
|
|
||||||
class FederationReaderSlavedStore(
|
|
||||||
SlavedEventStore,
|
|
||||||
SlavedKeyStore,
|
|
||||||
RoomStore,
|
|
||||||
DirectoryStore,
|
|
||||||
TransactionStore,
|
|
||||||
BaseSlavedStore,
|
|
||||||
):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FederationReaderServer(HomeServer):
|
|
||||||
def get_db_conn(self, run_new_connection=True):
|
|
||||||
# Any param beginning with cp_ is a parameter for adbapi, and should
|
|
||||||
# not be passed to the database engine.
|
|
||||||
db_params = {
|
|
||||||
k: v for k, v in self.db_config.get("args", {}).items()
|
|
||||||
if not k.startswith("cp_")
|
|
||||||
}
|
|
||||||
db_conn = self.database_engine.module.connect(**db_params)
|
|
||||||
|
|
||||||
if run_new_connection:
|
|
||||||
self.database_engine.on_new_connection(db_conn)
|
|
||||||
return db_conn
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
logger.info("Setting up.")
|
|
||||||
self.datastore = FederationReaderSlavedStore(self.get_db_conn(), self)
|
|
||||||
logger.info("Finished setting up.")
|
|
||||||
|
|
||||||
def _listen_http(self, listener_config):
|
|
||||||
port = listener_config["port"]
|
|
||||||
bind_addresses = listener_config["bind_addresses"]
|
|
||||||
site_tag = listener_config.get("tag", port)
|
|
||||||
resources = {}
|
|
||||||
for res in listener_config["resources"]:
|
|
||||||
for name in res["names"]:
|
|
||||||
if name == "metrics":
|
|
||||||
resources[METRICS_PREFIX] = MetricsResource(self)
|
|
||||||
elif name == "federation":
|
|
||||||
resources.update({
|
|
||||||
FEDERATION_PREFIX: TransportLayerServer(self),
|
|
||||||
})
|
|
||||||
|
|
||||||
root_resource = create_resource_tree(resources, Resource())
|
|
||||||
|
|
||||||
for address in bind_addresses:
|
|
||||||
reactor.listenTCP(
|
|
||||||
port,
|
|
||||||
SynapseSite(
|
|
||||||
"synapse.access.http.%s" % (site_tag,),
|
|
||||||
site_tag,
|
|
||||||
listener_config,
|
|
||||||
root_resource,
|
|
||||||
),
|
|
||||||
interface=address
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Synapse federation reader now listening on port %d", port)
|
|
||||||
|
|
||||||
def start_listening(self, listeners):
|
|
||||||
for listener in listeners:
|
|
||||||
if listener["type"] == "http":
|
|
||||||
self._listen_http(listener)
|
|
||||||
elif listener["type"] == "manhole":
|
|
||||||
bind_addresses = listener["bind_addresses"]
|
|
||||||
|
|
||||||
for address in bind_addresses:
|
|
||||||
reactor.listenTCP(
|
|
||||||
listener["port"],
|
|
||||||
manhole(
|
|
||||||
username="matrix",
|
|
||||||
password="rabbithole",
|
|
||||||
globals={"hs": self},
|
|
||||||
),
|
|
||||||
interface=address
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warn("Unrecognized listener type: %s", listener["type"])
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def replicate(self):
|
|
||||||
http_client = self.get_simple_http_client()
|
|
||||||
store = self.get_datastore()
|
|
||||||
replication_url = self.config.worker_replication_url
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
args = store.stream_positions()
|
|
||||||
args["timeout"] = 30000
|
|
||||||
result = yield http_client.get_json(replication_url, args=args)
|
|
||||||
yield store.process_replication(result)
|
|
||||||
except:
|
|
||||||
logger.exception("Error replicating from %r", replication_url)
|
|
||||||
yield sleep(5)
|
|
||||||
|
|
||||||
|
|
||||||
def start(config_options):
|
|
||||||
try:
|
|
||||||
config = HomeServerConfig.load_config(
|
|
||||||
"Synapse federation reader", config_options
|
|
||||||
)
|
|
||||||
except ConfigError as e:
|
|
||||||
sys.stderr.write("\n" + e.message + "\n")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
assert config.worker_app == "synapse.app.federation_reader"
|
|
||||||
|
|
||||||
setup_logging(config.worker_log_config, config.worker_log_file)
|
|
||||||
|
|
||||||
events.USE_FROZEN_DICTS = config.use_frozen_dicts
|
|
||||||
|
|
||||||
database_engine = create_engine(config.database_config)
|
|
||||||
|
|
||||||
tls_server_context_factory = context_factory.ServerContextFactory(config)
|
|
||||||
|
|
||||||
ss = FederationReaderServer(
|
|
||||||
config.server_name,
|
|
||||||
db_config=config.database_config,
|
|
||||||
tls_server_context_factory=tls_server_context_factory,
|
|
||||||
config=config,
|
|
||||||
version_string="Synapse/" + get_version_string(synapse),
|
|
||||||
database_engine=database_engine,
|
|
||||||
)
|
|
||||||
|
|
||||||
ss.setup()
|
|
||||||
ss.get_handlers()
|
|
||||||
ss.start_listening(config.worker_listeners)
|
|
||||||
|
|
||||||
def run():
|
|
||||||
with LoggingContext("run"):
|
|
||||||
logger.info("Running")
|
|
||||||
change_resource_limit(config.soft_file_limit)
|
|
||||||
if config.gc_thresholds:
|
|
||||||
gc.set_threshold(*config.gc_thresholds)
|
|
||||||
reactor.run()
|
|
||||||
|
|
||||||
def start():
|
|
||||||
ss.get_state_handler().start_caching()
|
|
||||||
ss.get_datastore().start_profiling()
|
|
||||||
ss.replicate()
|
|
||||||
|
|
||||||
reactor.callWhenRunning(start)
|
|
||||||
|
|
||||||
if config.worker_daemonize:
|
|
||||||
daemon = Daemonize(
|
|
||||||
app="synapse-federation-reader",
|
|
||||||
pid=config.worker_pid_file,
|
|
||||||
action=run,
|
|
||||||
auto_close_fds=False,
|
|
||||||
verbose=True,
|
|
||||||
logger=logger,
|
|
||||||
)
|
|
||||||
daemon.start()
|
|
||||||
else:
|
|
||||||
run()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
with LoggingContext("main"):
|
|
||||||
start(sys.argv[1:])
|
|
||||||
@@ -1,338 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2016 OpenMarket Ltd
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import synapse
|
|
||||||
|
|
||||||
from synapse.server import HomeServer
|
|
||||||
from synapse.config._base import ConfigError
|
|
||||||
from synapse.config.logger import setup_logging
|
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
|
||||||
from synapse.crypto import context_factory
|
|
||||||
from synapse.http.site import SynapseSite
|
|
||||||
from synapse.federation import send_queue
|
|
||||||
from synapse.federation.units import Edu
|
|
||||||
from synapse.metrics.resource import MetricsResource, METRICS_PREFIX
|
|
||||||
from synapse.replication.slave.storage.deviceinbox import SlavedDeviceInboxStore
|
|
||||||
from synapse.replication.slave.storage.events import SlavedEventStore
|
|
||||||
from synapse.replication.slave.storage.receipts import SlavedReceiptsStore
|
|
||||||
from synapse.replication.slave.storage.registration import SlavedRegistrationStore
|
|
||||||
from synapse.replication.slave.storage.transactions import TransactionStore
|
|
||||||
from synapse.replication.slave.storage.devices import SlavedDeviceStore
|
|
||||||
from synapse.storage.engines import create_engine
|
|
||||||
from synapse.storage.presence import UserPresenceState
|
|
||||||
from synapse.util.async import sleep
|
|
||||||
from synapse.util.httpresourcetree import create_resource_tree
|
|
||||||
from synapse.util.logcontext import LoggingContext
|
|
||||||
from synapse.util.manhole import manhole
|
|
||||||
from synapse.util.rlimit import change_resource_limit
|
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
|
|
||||||
from synapse import events
|
|
||||||
|
|
||||||
from twisted.internet import reactor, defer
|
|
||||||
from twisted.web.resource import Resource
|
|
||||||
|
|
||||||
from daemonize import Daemonize
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import gc
|
|
||||||
import ujson as json
|
|
||||||
|
|
||||||
logger = logging.getLogger("synapse.app.appservice")
|
|
||||||
|
|
||||||
|
|
||||||
class FederationSenderSlaveStore(
|
|
||||||
SlavedDeviceInboxStore, TransactionStore, SlavedReceiptsStore, SlavedEventStore,
|
|
||||||
SlavedRegistrationStore, SlavedDeviceStore,
|
|
||||||
):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FederationSenderServer(HomeServer):
|
|
||||||
def get_db_conn(self, run_new_connection=True):
|
|
||||||
# Any param beginning with cp_ is a parameter for adbapi, and should
|
|
||||||
# not be passed to the database engine.
|
|
||||||
db_params = {
|
|
||||||
k: v for k, v in self.db_config.get("args", {}).items()
|
|
||||||
if not k.startswith("cp_")
|
|
||||||
}
|
|
||||||
db_conn = self.database_engine.module.connect(**db_params)
|
|
||||||
|
|
||||||
if run_new_connection:
|
|
||||||
self.database_engine.on_new_connection(db_conn)
|
|
||||||
return db_conn
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
logger.info("Setting up.")
|
|
||||||
self.datastore = FederationSenderSlaveStore(self.get_db_conn(), self)
|
|
||||||
logger.info("Finished setting up.")
|
|
||||||
|
|
||||||
def _listen_http(self, listener_config):
|
|
||||||
port = listener_config["port"]
|
|
||||||
bind_addresses = listener_config["bind_addresses"]
|
|
||||||
site_tag = listener_config.get("tag", port)
|
|
||||||
resources = {}
|
|
||||||
for res in listener_config["resources"]:
|
|
||||||
for name in res["names"]:
|
|
||||||
if name == "metrics":
|
|
||||||
resources[METRICS_PREFIX] = MetricsResource(self)
|
|
||||||
|
|
||||||
root_resource = create_resource_tree(resources, Resource())
|
|
||||||
|
|
||||||
for address in bind_addresses:
|
|
||||||
reactor.listenTCP(
|
|
||||||
port,
|
|
||||||
SynapseSite(
|
|
||||||
"synapse.access.http.%s" % (site_tag,),
|
|
||||||
site_tag,
|
|
||||||
listener_config,
|
|
||||||
root_resource,
|
|
||||||
),
|
|
||||||
interface=address
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Synapse federation_sender now listening on port %d", port)
|
|
||||||
|
|
||||||
def start_listening(self, listeners):
|
|
||||||
for listener in listeners:
|
|
||||||
if listener["type"] == "http":
|
|
||||||
self._listen_http(listener)
|
|
||||||
elif listener["type"] == "manhole":
|
|
||||||
bind_addresses = listener["bind_addresses"]
|
|
||||||
|
|
||||||
for address in bind_addresses:
|
|
||||||
reactor.listenTCP(
|
|
||||||
listener["port"],
|
|
||||||
manhole(
|
|
||||||
username="matrix",
|
|
||||||
password="rabbithole",
|
|
||||||
globals={"hs": self},
|
|
||||||
),
|
|
||||||
interface=address
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warn("Unrecognized listener type: %s", listener["type"])
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def replicate(self):
|
|
||||||
http_client = self.get_simple_http_client()
|
|
||||||
store = self.get_datastore()
|
|
||||||
replication_url = self.config.worker_replication_url
|
|
||||||
send_handler = FederationSenderHandler(self)
|
|
||||||
|
|
||||||
send_handler.on_start()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
args = store.stream_positions()
|
|
||||||
args.update((yield send_handler.stream_positions()))
|
|
||||||
args["timeout"] = 30000
|
|
||||||
result = yield http_client.get_json(replication_url, args=args)
|
|
||||||
yield store.process_replication(result)
|
|
||||||
yield send_handler.process_replication(result)
|
|
||||||
except:
|
|
||||||
logger.exception("Error replicating from %r", replication_url)
|
|
||||||
yield sleep(30)
|
|
||||||
|
|
||||||
|
|
||||||
def start(config_options):
|
|
||||||
try:
|
|
||||||
config = HomeServerConfig.load_config(
|
|
||||||
"Synapse federation sender", config_options
|
|
||||||
)
|
|
||||||
except ConfigError as e:
|
|
||||||
sys.stderr.write("\n" + e.message + "\n")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
assert config.worker_app == "synapse.app.federation_sender"
|
|
||||||
|
|
||||||
setup_logging(config.worker_log_config, config.worker_log_file)
|
|
||||||
|
|
||||||
events.USE_FROZEN_DICTS = config.use_frozen_dicts
|
|
||||||
|
|
||||||
database_engine = create_engine(config.database_config)
|
|
||||||
|
|
||||||
if config.send_federation:
|
|
||||||
sys.stderr.write(
|
|
||||||
"\nThe send_federation must be disabled in the main synapse process"
|
|
||||||
"\nbefore they can be run in a separate worker."
|
|
||||||
"\nPlease add ``send_federation: false`` to the main config"
|
|
||||||
"\n"
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Force the pushers to start since they will be disabled in the main config
|
|
||||||
config.send_federation = True
|
|
||||||
|
|
||||||
tls_server_context_factory = context_factory.ServerContextFactory(config)
|
|
||||||
|
|
||||||
ps = FederationSenderServer(
|
|
||||||
config.server_name,
|
|
||||||
db_config=config.database_config,
|
|
||||||
tls_server_context_factory=tls_server_context_factory,
|
|
||||||
config=config,
|
|
||||||
version_string="Synapse/" + get_version_string(synapse),
|
|
||||||
database_engine=database_engine,
|
|
||||||
)
|
|
||||||
|
|
||||||
ps.setup()
|
|
||||||
ps.start_listening(config.worker_listeners)
|
|
||||||
|
|
||||||
def run():
|
|
||||||
with LoggingContext("run"):
|
|
||||||
logger.info("Running")
|
|
||||||
change_resource_limit(config.soft_file_limit)
|
|
||||||
if config.gc_thresholds:
|
|
||||||
gc.set_threshold(*config.gc_thresholds)
|
|
||||||
reactor.run()
|
|
||||||
|
|
||||||
def start():
|
|
||||||
ps.replicate()
|
|
||||||
ps.get_datastore().start_profiling()
|
|
||||||
ps.get_state_handler().start_caching()
|
|
||||||
|
|
||||||
reactor.callWhenRunning(start)
|
|
||||||
|
|
||||||
if config.worker_daemonize:
|
|
||||||
daemon = Daemonize(
|
|
||||||
app="synapse-federation-sender",
|
|
||||||
pid=config.worker_pid_file,
|
|
||||||
action=run,
|
|
||||||
auto_close_fds=False,
|
|
||||||
verbose=True,
|
|
||||||
logger=logger,
|
|
||||||
)
|
|
||||||
daemon.start()
|
|
||||||
else:
|
|
||||||
run()
|
|
||||||
|
|
||||||
|
|
||||||
class FederationSenderHandler(object):
|
|
||||||
"""Processes the replication stream and forwards the appropriate entries
|
|
||||||
to the federation sender.
|
|
||||||
"""
|
|
||||||
def __init__(self, hs):
|
|
||||||
self.store = hs.get_datastore()
|
|
||||||
self.federation_sender = hs.get_federation_sender()
|
|
||||||
|
|
||||||
self._room_serials = {}
|
|
||||||
self._room_typing = {}
|
|
||||||
|
|
||||||
def on_start(self):
|
|
||||||
# There may be some events that are persisted but haven't been sent,
|
|
||||||
# so send them now.
|
|
||||||
self.federation_sender.notify_new_events(
|
|
||||||
self.store.get_room_max_stream_ordering()
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def stream_positions(self):
|
|
||||||
stream_id = yield self.store.get_federation_out_pos("federation")
|
|
||||||
defer.returnValue({
|
|
||||||
"federation": stream_id,
|
|
||||||
|
|
||||||
# Ack stuff we've "processed", this should only be called from
|
|
||||||
# one process.
|
|
||||||
"federation_ack": stream_id,
|
|
||||||
})
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def process_replication(self, result):
|
|
||||||
# The federation stream contains things that we want to send out, e.g.
|
|
||||||
# presence, typing, etc.
|
|
||||||
fed_stream = result.get("federation")
|
|
||||||
if fed_stream:
|
|
||||||
latest_id = int(fed_stream["position"])
|
|
||||||
|
|
||||||
# The federation stream containis a bunch of different types of
|
|
||||||
# rows that need to be handled differently. We parse the rows, put
|
|
||||||
# them into the appropriate collection and then send them off.
|
|
||||||
presence_to_send = {}
|
|
||||||
keyed_edus = {}
|
|
||||||
edus = {}
|
|
||||||
failures = {}
|
|
||||||
device_destinations = set()
|
|
||||||
|
|
||||||
# Parse the rows in the stream
|
|
||||||
for row in fed_stream["rows"]:
|
|
||||||
position, typ, content_js = row
|
|
||||||
content = json.loads(content_js)
|
|
||||||
|
|
||||||
if typ == send_queue.PRESENCE_TYPE:
|
|
||||||
destination = content["destination"]
|
|
||||||
state = UserPresenceState.from_dict(content["state"])
|
|
||||||
|
|
||||||
presence_to_send.setdefault(destination, []).append(state)
|
|
||||||
elif typ == send_queue.KEYED_EDU_TYPE:
|
|
||||||
key = content["key"]
|
|
||||||
edu = Edu(**content["edu"])
|
|
||||||
|
|
||||||
keyed_edus.setdefault(
|
|
||||||
edu.destination, {}
|
|
||||||
)[(edu.destination, tuple(key))] = edu
|
|
||||||
elif typ == send_queue.EDU_TYPE:
|
|
||||||
edu = Edu(**content)
|
|
||||||
|
|
||||||
edus.setdefault(edu.destination, []).append(edu)
|
|
||||||
elif typ == send_queue.FAILURE_TYPE:
|
|
||||||
destination = content["destination"]
|
|
||||||
failure = content["failure"]
|
|
||||||
|
|
||||||
failures.setdefault(destination, []).append(failure)
|
|
||||||
elif typ == send_queue.DEVICE_MESSAGE_TYPE:
|
|
||||||
device_destinations.add(content["destination"])
|
|
||||||
else:
|
|
||||||
raise Exception("Unrecognised federation type: %r", typ)
|
|
||||||
|
|
||||||
# We've finished collecting, send everything off
|
|
||||||
for destination, states in presence_to_send.items():
|
|
||||||
self.federation_sender.send_presence(destination, states)
|
|
||||||
|
|
||||||
for destination, edu_map in keyed_edus.items():
|
|
||||||
for key, edu in edu_map.items():
|
|
||||||
self.federation_sender.send_edu(
|
|
||||||
edu.destination, edu.edu_type, edu.content, key=key,
|
|
||||||
)
|
|
||||||
|
|
||||||
for destination, edu_list in edus.items():
|
|
||||||
for edu in edu_list:
|
|
||||||
self.federation_sender.send_edu(
|
|
||||||
edu.destination, edu.edu_type, edu.content, key=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
for destination, failure_list in failures.items():
|
|
||||||
for failure in failure_list:
|
|
||||||
self.federation_sender.send_failure(destination, failure)
|
|
||||||
|
|
||||||
for destination in device_destinations:
|
|
||||||
self.federation_sender.send_device_messages(destination)
|
|
||||||
|
|
||||||
# Record where we are in the stream.
|
|
||||||
yield self.store.update_federation_out_pos(
|
|
||||||
"federation", latest_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# We also need to poke the federation sender when new events happen
|
|
||||||
event_stream = result.get("events")
|
|
||||||
if event_stream:
|
|
||||||
latest_pos = event_stream["position"]
|
|
||||||
self.federation_sender.notify_new_events(latest_pos)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
with LoggingContext("main"):
|
|
||||||
start(sys.argv[1:])
|
|
||||||
@@ -16,10 +16,14 @@
|
|||||||
|
|
||||||
import synapse
|
import synapse
|
||||||
|
|
||||||
import gc
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
import resource
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
from synapse.config._base import ConfigError
|
from synapse.config._base import ConfigError
|
||||||
|
|
||||||
from synapse.python_dependencies import (
|
from synapse.python_dependencies import (
|
||||||
@@ -33,11 +37,18 @@ from synapse.storage.prepare_database import UpgradeDatabaseException, prepare_d
|
|||||||
|
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
|
|
||||||
|
|
||||||
|
from twisted.conch.manhole import ColoredManhole
|
||||||
|
from twisted.conch.insults import insults
|
||||||
|
from twisted.conch import manhole_ssh
|
||||||
|
from twisted.cred import checkers, portal
|
||||||
|
|
||||||
|
|
||||||
from twisted.internet import reactor, task, defer
|
from twisted.internet import reactor, task, defer
|
||||||
from twisted.application import service
|
from twisted.application import service
|
||||||
from twisted.web.resource import Resource, EncodingResourceWrapper
|
from twisted.web.resource import Resource, EncodingResourceWrapper
|
||||||
from twisted.web.static import File
|
from twisted.web.static import File
|
||||||
from twisted.web.server import GzipEncoderFactory
|
from twisted.web.server import Site, GzipEncoderFactory, Request
|
||||||
from synapse.http.server import RootRedirect
|
from synapse.http.server import RootRedirect
|
||||||
from synapse.rest.media.v0.content_repository import ContentRepoResource
|
from synapse.rest.media.v0.content_repository import ContentRepoResource
|
||||||
from synapse.rest.media.v1.media_repository import MediaRepositoryResource
|
from synapse.rest.media.v1.media_repository import MediaRepositoryResource
|
||||||
@@ -51,18 +62,10 @@ from synapse.api.urls import (
|
|||||||
from synapse.config.homeserver import HomeServerConfig
|
from synapse.config.homeserver import HomeServerConfig
|
||||||
from synapse.crypto import context_factory
|
from synapse.crypto import context_factory
|
||||||
from synapse.util.logcontext import LoggingContext
|
from synapse.util.logcontext import LoggingContext
|
||||||
from synapse.metrics import register_memory_metrics, get_metrics_for
|
|
||||||
from synapse.metrics.resource import MetricsResource, METRICS_PREFIX
|
from synapse.metrics.resource import MetricsResource, METRICS_PREFIX
|
||||||
from synapse.replication.resource import ReplicationResource, REPLICATION_PREFIX
|
from synapse.replication.resource import ReplicationResource, REPLICATION_PREFIX
|
||||||
from synapse.federation.transport.server import TransportLayerServer
|
from synapse.federation.transport.server import TransportLayerServer
|
||||||
|
|
||||||
from synapse.util.rlimit import change_resource_limit
|
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
from synapse.util.httpresourcetree import create_resource_tree
|
|
||||||
from synapse.util.manhole import manhole
|
|
||||||
|
|
||||||
from synapse.http.site import SynapseSite
|
|
||||||
|
|
||||||
from synapse import events
|
from synapse import events
|
||||||
|
|
||||||
from daemonize import Daemonize
|
from daemonize import Daemonize
|
||||||
@@ -70,6 +73,9 @@ from daemonize import Daemonize
|
|||||||
logger = logging.getLogger("synapse.app.homeserver")
|
logger = logging.getLogger("synapse.app.homeserver")
|
||||||
|
|
||||||
|
|
||||||
|
ACCESS_TOKEN_RE = re.compile(r'(\?.*access(_|%5[Ff])token=)[^&]*(.*)$')
|
||||||
|
|
||||||
|
|
||||||
def gz_wrap(r):
|
def gz_wrap(r):
|
||||||
return EncodingResourceWrapper(r, [GzipEncoderFactory()])
|
return EncodingResourceWrapper(r, [GzipEncoderFactory()])
|
||||||
|
|
||||||
@@ -107,7 +113,7 @@ def build_resource_for_web_client(hs):
|
|||||||
class SynapseHomeServer(HomeServer):
|
class SynapseHomeServer(HomeServer):
|
||||||
def _listener_http(self, config, listener_config):
|
def _listener_http(self, config, listener_config):
|
||||||
port = listener_config["port"]
|
port = listener_config["port"]
|
||||||
bind_addresses = listener_config["bind_addresses"]
|
bind_address = listener_config.get("bind_address", "")
|
||||||
tls = listener_config.get("tls", False)
|
tls = listener_config.get("tls", False)
|
||||||
site_tag = listener_config.get("tag", port)
|
site_tag = listener_config.get("tag", port)
|
||||||
|
|
||||||
@@ -148,7 +154,7 @@ class SynapseHomeServer(HomeServer):
|
|||||||
MEDIA_PREFIX: media_repo,
|
MEDIA_PREFIX: media_repo,
|
||||||
LEGACY_MEDIA_PREFIX: media_repo,
|
LEGACY_MEDIA_PREFIX: media_repo,
|
||||||
CONTENT_REPO_PREFIX: ContentRepoResource(
|
CONTENT_REPO_PREFIX: ContentRepoResource(
|
||||||
self, self.config.uploads_path
|
self, self.config.uploads_path, self.auth, self.content_addr
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -167,38 +173,30 @@ class SynapseHomeServer(HomeServer):
|
|||||||
if name == "replication":
|
if name == "replication":
|
||||||
resources[REPLICATION_PREFIX] = ReplicationResource(self)
|
resources[REPLICATION_PREFIX] = ReplicationResource(self)
|
||||||
|
|
||||||
if WEB_CLIENT_PREFIX in resources:
|
root_resource = create_resource_tree(resources)
|
||||||
root_resource = RootRedirect(WEB_CLIENT_PREFIX)
|
|
||||||
else:
|
|
||||||
root_resource = Resource()
|
|
||||||
|
|
||||||
root_resource = create_resource_tree(resources, root_resource)
|
|
||||||
|
|
||||||
if tls:
|
if tls:
|
||||||
for address in bind_addresses:
|
reactor.listenSSL(
|
||||||
reactor.listenSSL(
|
port,
|
||||||
port,
|
SynapseSite(
|
||||||
SynapseSite(
|
"synapse.access.https.%s" % (site_tag,),
|
||||||
"synapse.access.https.%s" % (site_tag,),
|
site_tag,
|
||||||
site_tag,
|
listener_config,
|
||||||
listener_config,
|
root_resource,
|
||||||
root_resource,
|
),
|
||||||
),
|
self.tls_server_context_factory,
|
||||||
self.tls_server_context_factory,
|
interface=bind_address
|
||||||
interface=address
|
)
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
for address in bind_addresses:
|
reactor.listenTCP(
|
||||||
reactor.listenTCP(
|
port,
|
||||||
port,
|
SynapseSite(
|
||||||
SynapseSite(
|
"synapse.access.http.%s" % (site_tag,),
|
||||||
"synapse.access.http.%s" % (site_tag,),
|
site_tag,
|
||||||
site_tag,
|
listener_config,
|
||||||
listener_config,
|
root_resource,
|
||||||
root_resource,
|
),
|
||||||
),
|
interface=bind_address
|
||||||
interface=address
|
)
|
||||||
)
|
|
||||||
logger.info("Synapse now listening on port %d", port)
|
logger.info("Synapse now listening on port %d", port)
|
||||||
|
|
||||||
def start_listening(self):
|
def start_listening(self):
|
||||||
@@ -208,18 +206,26 @@ class SynapseHomeServer(HomeServer):
|
|||||||
if listener["type"] == "http":
|
if listener["type"] == "http":
|
||||||
self._listener_http(config, listener)
|
self._listener_http(config, listener)
|
||||||
elif listener["type"] == "manhole":
|
elif listener["type"] == "manhole":
|
||||||
bind_addresses = listener["bind_addresses"]
|
checker = checkers.InMemoryUsernamePasswordDatabaseDontUse(
|
||||||
|
matrix="rabbithole"
|
||||||
|
)
|
||||||
|
|
||||||
for address in bind_addresses:
|
rlm = manhole_ssh.TerminalRealm()
|
||||||
reactor.listenTCP(
|
rlm.chainedProtocolFactory = lambda: insults.ServerProtocol(
|
||||||
listener["port"],
|
ColoredManhole,
|
||||||
manhole(
|
{
|
||||||
username="matrix",
|
"__name__": "__console__",
|
||||||
password="rabbithole",
|
"hs": self,
|
||||||
globals={"hs": self},
|
}
|
||||||
),
|
)
|
||||||
interface=address
|
|
||||||
)
|
f = manhole_ssh.ConchFactory(portal.Portal(rlm, [checker]))
|
||||||
|
|
||||||
|
reactor.listenTCP(
|
||||||
|
listener["port"],
|
||||||
|
f,
|
||||||
|
interface=listener.get("bind_address", '127.0.0.1')
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.warn("Unrecognized listener type: %s", listener["type"])
|
logger.warn("Unrecognized listener type: %s", listener["type"])
|
||||||
|
|
||||||
@@ -263,6 +269,86 @@ def quit_with_error(error_string):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_version_string():
|
||||||
|
try:
|
||||||
|
null = open(os.devnull, 'w')
|
||||||
|
cwd = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
try:
|
||||||
|
git_branch = subprocess.check_output(
|
||||||
|
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
|
||||||
|
stderr=null,
|
||||||
|
cwd=cwd,
|
||||||
|
).strip()
|
||||||
|
git_branch = "b=" + git_branch
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
git_branch = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
git_tag = subprocess.check_output(
|
||||||
|
['git', 'describe', '--exact-match'],
|
||||||
|
stderr=null,
|
||||||
|
cwd=cwd,
|
||||||
|
).strip()
|
||||||
|
git_tag = "t=" + git_tag
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
git_tag = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
git_commit = subprocess.check_output(
|
||||||
|
['git', 'rev-parse', '--short', 'HEAD'],
|
||||||
|
stderr=null,
|
||||||
|
cwd=cwd,
|
||||||
|
).strip()
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
git_commit = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
dirty_string = "-this_is_a_dirty_checkout"
|
||||||
|
is_dirty = subprocess.check_output(
|
||||||
|
['git', 'describe', '--dirty=' + dirty_string],
|
||||||
|
stderr=null,
|
||||||
|
cwd=cwd,
|
||||||
|
).strip().endswith(dirty_string)
|
||||||
|
|
||||||
|
git_dirty = "dirty" if is_dirty else ""
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
git_dirty = ""
|
||||||
|
|
||||||
|
if git_branch or git_tag or git_commit or git_dirty:
|
||||||
|
git_version = ",".join(
|
||||||
|
s for s in
|
||||||
|
(git_branch, git_tag, git_commit, git_dirty,)
|
||||||
|
if s
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
"Synapse/%s (%s)" % (
|
||||||
|
synapse.__version__, git_version,
|
||||||
|
)
|
||||||
|
).encode("ascii")
|
||||||
|
except Exception as e:
|
||||||
|
logger.info("Failed to check for git repository: %s", e)
|
||||||
|
|
||||||
|
return ("Synapse/%s" % (synapse.__version__,)).encode("ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def change_resource_limit(soft_file_no):
|
||||||
|
try:
|
||||||
|
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
|
||||||
|
|
||||||
|
if not soft_file_no:
|
||||||
|
soft_file_no = hard
|
||||||
|
|
||||||
|
resource.setrlimit(resource.RLIMIT_NOFILE, (soft_file_no, hard))
|
||||||
|
logger.info("Set file limit to: %d", soft_file_no)
|
||||||
|
|
||||||
|
resource.setrlimit(
|
||||||
|
resource.RLIMIT_CORE, (resource.RLIM_INFINITY, resource.RLIM_INFINITY)
|
||||||
|
)
|
||||||
|
except (ValueError, resource.error) as e:
|
||||||
|
logger.warn("Failed to set file or core limit: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def setup(config_options):
|
def setup(config_options):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
@@ -273,9 +359,10 @@ def setup(config_options):
|
|||||||
HomeServer
|
HomeServer
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
config = HomeServerConfig.load_or_generate_config(
|
config = HomeServerConfig.load_config(
|
||||||
"Synapse Homeserver",
|
"Synapse Homeserver",
|
||||||
config_options,
|
config_options,
|
||||||
|
generate_section="Homeserver"
|
||||||
)
|
)
|
||||||
except ConfigError as e:
|
except ConfigError as e:
|
||||||
sys.stderr.write("\n" + e.message + "\n")
|
sys.stderr.write("\n" + e.message + "\n")
|
||||||
@@ -291,7 +378,7 @@ def setup(config_options):
|
|||||||
# check any extra requirements we have now we have a config
|
# check any extra requirements we have now we have a config
|
||||||
check_requirements(config)
|
check_requirements(config)
|
||||||
|
|
||||||
version_string = "Synapse/" + get_version_string(synapse)
|
version_string = get_version_string()
|
||||||
|
|
||||||
logger.info("Server hostname: %s", config.server_name)
|
logger.info("Server hostname: %s", config.server_name)
|
||||||
logger.info("Server version: %s", version_string)
|
logger.info("Server version: %s", version_string)
|
||||||
@@ -308,6 +395,7 @@ def setup(config_options):
|
|||||||
db_config=config.database_config,
|
db_config=config.database_config,
|
||||||
tls_server_context_factory=tls_server_context_factory,
|
tls_server_context_factory=tls_server_context_factory,
|
||||||
config=config,
|
config=config,
|
||||||
|
content_addr=config.content_addr,
|
||||||
version_string=version_string,
|
version_string=version_string,
|
||||||
database_engine=database_engine,
|
database_engine=database_engine,
|
||||||
)
|
)
|
||||||
@@ -342,8 +430,6 @@ def setup(config_options):
|
|||||||
hs.get_datastore().start_doing_background_updates()
|
hs.get_datastore().start_doing_background_updates()
|
||||||
hs.get_replication_layer().start_get_pdu_cache()
|
hs.get_replication_layer().start_get_pdu_cache()
|
||||||
|
|
||||||
register_memory_metrics(hs)
|
|
||||||
|
|
||||||
reactor.callWhenRunning(start)
|
reactor.callWhenRunning(start)
|
||||||
|
|
||||||
return hs
|
return hs
|
||||||
@@ -359,13 +445,215 @@ class SynapseService(service.Service):
|
|||||||
def startService(self):
|
def startService(self):
|
||||||
hs = setup(self.config)
|
hs = setup(self.config)
|
||||||
change_resource_limit(hs.config.soft_file_limit)
|
change_resource_limit(hs.config.soft_file_limit)
|
||||||
if hs.config.gc_thresholds:
|
|
||||||
gc.set_threshold(*hs.config.gc_thresholds)
|
|
||||||
|
|
||||||
def stopService(self):
|
def stopService(self):
|
||||||
return self._port.stopListening()
|
return self._port.stopListening()
|
||||||
|
|
||||||
|
|
||||||
|
class SynapseRequest(Request):
|
||||||
|
def __init__(self, site, *args, **kw):
|
||||||
|
Request.__init__(self, *args, **kw)
|
||||||
|
self.site = site
|
||||||
|
self.authenticated_entity = None
|
||||||
|
self.start_time = 0
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
# We overwrite this so that we don't log ``access_token``
|
||||||
|
return '<%s at 0x%x method=%s uri=%s clientproto=%s site=%s>' % (
|
||||||
|
self.__class__.__name__,
|
||||||
|
id(self),
|
||||||
|
self.method,
|
||||||
|
self.get_redacted_uri(),
|
||||||
|
self.clientproto,
|
||||||
|
self.site.site_tag,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_redacted_uri(self):
|
||||||
|
return ACCESS_TOKEN_RE.sub(
|
||||||
|
r'\1<redacted>\3',
|
||||||
|
self.uri
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_user_agent(self):
|
||||||
|
return self.requestHeaders.getRawHeaders("User-Agent", [None])[-1]
|
||||||
|
|
||||||
|
def started_processing(self):
|
||||||
|
self.site.access_logger.info(
|
||||||
|
"%s - %s - Received request: %s %s",
|
||||||
|
self.getClientIP(),
|
||||||
|
self.site.site_tag,
|
||||||
|
self.method,
|
||||||
|
self.get_redacted_uri()
|
||||||
|
)
|
||||||
|
self.start_time = int(time.time() * 1000)
|
||||||
|
|
||||||
|
def finished_processing(self):
|
||||||
|
|
||||||
|
try:
|
||||||
|
context = LoggingContext.current_context()
|
||||||
|
ru_utime, ru_stime = context.get_resource_usage()
|
||||||
|
db_txn_count = context.db_txn_count
|
||||||
|
db_txn_duration = context.db_txn_duration
|
||||||
|
except:
|
||||||
|
ru_utime, ru_stime = (0, 0)
|
||||||
|
db_txn_count, db_txn_duration = (0, 0)
|
||||||
|
|
||||||
|
self.site.access_logger.info(
|
||||||
|
"%s - %s - {%s}"
|
||||||
|
" Processed request: %dms (%dms, %dms) (%dms/%d)"
|
||||||
|
" %sB %s \"%s %s %s\" \"%s\"",
|
||||||
|
self.getClientIP(),
|
||||||
|
self.site.site_tag,
|
||||||
|
self.authenticated_entity,
|
||||||
|
int(time.time() * 1000) - self.start_time,
|
||||||
|
int(ru_utime * 1000),
|
||||||
|
int(ru_stime * 1000),
|
||||||
|
int(db_txn_duration * 1000),
|
||||||
|
int(db_txn_count),
|
||||||
|
self.sentLength,
|
||||||
|
self.code,
|
||||||
|
self.method,
|
||||||
|
self.get_redacted_uri(),
|
||||||
|
self.clientproto,
|
||||||
|
self.get_user_agent(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def processing(self):
|
||||||
|
self.started_processing()
|
||||||
|
yield
|
||||||
|
self.finished_processing()
|
||||||
|
|
||||||
|
|
||||||
|
class XForwardedForRequest(SynapseRequest):
|
||||||
|
def __init__(self, *args, **kw):
|
||||||
|
SynapseRequest.__init__(self, *args, **kw)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Add a layer on top of another request that only uses the value of an
|
||||||
|
X-Forwarded-For header as the result of C{getClientIP}.
|
||||||
|
"""
|
||||||
|
def getClientIP(self):
|
||||||
|
"""
|
||||||
|
@return: The client address (the first address) in the value of the
|
||||||
|
I{X-Forwarded-For header}. If the header is not present, return
|
||||||
|
C{b"-"}.
|
||||||
|
"""
|
||||||
|
return self.requestHeaders.getRawHeaders(
|
||||||
|
b"x-forwarded-for", [b"-"])[0].split(b",")[0].strip()
|
||||||
|
|
||||||
|
|
||||||
|
class SynapseRequestFactory(object):
|
||||||
|
def __init__(self, site, x_forwarded_for):
|
||||||
|
self.site = site
|
||||||
|
self.x_forwarded_for = x_forwarded_for
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
if self.x_forwarded_for:
|
||||||
|
return XForwardedForRequest(self.site, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
return SynapseRequest(self.site, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SynapseSite(Site):
|
||||||
|
"""
|
||||||
|
Subclass of a twisted http Site that does access logging with python's
|
||||||
|
standard logging
|
||||||
|
"""
|
||||||
|
def __init__(self, logger_name, site_tag, config, resource, *args, **kwargs):
|
||||||
|
Site.__init__(self, resource, *args, **kwargs)
|
||||||
|
|
||||||
|
self.site_tag = site_tag
|
||||||
|
|
||||||
|
proxied = config.get("x_forwarded", False)
|
||||||
|
self.requestFactory = SynapseRequestFactory(self, proxied)
|
||||||
|
self.access_logger = logging.getLogger(logger_name)
|
||||||
|
|
||||||
|
def log(self, request):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def create_resource_tree(desired_tree, redirect_root_to_web_client=True):
|
||||||
|
"""Create the resource tree for this Home Server.
|
||||||
|
|
||||||
|
This in unduly complicated because Twisted does not support putting
|
||||||
|
child resources more than 1 level deep at a time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
web_client (bool): True to enable the web client.
|
||||||
|
redirect_root_to_web_client (bool): True to redirect '/' to the
|
||||||
|
location of the web client. This does nothing if web_client is not
|
||||||
|
True.
|
||||||
|
"""
|
||||||
|
if redirect_root_to_web_client and WEB_CLIENT_PREFIX in desired_tree:
|
||||||
|
root_resource = RootRedirect(WEB_CLIENT_PREFIX)
|
||||||
|
else:
|
||||||
|
root_resource = Resource()
|
||||||
|
|
||||||
|
# ideally we'd just use getChild and putChild but getChild doesn't work
|
||||||
|
# unless you give it a Request object IN ADDITION to the name :/ So
|
||||||
|
# instead, we'll store a copy of this mapping so we can actually add
|
||||||
|
# extra resources to existing nodes. See self._resource_id for the key.
|
||||||
|
resource_mappings = {}
|
||||||
|
for full_path, res in desired_tree.items():
|
||||||
|
logger.info("Attaching %s to path %s", res, full_path)
|
||||||
|
last_resource = root_resource
|
||||||
|
for path_seg in full_path.split('/')[1:-1]:
|
||||||
|
if path_seg not in last_resource.listNames():
|
||||||
|
# resource doesn't exist, so make a "dummy resource"
|
||||||
|
child_resource = Resource()
|
||||||
|
last_resource.putChild(path_seg, child_resource)
|
||||||
|
res_id = _resource_id(last_resource, path_seg)
|
||||||
|
resource_mappings[res_id] = child_resource
|
||||||
|
last_resource = child_resource
|
||||||
|
else:
|
||||||
|
# we have an existing Resource, use that instead.
|
||||||
|
res_id = _resource_id(last_resource, path_seg)
|
||||||
|
last_resource = resource_mappings[res_id]
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# now attach the actual desired resource
|
||||||
|
last_path_seg = full_path.split('/')[-1]
|
||||||
|
|
||||||
|
# if there is already a resource here, thieve its children and
|
||||||
|
# replace it
|
||||||
|
res_id = _resource_id(last_resource, last_path_seg)
|
||||||
|
if res_id in resource_mappings:
|
||||||
|
# there is a dummy resource at this path already, which needs
|
||||||
|
# to be replaced with the desired resource.
|
||||||
|
existing_dummy_resource = resource_mappings[res_id]
|
||||||
|
for child_name in existing_dummy_resource.listNames():
|
||||||
|
child_res_id = _resource_id(
|
||||||
|
existing_dummy_resource, child_name
|
||||||
|
)
|
||||||
|
child_resource = resource_mappings[child_res_id]
|
||||||
|
# steal the children
|
||||||
|
res.putChild(child_name, child_resource)
|
||||||
|
|
||||||
|
# finally, insert the desired resource in the right place
|
||||||
|
last_resource.putChild(last_path_seg, res)
|
||||||
|
res_id = _resource_id(last_resource, last_path_seg)
|
||||||
|
resource_mappings[res_id] = res
|
||||||
|
|
||||||
|
return root_resource
|
||||||
|
|
||||||
|
|
||||||
|
def _resource_id(resource, path_seg):
|
||||||
|
"""Construct an arbitrary resource ID so you can retrieve the mapping
|
||||||
|
later.
|
||||||
|
|
||||||
|
If you want to represent resource A putChild resource B with path C,
|
||||||
|
the mapping should looks like _resource_id(A,C) = B.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource (Resource): The *parent* Resourceb
|
||||||
|
path_seg (str): The name of the child Resource to be attached.
|
||||||
|
Returns:
|
||||||
|
str: A unique string which can be a key to the child Resource.
|
||||||
|
"""
|
||||||
|
return "%s-%s" % (resource, path_seg)
|
||||||
|
|
||||||
|
|
||||||
def run(hs):
|
def run(hs):
|
||||||
PROFILE_SYNAPSE = False
|
PROFILE_SYNAPSE = False
|
||||||
if PROFILE_SYNAPSE:
|
if PROFILE_SYNAPSE:
|
||||||
@@ -391,8 +679,6 @@ def run(hs):
|
|||||||
|
|
||||||
start_time = hs.get_clock().time()
|
start_time = hs.get_clock().time()
|
||||||
|
|
||||||
stats = {}
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def phone_stats_home():
|
def phone_stats_home():
|
||||||
logger.info("Gathering stats for reporting")
|
logger.info("Gathering stats for reporting")
|
||||||
@@ -401,10 +687,7 @@ def run(hs):
|
|||||||
if uptime < 0:
|
if uptime < 0:
|
||||||
uptime = 0
|
uptime = 0
|
||||||
|
|
||||||
# If the stats directory is empty then this is the first time we've
|
stats = {}
|
||||||
# reported stats.
|
|
||||||
first_time = not stats
|
|
||||||
|
|
||||||
stats["homeserver"] = hs.config.server_name
|
stats["homeserver"] = hs.config.server_name
|
||||||
stats["timestamp"] = now
|
stats["timestamp"] = now
|
||||||
stats["uptime_seconds"] = uptime
|
stats["uptime_seconds"] = uptime
|
||||||
@@ -417,25 +700,6 @@ def run(hs):
|
|||||||
daily_messages = yield hs.get_datastore().count_daily_messages()
|
daily_messages = yield hs.get_datastore().count_daily_messages()
|
||||||
if daily_messages is not None:
|
if daily_messages is not None:
|
||||||
stats["daily_messages"] = daily_messages
|
stats["daily_messages"] = daily_messages
|
||||||
else:
|
|
||||||
stats.pop("daily_messages", None)
|
|
||||||
|
|
||||||
if first_time:
|
|
||||||
# Add callbacks to report the synapse stats as metrics whenever
|
|
||||||
# prometheus requests them, typically every 30s.
|
|
||||||
# As some of the stats are expensive to calculate we only update
|
|
||||||
# them when synapse phones home to matrix.org every 24 hours.
|
|
||||||
metrics = get_metrics_for("synapse.usage")
|
|
||||||
metrics.add_callback("timestamp", lambda: stats["timestamp"])
|
|
||||||
metrics.add_callback("uptime_seconds", lambda: stats["uptime_seconds"])
|
|
||||||
metrics.add_callback("total_users", lambda: stats["total_users"])
|
|
||||||
metrics.add_callback("total_room_count", lambda: stats["total_room_count"])
|
|
||||||
metrics.add_callback(
|
|
||||||
"daily_active_users", lambda: stats["daily_active_users"]
|
|
||||||
)
|
|
||||||
metrics.add_callback(
|
|
||||||
"daily_messages", lambda: stats.get("daily_messages", 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Reporting stats to matrix.org: %s" % (stats,))
|
logger.info("Reporting stats to matrix.org: %s" % (stats,))
|
||||||
try:
|
try:
|
||||||
@@ -456,8 +720,6 @@ def run(hs):
|
|||||||
# sys.settrace(logcontext_tracer)
|
# sys.settrace(logcontext_tracer)
|
||||||
with LoggingContext("run"):
|
with LoggingContext("run"):
|
||||||
change_resource_limit(hs.config.soft_file_limit)
|
change_resource_limit(hs.config.soft_file_limit)
|
||||||
if hs.config.gc_thresholds:
|
|
||||||
gc.set_threshold(*hs.config.gc_thresholds)
|
|
||||||
reactor.run()
|
reactor.run()
|
||||||
|
|
||||||
if hs.config.daemonize:
|
if hs.config.daemonize:
|
||||||
|
|||||||
@@ -1,223 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2016 OpenMarket Ltd
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import synapse
|
|
||||||
|
|
||||||
from synapse.config._base import ConfigError
|
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
|
||||||
from synapse.config.logger import setup_logging
|
|
||||||
from synapse.http.site import SynapseSite
|
|
||||||
from synapse.metrics.resource import MetricsResource, METRICS_PREFIX
|
|
||||||
from synapse.replication.slave.storage._base import BaseSlavedStore
|
|
||||||
from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore
|
|
||||||
from synapse.replication.slave.storage.registration import SlavedRegistrationStore
|
|
||||||
from synapse.rest.media.v0.content_repository import ContentRepoResource
|
|
||||||
from synapse.rest.media.v1.media_repository import MediaRepositoryResource
|
|
||||||
from synapse.server import HomeServer
|
|
||||||
from synapse.storage.client_ips import ClientIpStore
|
|
||||||
from synapse.storage.engines import create_engine
|
|
||||||
from synapse.storage.media_repository import MediaRepositoryStore
|
|
||||||
from synapse.util.async import sleep
|
|
||||||
from synapse.util.httpresourcetree import create_resource_tree
|
|
||||||
from synapse.util.logcontext import LoggingContext
|
|
||||||
from synapse.util.manhole import manhole
|
|
||||||
from synapse.util.rlimit import change_resource_limit
|
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
from synapse.api.urls import (
|
|
||||||
CONTENT_REPO_PREFIX, LEGACY_MEDIA_PREFIX, MEDIA_PREFIX
|
|
||||||
)
|
|
||||||
from synapse.crypto import context_factory
|
|
||||||
|
|
||||||
from synapse import events
|
|
||||||
|
|
||||||
|
|
||||||
from twisted.internet import reactor, defer
|
|
||||||
from twisted.web.resource import Resource
|
|
||||||
|
|
||||||
from daemonize import Daemonize
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import gc
|
|
||||||
|
|
||||||
logger = logging.getLogger("synapse.app.media_repository")
|
|
||||||
|
|
||||||
|
|
||||||
class MediaRepositorySlavedStore(
|
|
||||||
SlavedApplicationServiceStore,
|
|
||||||
SlavedRegistrationStore,
|
|
||||||
BaseSlavedStore,
|
|
||||||
MediaRepositoryStore,
|
|
||||||
ClientIpStore,
|
|
||||||
):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MediaRepositoryServer(HomeServer):
|
|
||||||
def get_db_conn(self, run_new_connection=True):
|
|
||||||
# Any param beginning with cp_ is a parameter for adbapi, and should
|
|
||||||
# not be passed to the database engine.
|
|
||||||
db_params = {
|
|
||||||
k: v for k, v in self.db_config.get("args", {}).items()
|
|
||||||
if not k.startswith("cp_")
|
|
||||||
}
|
|
||||||
db_conn = self.database_engine.module.connect(**db_params)
|
|
||||||
|
|
||||||
if run_new_connection:
|
|
||||||
self.database_engine.on_new_connection(db_conn)
|
|
||||||
return db_conn
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
logger.info("Setting up.")
|
|
||||||
self.datastore = MediaRepositorySlavedStore(self.get_db_conn(), self)
|
|
||||||
logger.info("Finished setting up.")
|
|
||||||
|
|
||||||
def _listen_http(self, listener_config):
|
|
||||||
port = listener_config["port"]
|
|
||||||
bind_addresses = listener_config["bind_addresses"]
|
|
||||||
site_tag = listener_config.get("tag", port)
|
|
||||||
resources = {}
|
|
||||||
for res in listener_config["resources"]:
|
|
||||||
for name in res["names"]:
|
|
||||||
if name == "metrics":
|
|
||||||
resources[METRICS_PREFIX] = MetricsResource(self)
|
|
||||||
elif name == "media":
|
|
||||||
media_repo = MediaRepositoryResource(self)
|
|
||||||
resources.update({
|
|
||||||
MEDIA_PREFIX: media_repo,
|
|
||||||
LEGACY_MEDIA_PREFIX: media_repo,
|
|
||||||
CONTENT_REPO_PREFIX: ContentRepoResource(
|
|
||||||
self, self.config.uploads_path
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
root_resource = create_resource_tree(resources, Resource())
|
|
||||||
|
|
||||||
for address in bind_addresses:
|
|
||||||
reactor.listenTCP(
|
|
||||||
port,
|
|
||||||
SynapseSite(
|
|
||||||
"synapse.access.http.%s" % (site_tag,),
|
|
||||||
site_tag,
|
|
||||||
listener_config,
|
|
||||||
root_resource,
|
|
||||||
),
|
|
||||||
interface=address
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Synapse media repository now listening on port %d", port)
|
|
||||||
|
|
||||||
def start_listening(self, listeners):
|
|
||||||
for listener in listeners:
|
|
||||||
if listener["type"] == "http":
|
|
||||||
self._listen_http(listener)
|
|
||||||
elif listener["type"] == "manhole":
|
|
||||||
bind_addresses = listener["bind_addresses"]
|
|
||||||
|
|
||||||
for address in bind_addresses:
|
|
||||||
reactor.listenTCP(
|
|
||||||
listener["port"],
|
|
||||||
manhole(
|
|
||||||
username="matrix",
|
|
||||||
password="rabbithole",
|
|
||||||
globals={"hs": self},
|
|
||||||
),
|
|
||||||
interface=address
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warn("Unrecognized listener type: %s", listener["type"])
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def replicate(self):
|
|
||||||
http_client = self.get_simple_http_client()
|
|
||||||
store = self.get_datastore()
|
|
||||||
replication_url = self.config.worker_replication_url
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
args = store.stream_positions()
|
|
||||||
args["timeout"] = 30000
|
|
||||||
result = yield http_client.get_json(replication_url, args=args)
|
|
||||||
yield store.process_replication(result)
|
|
||||||
except:
|
|
||||||
logger.exception("Error replicating from %r", replication_url)
|
|
||||||
yield sleep(5)
|
|
||||||
|
|
||||||
|
|
||||||
def start(config_options):
|
|
||||||
try:
|
|
||||||
config = HomeServerConfig.load_config(
|
|
||||||
"Synapse media repository", config_options
|
|
||||||
)
|
|
||||||
except ConfigError as e:
|
|
||||||
sys.stderr.write("\n" + e.message + "\n")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
assert config.worker_app == "synapse.app.media_repository"
|
|
||||||
|
|
||||||
setup_logging(config.worker_log_config, config.worker_log_file)
|
|
||||||
|
|
||||||
events.USE_FROZEN_DICTS = config.use_frozen_dicts
|
|
||||||
|
|
||||||
database_engine = create_engine(config.database_config)
|
|
||||||
|
|
||||||
tls_server_context_factory = context_factory.ServerContextFactory(config)
|
|
||||||
|
|
||||||
ss = MediaRepositoryServer(
|
|
||||||
config.server_name,
|
|
||||||
db_config=config.database_config,
|
|
||||||
tls_server_context_factory=tls_server_context_factory,
|
|
||||||
config=config,
|
|
||||||
version_string="Synapse/" + get_version_string(synapse),
|
|
||||||
database_engine=database_engine,
|
|
||||||
)
|
|
||||||
|
|
||||||
ss.setup()
|
|
||||||
ss.get_handlers()
|
|
||||||
ss.start_listening(config.worker_listeners)
|
|
||||||
|
|
||||||
def run():
|
|
||||||
with LoggingContext("run"):
|
|
||||||
logger.info("Running")
|
|
||||||
change_resource_limit(config.soft_file_limit)
|
|
||||||
if config.gc_thresholds:
|
|
||||||
gc.set_threshold(*config.gc_thresholds)
|
|
||||||
reactor.run()
|
|
||||||
|
|
||||||
def start():
|
|
||||||
ss.get_state_handler().start_caching()
|
|
||||||
ss.get_datastore().start_profiling()
|
|
||||||
ss.replicate()
|
|
||||||
|
|
||||||
reactor.callWhenRunning(start)
|
|
||||||
|
|
||||||
if config.worker_daemonize:
|
|
||||||
daemon = Daemonize(
|
|
||||||
app="synapse-media-repository",
|
|
||||||
pid=config.worker_pid_file,
|
|
||||||
action=run,
|
|
||||||
auto_close_fds=False,
|
|
||||||
verbose=True,
|
|
||||||
logger=logger,
|
|
||||||
)
|
|
||||||
daemon.start()
|
|
||||||
else:
|
|
||||||
run()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
with LoggingContext("main"):
|
|
||||||
start(sys.argv[1:])
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2016 OpenMarket Ltd
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import synapse
|
|
||||||
|
|
||||||
from synapse.server import HomeServer
|
|
||||||
from synapse.config._base import ConfigError
|
|
||||||
from synapse.config.logger import setup_logging
|
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
|
||||||
from synapse.http.site import SynapseSite
|
|
||||||
from synapse.metrics.resource import MetricsResource, METRICS_PREFIX
|
|
||||||
from synapse.storage.roommember import RoomMemberStore
|
|
||||||
from synapse.replication.slave.storage.events import SlavedEventStore
|
|
||||||
from synapse.replication.slave.storage.pushers import SlavedPusherStore
|
|
||||||
from synapse.replication.slave.storage.receipts import SlavedReceiptsStore
|
|
||||||
from synapse.replication.slave.storage.account_data import SlavedAccountDataStore
|
|
||||||
from synapse.storage.engines import create_engine
|
|
||||||
from synapse.storage import DataStore
|
|
||||||
from synapse.util.async import sleep
|
|
||||||
from synapse.util.httpresourcetree import create_resource_tree
|
|
||||||
from synapse.util.logcontext import LoggingContext, preserve_fn
|
|
||||||
from synapse.util.manhole import manhole
|
|
||||||
from synapse.util.rlimit import change_resource_limit
|
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
|
|
||||||
from synapse import events
|
|
||||||
|
|
||||||
from twisted.internet import reactor, defer
|
|
||||||
from twisted.web.resource import Resource
|
|
||||||
|
|
||||||
from daemonize import Daemonize
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import gc
|
|
||||||
|
|
||||||
logger = logging.getLogger("synapse.app.pusher")
|
|
||||||
|
|
||||||
|
|
||||||
class PusherSlaveStore(
|
|
||||||
SlavedEventStore, SlavedPusherStore, SlavedReceiptsStore,
|
|
||||||
SlavedAccountDataStore
|
|
||||||
):
|
|
||||||
update_pusher_last_stream_ordering_and_success = (
|
|
||||||
DataStore.update_pusher_last_stream_ordering_and_success.__func__
|
|
||||||
)
|
|
||||||
|
|
||||||
update_pusher_failing_since = (
|
|
||||||
DataStore.update_pusher_failing_since.__func__
|
|
||||||
)
|
|
||||||
|
|
||||||
update_pusher_last_stream_ordering = (
|
|
||||||
DataStore.update_pusher_last_stream_ordering.__func__
|
|
||||||
)
|
|
||||||
|
|
||||||
get_throttle_params_by_room = (
|
|
||||||
DataStore.get_throttle_params_by_room.__func__
|
|
||||||
)
|
|
||||||
|
|
||||||
set_throttle_params = (
|
|
||||||
DataStore.set_throttle_params.__func__
|
|
||||||
)
|
|
||||||
|
|
||||||
get_time_of_last_push_action_before = (
|
|
||||||
DataStore.get_time_of_last_push_action_before.__func__
|
|
||||||
)
|
|
||||||
|
|
||||||
get_profile_displayname = (
|
|
||||||
DataStore.get_profile_displayname.__func__
|
|
||||||
)
|
|
||||||
|
|
||||||
who_forgot_in_room = (
|
|
||||||
RoomMemberStore.__dict__["who_forgot_in_room"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PusherServer(HomeServer):
|
|
||||||
|
|
||||||
def get_db_conn(self, run_new_connection=True):
|
|
||||||
# Any param beginning with cp_ is a parameter for adbapi, and should
|
|
||||||
# not be passed to the database engine.
|
|
||||||
db_params = {
|
|
||||||
k: v for k, v in self.db_config.get("args", {}).items()
|
|
||||||
if not k.startswith("cp_")
|
|
||||||
}
|
|
||||||
db_conn = self.database_engine.module.connect(**db_params)
|
|
||||||
|
|
||||||
if run_new_connection:
|
|
||||||
self.database_engine.on_new_connection(db_conn)
|
|
||||||
return db_conn
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
logger.info("Setting up.")
|
|
||||||
self.datastore = PusherSlaveStore(self.get_db_conn(), self)
|
|
||||||
logger.info("Finished setting up.")
|
|
||||||
|
|
||||||
def remove_pusher(self, app_id, push_key, user_id):
|
|
||||||
http_client = self.get_simple_http_client()
|
|
||||||
replication_url = self.config.worker_replication_url
|
|
||||||
url = replication_url + "/remove_pushers"
|
|
||||||
return http_client.post_json_get_json(url, {
|
|
||||||
"remove": [{
|
|
||||||
"app_id": app_id,
|
|
||||||
"push_key": push_key,
|
|
||||||
"user_id": user_id,
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
|
|
||||||
def _listen_http(self, listener_config):
|
|
||||||
port = listener_config["port"]
|
|
||||||
bind_addresses = listener_config["bind_addresses"]
|
|
||||||
site_tag = listener_config.get("tag", port)
|
|
||||||
resources = {}
|
|
||||||
for res in listener_config["resources"]:
|
|
||||||
for name in res["names"]:
|
|
||||||
if name == "metrics":
|
|
||||||
resources[METRICS_PREFIX] = MetricsResource(self)
|
|
||||||
|
|
||||||
root_resource = create_resource_tree(resources, Resource())
|
|
||||||
|
|
||||||
for address in bind_addresses:
|
|
||||||
reactor.listenTCP(
|
|
||||||
port,
|
|
||||||
SynapseSite(
|
|
||||||
"synapse.access.http.%s" % (site_tag,),
|
|
||||||
site_tag,
|
|
||||||
listener_config,
|
|
||||||
root_resource,
|
|
||||||
),
|
|
||||||
interface=address
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Synapse pusher now listening on port %d", port)
|
|
||||||
|
|
||||||
def start_listening(self, listeners):
|
|
||||||
for listener in listeners:
|
|
||||||
if listener["type"] == "http":
|
|
||||||
self._listen_http(listener)
|
|
||||||
elif listener["type"] == "manhole":
|
|
||||||
bind_addresses = listener["bind_addresses"]
|
|
||||||
|
|
||||||
for address in bind_addresses:
|
|
||||||
reactor.listenTCP(
|
|
||||||
listener["port"],
|
|
||||||
manhole(
|
|
||||||
username="matrix",
|
|
||||||
password="rabbithole",
|
|
||||||
globals={"hs": self},
|
|
||||||
),
|
|
||||||
interface=address
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warn("Unrecognized listener type: %s", listener["type"])
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def replicate(self):
|
|
||||||
http_client = self.get_simple_http_client()
|
|
||||||
store = self.get_datastore()
|
|
||||||
replication_url = self.config.worker_replication_url
|
|
||||||
pusher_pool = self.get_pusherpool()
|
|
||||||
|
|
||||||
def stop_pusher(user_id, app_id, pushkey):
|
|
||||||
key = "%s:%s" % (app_id, pushkey)
|
|
||||||
pushers_for_user = pusher_pool.pushers.get(user_id, {})
|
|
||||||
pusher = pushers_for_user.pop(key, None)
|
|
||||||
if pusher is None:
|
|
||||||
return
|
|
||||||
logger.info("Stopping pusher %r / %r", user_id, key)
|
|
||||||
pusher.on_stop()
|
|
||||||
|
|
||||||
def start_pusher(user_id, app_id, pushkey):
|
|
||||||
key = "%s:%s" % (app_id, pushkey)
|
|
||||||
logger.info("Starting pusher %r / %r", user_id, key)
|
|
||||||
return pusher_pool._refresh_pusher(app_id, pushkey, user_id)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def poke_pushers(results):
|
|
||||||
pushers_rows = set(
|
|
||||||
map(tuple, results.get("pushers", {}).get("rows", []))
|
|
||||||
)
|
|
||||||
deleted_pushers_rows = set(
|
|
||||||
map(tuple, results.get("deleted_pushers", {}).get("rows", []))
|
|
||||||
)
|
|
||||||
for row in sorted(pushers_rows | deleted_pushers_rows):
|
|
||||||
if row in deleted_pushers_rows:
|
|
||||||
user_id, app_id, pushkey = row[1:4]
|
|
||||||
stop_pusher(user_id, app_id, pushkey)
|
|
||||||
elif row in pushers_rows:
|
|
||||||
user_id = row[1]
|
|
||||||
app_id = row[5]
|
|
||||||
pushkey = row[8]
|
|
||||||
yield start_pusher(user_id, app_id, pushkey)
|
|
||||||
|
|
||||||
stream = results.get("events")
|
|
||||||
if stream and stream["rows"]:
|
|
||||||
min_stream_id = stream["rows"][0][0]
|
|
||||||
max_stream_id = stream["position"]
|
|
||||||
preserve_fn(pusher_pool.on_new_notifications)(
|
|
||||||
min_stream_id, max_stream_id
|
|
||||||
)
|
|
||||||
|
|
||||||
stream = results.get("receipts")
|
|
||||||
if stream and stream["rows"]:
|
|
||||||
rows = stream["rows"]
|
|
||||||
affected_room_ids = set(row[1] for row in rows)
|
|
||||||
min_stream_id = rows[0][0]
|
|
||||||
max_stream_id = stream["position"]
|
|
||||||
preserve_fn(pusher_pool.on_new_receipts)(
|
|
||||||
min_stream_id, max_stream_id, affected_room_ids
|
|
||||||
)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
args = store.stream_positions()
|
|
||||||
args["timeout"] = 30000
|
|
||||||
result = yield http_client.get_json(replication_url, args=args)
|
|
||||||
yield store.process_replication(result)
|
|
||||||
poke_pushers(result)
|
|
||||||
except:
|
|
||||||
logger.exception("Error replicating from %r", replication_url)
|
|
||||||
yield sleep(30)
|
|
||||||
|
|
||||||
|
|
||||||
def start(config_options):
|
|
||||||
try:
|
|
||||||
config = HomeServerConfig.load_config(
|
|
||||||
"Synapse pusher", config_options
|
|
||||||
)
|
|
||||||
except ConfigError as e:
|
|
||||||
sys.stderr.write("\n" + e.message + "\n")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
assert config.worker_app == "synapse.app.pusher"
|
|
||||||
|
|
||||||
setup_logging(config.worker_log_config, config.worker_log_file)
|
|
||||||
|
|
||||||
events.USE_FROZEN_DICTS = config.use_frozen_dicts
|
|
||||||
|
|
||||||
if config.start_pushers:
|
|
||||||
sys.stderr.write(
|
|
||||||
"\nThe pushers must be disabled in the main synapse process"
|
|
||||||
"\nbefore they can be run in a separate worker."
|
|
||||||
"\nPlease add ``start_pushers: false`` to the main config"
|
|
||||||
"\n"
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Force the pushers to start since they will be disabled in the main config
|
|
||||||
config.start_pushers = True
|
|
||||||
|
|
||||||
database_engine = create_engine(config.database_config)
|
|
||||||
|
|
||||||
ps = PusherServer(
|
|
||||||
config.server_name,
|
|
||||||
db_config=config.database_config,
|
|
||||||
config=config,
|
|
||||||
version_string="Synapse/" + get_version_string(synapse),
|
|
||||||
database_engine=database_engine,
|
|
||||||
)
|
|
||||||
|
|
||||||
ps.setup()
|
|
||||||
ps.start_listening(config.worker_listeners)
|
|
||||||
|
|
||||||
def run():
|
|
||||||
with LoggingContext("run"):
|
|
||||||
logger.info("Running")
|
|
||||||
change_resource_limit(config.soft_file_limit)
|
|
||||||
if config.gc_thresholds:
|
|
||||||
gc.set_threshold(*config.gc_thresholds)
|
|
||||||
reactor.run()
|
|
||||||
|
|
||||||
def start():
|
|
||||||
ps.replicate()
|
|
||||||
ps.get_pusherpool().start()
|
|
||||||
ps.get_datastore().start_profiling()
|
|
||||||
ps.get_state_handler().start_caching()
|
|
||||||
|
|
||||||
reactor.callWhenRunning(start)
|
|
||||||
|
|
||||||
if config.worker_daemonize:
|
|
||||||
daemon = Daemonize(
|
|
||||||
app="synapse-pusher",
|
|
||||||
pid=config.worker_pid_file,
|
|
||||||
action=run,
|
|
||||||
auto_close_fds=False,
|
|
||||||
verbose=True,
|
|
||||||
logger=logger,
|
|
||||||
)
|
|
||||||
daemon.start()
|
|
||||||
else:
|
|
||||||
run()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
with LoggingContext("main"):
|
|
||||||
ps = start(sys.argv[1:])
|
|
||||||
@@ -1,526 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2016 OpenMarket Ltd
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import synapse
|
|
||||||
|
|
||||||
from synapse.api.constants import EventTypes, PresenceState
|
|
||||||
from synapse.config._base import ConfigError
|
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
|
||||||
from synapse.config.logger import setup_logging
|
|
||||||
from synapse.events import FrozenEvent
|
|
||||||
from synapse.handlers.presence import PresenceHandler
|
|
||||||
from synapse.http.site import SynapseSite
|
|
||||||
from synapse.http.server import JsonResource
|
|
||||||
from synapse.metrics.resource import MetricsResource, METRICS_PREFIX
|
|
||||||
from synapse.rest.client.v2_alpha import sync
|
|
||||||
from synapse.rest.client.v1 import events
|
|
||||||
from synapse.rest.client.v1.room import RoomInitialSyncRestServlet
|
|
||||||
from synapse.rest.client.v1.initial_sync import InitialSyncRestServlet
|
|
||||||
from synapse.replication.slave.storage._base import BaseSlavedStore
|
|
||||||
from synapse.replication.slave.storage.events import SlavedEventStore
|
|
||||||
from synapse.replication.slave.storage.receipts import SlavedReceiptsStore
|
|
||||||
from synapse.replication.slave.storage.account_data import SlavedAccountDataStore
|
|
||||||
from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore
|
|
||||||
from synapse.replication.slave.storage.registration import SlavedRegistrationStore
|
|
||||||
from synapse.replication.slave.storage.filtering import SlavedFilteringStore
|
|
||||||
from synapse.replication.slave.storage.push_rule import SlavedPushRuleStore
|
|
||||||
from synapse.replication.slave.storage.presence import SlavedPresenceStore
|
|
||||||
from synapse.replication.slave.storage.deviceinbox import SlavedDeviceInboxStore
|
|
||||||
from synapse.replication.slave.storage.devices import SlavedDeviceStore
|
|
||||||
from synapse.replication.slave.storage.room import RoomStore
|
|
||||||
from synapse.server import HomeServer
|
|
||||||
from synapse.storage.client_ips import ClientIpStore
|
|
||||||
from synapse.storage.engines import create_engine
|
|
||||||
from synapse.storage.presence import PresenceStore, UserPresenceState
|
|
||||||
from synapse.storage.roommember import RoomMemberStore
|
|
||||||
from synapse.util.async import sleep
|
|
||||||
from synapse.util.httpresourcetree import create_resource_tree
|
|
||||||
from synapse.util.logcontext import LoggingContext, preserve_fn
|
|
||||||
from synapse.util.manhole import manhole
|
|
||||||
from synapse.util.rlimit import change_resource_limit
|
|
||||||
from synapse.util.stringutils import random_string
|
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
|
|
||||||
from twisted.internet import reactor, defer
|
|
||||||
from twisted.web.resource import Resource
|
|
||||||
|
|
||||||
from daemonize import Daemonize
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import contextlib
|
|
||||||
import gc
|
|
||||||
import ujson as json
|
|
||||||
|
|
||||||
logger = logging.getLogger("synapse.app.synchrotron")
|
|
||||||
|
|
||||||
|
|
||||||
class SynchrotronSlavedStore(
|
|
||||||
SlavedPushRuleStore,
|
|
||||||
SlavedEventStore,
|
|
||||||
SlavedReceiptsStore,
|
|
||||||
SlavedAccountDataStore,
|
|
||||||
SlavedApplicationServiceStore,
|
|
||||||
SlavedRegistrationStore,
|
|
||||||
SlavedFilteringStore,
|
|
||||||
SlavedPresenceStore,
|
|
||||||
SlavedDeviceInboxStore,
|
|
||||||
SlavedDeviceStore,
|
|
||||||
RoomStore,
|
|
||||||
BaseSlavedStore,
|
|
||||||
ClientIpStore, # After BaseSlavedStore because the constructor is different
|
|
||||||
):
|
|
||||||
who_forgot_in_room = (
|
|
||||||
RoomMemberStore.__dict__["who_forgot_in_room"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# XXX: This is a bit broken because we don't persist the accepted list in a
|
|
||||||
# way that can be replicated. This means that we don't have a way to
|
|
||||||
# invalidate the cache correctly.
|
|
||||||
get_presence_list_accepted = PresenceStore.__dict__[
|
|
||||||
"get_presence_list_accepted"
|
|
||||||
]
|
|
||||||
get_presence_list_observers_accepted = PresenceStore.__dict__[
|
|
||||||
"get_presence_list_observers_accepted"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
UPDATE_SYNCING_USERS_MS = 10 * 1000
|
|
||||||
|
|
||||||
|
|
||||||
class SynchrotronPresence(object):
|
|
||||||
def __init__(self, hs):
|
|
||||||
self.is_mine_id = hs.is_mine_id
|
|
||||||
self.http_client = hs.get_simple_http_client()
|
|
||||||
self.store = hs.get_datastore()
|
|
||||||
self.user_to_num_current_syncs = {}
|
|
||||||
self.syncing_users_url = hs.config.worker_replication_url + "/syncing_users"
|
|
||||||
self.clock = hs.get_clock()
|
|
||||||
self.notifier = hs.get_notifier()
|
|
||||||
|
|
||||||
active_presence = self.store.take_presence_startup_info()
|
|
||||||
self.user_to_current_state = {
|
|
||||||
state.user_id: state
|
|
||||||
for state in active_presence
|
|
||||||
}
|
|
||||||
|
|
||||||
self.process_id = random_string(16)
|
|
||||||
logger.info("Presence process_id is %r", self.process_id)
|
|
||||||
|
|
||||||
self._sending_sync = False
|
|
||||||
self._need_to_send_sync = False
|
|
||||||
self.clock.looping_call(
|
|
||||||
self._send_syncing_users_regularly,
|
|
||||||
UPDATE_SYNCING_USERS_MS,
|
|
||||||
)
|
|
||||||
|
|
||||||
reactor.addSystemEventTrigger("before", "shutdown", self._on_shutdown)
|
|
||||||
|
|
||||||
def set_state(self, user, state, ignore_status_msg=False):
|
|
||||||
# TODO Hows this supposed to work?
|
|
||||||
pass
|
|
||||||
|
|
||||||
get_states = PresenceHandler.get_states.__func__
|
|
||||||
get_state = PresenceHandler.get_state.__func__
|
|
||||||
_get_interested_parties = PresenceHandler._get_interested_parties.__func__
|
|
||||||
current_state_for_users = PresenceHandler.current_state_for_users.__func__
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def user_syncing(self, user_id, affect_presence):
|
|
||||||
if affect_presence:
|
|
||||||
curr_sync = self.user_to_num_current_syncs.get(user_id, 0)
|
|
||||||
self.user_to_num_current_syncs[user_id] = curr_sync + 1
|
|
||||||
prev_states = yield self.current_state_for_users([user_id])
|
|
||||||
if prev_states[user_id].state == PresenceState.OFFLINE:
|
|
||||||
# TODO: Don't block the sync request on this HTTP hit.
|
|
||||||
yield self._send_syncing_users_now()
|
|
||||||
|
|
||||||
def _end():
|
|
||||||
# We check that the user_id is in user_to_num_current_syncs because
|
|
||||||
# user_to_num_current_syncs may have been cleared if we are
|
|
||||||
# shutting down.
|
|
||||||
if affect_presence and user_id in self.user_to_num_current_syncs:
|
|
||||||
self.user_to_num_current_syncs[user_id] -= 1
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def _user_syncing():
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
_end()
|
|
||||||
|
|
||||||
defer.returnValue(_user_syncing())
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _on_shutdown(self):
|
|
||||||
# When the synchrotron is shutdown tell the master to clear the in
|
|
||||||
# progress syncs for this process
|
|
||||||
self.user_to_num_current_syncs.clear()
|
|
||||||
yield self._send_syncing_users_now()
|
|
||||||
|
|
||||||
def _send_syncing_users_regularly(self):
|
|
||||||
# Only send an update if we aren't in the middle of sending one.
|
|
||||||
if not self._sending_sync:
|
|
||||||
preserve_fn(self._send_syncing_users_now)()
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _send_syncing_users_now(self):
|
|
||||||
if self._sending_sync:
|
|
||||||
# We don't want to race with sending another update.
|
|
||||||
# Instead we wait for that update to finish and send another
|
|
||||||
# update afterwards.
|
|
||||||
self._need_to_send_sync = True
|
|
||||||
return
|
|
||||||
|
|
||||||
# Flag that we are sending an update.
|
|
||||||
self._sending_sync = True
|
|
||||||
|
|
||||||
yield self.http_client.post_json_get_json(self.syncing_users_url, {
|
|
||||||
"process_id": self.process_id,
|
|
||||||
"syncing_users": [
|
|
||||||
user_id for user_id, count in self.user_to_num_current_syncs.items()
|
|
||||||
if count > 0
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
# Unset the flag as we are no longer sending an update.
|
|
||||||
self._sending_sync = False
|
|
||||||
if self._need_to_send_sync:
|
|
||||||
# If something happened while we were sending the update then
|
|
||||||
# we might need to send another update.
|
|
||||||
# TODO: Check if the update that was sent matches the current state
|
|
||||||
# as we only need to send an update if they are different.
|
|
||||||
self._need_to_send_sync = False
|
|
||||||
yield self._send_syncing_users_now()
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def notify_from_replication(self, states, stream_id):
|
|
||||||
parties = yield self._get_interested_parties(
|
|
||||||
states, calculate_remote_hosts=False
|
|
||||||
)
|
|
||||||
room_ids_to_states, users_to_states, _ = parties
|
|
||||||
|
|
||||||
self.notifier.on_new_event(
|
|
||||||
"presence_key", stream_id, rooms=room_ids_to_states.keys(),
|
|
||||||
users=users_to_states.keys()
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def process_replication(self, result):
|
|
||||||
stream = result.get("presence", {"rows": []})
|
|
||||||
states = []
|
|
||||||
for row in stream["rows"]:
|
|
||||||
(
|
|
||||||
position, user_id, state, last_active_ts,
|
|
||||||
last_federation_update_ts, last_user_sync_ts, status_msg,
|
|
||||||
currently_active
|
|
||||||
) = row
|
|
||||||
state = UserPresenceState(
|
|
||||||
user_id, state, last_active_ts,
|
|
||||||
last_federation_update_ts, last_user_sync_ts, status_msg,
|
|
||||||
currently_active
|
|
||||||
)
|
|
||||||
self.user_to_current_state[user_id] = state
|
|
||||||
states.append(state)
|
|
||||||
|
|
||||||
if states and "position" in stream:
|
|
||||||
stream_id = int(stream["position"])
|
|
||||||
yield self.notify_from_replication(states, stream_id)
|
|
||||||
|
|
||||||
|
|
||||||
class SynchrotronTyping(object):
|
|
||||||
def __init__(self, hs):
|
|
||||||
self._latest_room_serial = 0
|
|
||||||
self._room_serials = {}
|
|
||||||
self._room_typing = {}
|
|
||||||
|
|
||||||
def stream_positions(self):
|
|
||||||
# We must update this typing token from the response of the previous
|
|
||||||
# sync. In particular, the stream id may "reset" back to zero/a low
|
|
||||||
# value which we *must* use for the next replication request.
|
|
||||||
return {"typing": self._latest_room_serial}
|
|
||||||
|
|
||||||
def process_replication(self, result):
|
|
||||||
stream = result.get("typing")
|
|
||||||
if stream:
|
|
||||||
self._latest_room_serial = int(stream["position"])
|
|
||||||
|
|
||||||
for row in stream["rows"]:
|
|
||||||
position, room_id, typing_json = row
|
|
||||||
typing = json.loads(typing_json)
|
|
||||||
self._room_serials[room_id] = position
|
|
||||||
self._room_typing[room_id] = typing
|
|
||||||
|
|
||||||
|
|
||||||
class SynchrotronApplicationService(object):
|
|
||||||
def notify_interested_services(self, event):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SynchrotronServer(HomeServer):
|
|
||||||
def get_db_conn(self, run_new_connection=True):
|
|
||||||
# Any param beginning with cp_ is a parameter for adbapi, and should
|
|
||||||
# not be passed to the database engine.
|
|
||||||
db_params = {
|
|
||||||
k: v for k, v in self.db_config.get("args", {}).items()
|
|
||||||
if not k.startswith("cp_")
|
|
||||||
}
|
|
||||||
db_conn = self.database_engine.module.connect(**db_params)
|
|
||||||
|
|
||||||
if run_new_connection:
|
|
||||||
self.database_engine.on_new_connection(db_conn)
|
|
||||||
return db_conn
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
logger.info("Setting up.")
|
|
||||||
self.datastore = SynchrotronSlavedStore(self.get_db_conn(), self)
|
|
||||||
logger.info("Finished setting up.")
|
|
||||||
|
|
||||||
def _listen_http(self, listener_config):
|
|
||||||
port = listener_config["port"]
|
|
||||||
bind_addresses = listener_config["bind_addresses"]
|
|
||||||
site_tag = listener_config.get("tag", port)
|
|
||||||
resources = {}
|
|
||||||
for res in listener_config["resources"]:
|
|
||||||
for name in res["names"]:
|
|
||||||
if name == "metrics":
|
|
||||||
resources[METRICS_PREFIX] = MetricsResource(self)
|
|
||||||
elif name == "client":
|
|
||||||
resource = JsonResource(self, canonical_json=False)
|
|
||||||
sync.register_servlets(self, resource)
|
|
||||||
events.register_servlets(self, resource)
|
|
||||||
InitialSyncRestServlet(self).register(resource)
|
|
||||||
RoomInitialSyncRestServlet(self).register(resource)
|
|
||||||
resources.update({
|
|
||||||
"/_matrix/client/r0": resource,
|
|
||||||
"/_matrix/client/unstable": resource,
|
|
||||||
"/_matrix/client/v2_alpha": resource,
|
|
||||||
"/_matrix/client/api/v1": resource,
|
|
||||||
})
|
|
||||||
|
|
||||||
root_resource = create_resource_tree(resources, Resource())
|
|
||||||
|
|
||||||
for address in bind_addresses:
|
|
||||||
reactor.listenTCP(
|
|
||||||
port,
|
|
||||||
SynapseSite(
|
|
||||||
"synapse.access.http.%s" % (site_tag,),
|
|
||||||
site_tag,
|
|
||||||
listener_config,
|
|
||||||
root_resource,
|
|
||||||
),
|
|
||||||
interface=address
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Synapse synchrotron now listening on port %d", port)
|
|
||||||
|
|
||||||
def start_listening(self, listeners):
|
|
||||||
for listener in listeners:
|
|
||||||
if listener["type"] == "http":
|
|
||||||
self._listen_http(listener)
|
|
||||||
elif listener["type"] == "manhole":
|
|
||||||
bind_addresses = listener["bind_addresses"]
|
|
||||||
|
|
||||||
for address in bind_addresses:
|
|
||||||
reactor.listenTCP(
|
|
||||||
listener["port"],
|
|
||||||
manhole(
|
|
||||||
username="matrix",
|
|
||||||
password="rabbithole",
|
|
||||||
globals={"hs": self},
|
|
||||||
),
|
|
||||||
interface=address
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warn("Unrecognized listener type: %s", listener["type"])
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def replicate(self):
|
|
||||||
http_client = self.get_simple_http_client()
|
|
||||||
store = self.get_datastore()
|
|
||||||
replication_url = self.config.worker_replication_url
|
|
||||||
notifier = self.get_notifier()
|
|
||||||
presence_handler = self.get_presence_handler()
|
|
||||||
typing_handler = self.get_typing_handler()
|
|
||||||
|
|
||||||
def notify_from_stream(
|
|
||||||
result, stream_name, stream_key, room=None, user=None
|
|
||||||
):
|
|
||||||
stream = result.get(stream_name)
|
|
||||||
if stream:
|
|
||||||
position_index = stream["field_names"].index("position")
|
|
||||||
if room:
|
|
||||||
room_index = stream["field_names"].index(room)
|
|
||||||
if user:
|
|
||||||
user_index = stream["field_names"].index(user)
|
|
||||||
|
|
||||||
users = ()
|
|
||||||
rooms = ()
|
|
||||||
for row in stream["rows"]:
|
|
||||||
position = row[position_index]
|
|
||||||
|
|
||||||
if user:
|
|
||||||
users = (row[user_index],)
|
|
||||||
|
|
||||||
if room:
|
|
||||||
rooms = (row[room_index],)
|
|
||||||
|
|
||||||
notifier.on_new_event(
|
|
||||||
stream_key, position, users=users, rooms=rooms
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def notify_device_list_update(result):
|
|
||||||
stream = result.get("device_lists")
|
|
||||||
if not stream:
|
|
||||||
return
|
|
||||||
|
|
||||||
position_index = stream["field_names"].index("position")
|
|
||||||
user_index = stream["field_names"].index("user_id")
|
|
||||||
|
|
||||||
for row in stream["rows"]:
|
|
||||||
position = row[position_index]
|
|
||||||
user_id = row[user_index]
|
|
||||||
|
|
||||||
rooms = yield store.get_rooms_for_user(user_id)
|
|
||||||
room_ids = [r.room_id for r in rooms]
|
|
||||||
|
|
||||||
notifier.on_new_event(
|
|
||||||
"device_list_key", position, rooms=room_ids,
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def notify(result):
|
|
||||||
stream = result.get("events")
|
|
||||||
if stream:
|
|
||||||
max_position = stream["position"]
|
|
||||||
for row in stream["rows"]:
|
|
||||||
position = row[0]
|
|
||||||
internal = json.loads(row[1])
|
|
||||||
event_json = json.loads(row[2])
|
|
||||||
event = FrozenEvent(event_json, internal_metadata_dict=internal)
|
|
||||||
extra_users = ()
|
|
||||||
if event.type == EventTypes.Member:
|
|
||||||
extra_users = (event.state_key,)
|
|
||||||
notifier.on_new_room_event(
|
|
||||||
event, position, max_position, extra_users
|
|
||||||
)
|
|
||||||
|
|
||||||
notify_from_stream(
|
|
||||||
result, "push_rules", "push_rules_key", user="user_id"
|
|
||||||
)
|
|
||||||
notify_from_stream(
|
|
||||||
result, "user_account_data", "account_data_key", user="user_id"
|
|
||||||
)
|
|
||||||
notify_from_stream(
|
|
||||||
result, "room_account_data", "account_data_key", user="user_id"
|
|
||||||
)
|
|
||||||
notify_from_stream(
|
|
||||||
result, "tag_account_data", "account_data_key", user="user_id"
|
|
||||||
)
|
|
||||||
notify_from_stream(
|
|
||||||
result, "receipts", "receipt_key", room="room_id"
|
|
||||||
)
|
|
||||||
notify_from_stream(
|
|
||||||
result, "typing", "typing_key", room="room_id"
|
|
||||||
)
|
|
||||||
notify_from_stream(
|
|
||||||
result, "to_device", "to_device_key", user="user_id"
|
|
||||||
)
|
|
||||||
yield notify_device_list_update(result)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
args = store.stream_positions()
|
|
||||||
args.update(typing_handler.stream_positions())
|
|
||||||
args["timeout"] = 30000
|
|
||||||
result = yield http_client.get_json(replication_url, args=args)
|
|
||||||
yield store.process_replication(result)
|
|
||||||
typing_handler.process_replication(result)
|
|
||||||
yield presence_handler.process_replication(result)
|
|
||||||
yield notify(result)
|
|
||||||
except:
|
|
||||||
logger.exception("Error replicating from %r", replication_url)
|
|
||||||
yield sleep(5)
|
|
||||||
|
|
||||||
def build_presence_handler(self):
|
|
||||||
return SynchrotronPresence(self)
|
|
||||||
|
|
||||||
def build_typing_handler(self):
|
|
||||||
return SynchrotronTyping(self)
|
|
||||||
|
|
||||||
|
|
||||||
def start(config_options):
|
|
||||||
try:
|
|
||||||
config = HomeServerConfig.load_config(
|
|
||||||
"Synapse synchrotron", config_options
|
|
||||||
)
|
|
||||||
except ConfigError as e:
|
|
||||||
sys.stderr.write("\n" + e.message + "\n")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
assert config.worker_app == "synapse.app.synchrotron"
|
|
||||||
|
|
||||||
setup_logging(config.worker_log_config, config.worker_log_file)
|
|
||||||
|
|
||||||
synapse.events.USE_FROZEN_DICTS = config.use_frozen_dicts
|
|
||||||
|
|
||||||
database_engine = create_engine(config.database_config)
|
|
||||||
|
|
||||||
ss = SynchrotronServer(
|
|
||||||
config.server_name,
|
|
||||||
db_config=config.database_config,
|
|
||||||
config=config,
|
|
||||||
version_string="Synapse/" + get_version_string(synapse),
|
|
||||||
database_engine=database_engine,
|
|
||||||
application_service_handler=SynchrotronApplicationService(),
|
|
||||||
)
|
|
||||||
|
|
||||||
ss.setup()
|
|
||||||
ss.start_listening(config.worker_listeners)
|
|
||||||
|
|
||||||
def run():
|
|
||||||
with LoggingContext("run"):
|
|
||||||
logger.info("Running")
|
|
||||||
change_resource_limit(config.soft_file_limit)
|
|
||||||
if config.gc_thresholds:
|
|
||||||
gc.set_threshold(*config.gc_thresholds)
|
|
||||||
reactor.run()
|
|
||||||
|
|
||||||
def start():
|
|
||||||
ss.get_datastore().start_profiling()
|
|
||||||
ss.replicate()
|
|
||||||
ss.get_state_handler().start_caching()
|
|
||||||
|
|
||||||
reactor.callWhenRunning(start)
|
|
||||||
|
|
||||||
if config.worker_daemonize:
|
|
||||||
daemon = Daemonize(
|
|
||||||
app="synapse-synchrotron",
|
|
||||||
pid=config.worker_pid_file,
|
|
||||||
action=run,
|
|
||||||
auto_close_fds=False,
|
|
||||||
verbose=True,
|
|
||||||
logger=logger,
|
|
||||||
)
|
|
||||||
daemon.start()
|
|
||||||
else:
|
|
||||||
run()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
with LoggingContext("main"):
|
|
||||||
start(sys.argv[1:])
|
|
||||||
@@ -14,198 +14,70 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import argparse
|
import sys
|
||||||
import collections
|
|
||||||
import glob
|
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import signal
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import signal
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
SYNAPSE = [sys.executable, "-B", "-m", "synapse.app.homeserver"]
|
SYNAPSE = ["python", "-B", "-m", "synapse.app.homeserver"]
|
||||||
|
|
||||||
GREEN = "\x1b[1;32m"
|
GREEN = "\x1b[1;32m"
|
||||||
RED = "\x1b[1;31m"
|
RED = "\x1b[1;31m"
|
||||||
NORMAL = "\x1b[m"
|
NORMAL = "\x1b[m"
|
||||||
|
|
||||||
|
|
||||||
def write(message, colour=NORMAL, stream=sys.stdout):
|
|
||||||
if colour == NORMAL:
|
|
||||||
stream.write(message + "\n")
|
|
||||||
else:
|
|
||||||
stream.write(colour + message + NORMAL + "\n")
|
|
||||||
|
|
||||||
|
|
||||||
def start(configfile):
|
def start(configfile):
|
||||||
write("Starting ...")
|
print ("Starting ...")
|
||||||
args = SYNAPSE
|
args = SYNAPSE
|
||||||
args.extend(["--daemonize", "-c", configfile])
|
args.extend(["--daemonize", "-c", configfile])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subprocess.check_call(args)
|
subprocess.check_call(args)
|
||||||
write("started synapse.app.homeserver(%r)" % (configfile,), colour=GREEN)
|
print (GREEN + "started" + NORMAL)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
write(
|
print (
|
||||||
"error starting (exit code: %d); see above for logs" % e.returncode,
|
RED +
|
||||||
colour=RED,
|
"error starting (exit code: %d); see above for logs" % e.returncode +
|
||||||
|
NORMAL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def start_worker(app, configfile, worker_configfile):
|
def stop(pidfile):
|
||||||
args = [
|
|
||||||
"python", "-B",
|
|
||||||
"-m", app,
|
|
||||||
"-c", configfile,
|
|
||||||
"-c", worker_configfile
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
subprocess.check_call(args)
|
|
||||||
write("started %s(%r)" % (app, worker_configfile), colour=GREEN)
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
write(
|
|
||||||
"error starting %s(%r) (exit code: %d); see above for logs" % (
|
|
||||||
app, worker_configfile, e.returncode,
|
|
||||||
),
|
|
||||||
colour=RED,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def stop(pidfile, app):
|
|
||||||
if os.path.exists(pidfile):
|
if os.path.exists(pidfile):
|
||||||
pid = int(open(pidfile).read())
|
pid = int(open(pidfile).read())
|
||||||
os.kill(pid, signal.SIGTERM)
|
os.kill(pid, signal.SIGTERM)
|
||||||
write("stopped %s" % (app,), colour=GREEN)
|
print (GREEN + "stopped" + NORMAL)
|
||||||
|
|
||||||
|
|
||||||
Worker = collections.namedtuple("Worker", [
|
|
||||||
"app", "configfile", "pidfile", "cache_factor"
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
configfile = sys.argv[2] if len(sys.argv) == 3 else "homeserver.yaml"
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"action",
|
|
||||||
choices=["start", "stop", "restart"],
|
|
||||||
help="whether to start, stop or restart the synapse",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"configfile",
|
|
||||||
nargs="?",
|
|
||||||
default="homeserver.yaml",
|
|
||||||
help="the homeserver config file, defaults to homserver.yaml",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-w", "--worker",
|
|
||||||
metavar="WORKERCONFIG",
|
|
||||||
help="start or stop a single worker",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-a", "--all-processes",
|
|
||||||
metavar="WORKERCONFIGDIR",
|
|
||||||
help="start or stop all the workers in the given directory"
|
|
||||||
" and the main synapse process",
|
|
||||||
)
|
|
||||||
|
|
||||||
options = parser.parse_args()
|
|
||||||
|
|
||||||
if options.worker and options.all_processes:
|
|
||||||
write(
|
|
||||||
'Cannot use "--worker" with "--all-processes"',
|
|
||||||
stream=sys.stderr
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
configfile = options.configfile
|
|
||||||
|
|
||||||
if not os.path.exists(configfile):
|
if not os.path.exists(configfile):
|
||||||
write(
|
sys.stderr.write(
|
||||||
"No config file found\n"
|
"No config file found\n"
|
||||||
"To generate a config file, run '%s -c %s --generate-config"
|
"To generate a config file, run '%s -c %s --generate-config"
|
||||||
" --server-name=<server name>'\n" % (
|
" --server-name=<server name>'\n" % (
|
||||||
" ".join(SYNAPSE), options.configfile
|
" ".join(SYNAPSE), configfile
|
||||||
),
|
)
|
||||||
stream=sys.stderr,
|
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
with open(configfile) as stream:
|
config = yaml.load(open(configfile))
|
||||||
config = yaml.load(stream)
|
|
||||||
|
|
||||||
pidfile = config["pid_file"]
|
pidfile = config["pid_file"]
|
||||||
cache_factor = config.get("synctl_cache_factor")
|
|
||||||
start_stop_synapse = True
|
|
||||||
|
|
||||||
if cache_factor:
|
action = sys.argv[1] if sys.argv[1:] else "usage"
|
||||||
os.environ["SYNAPSE_CACHE_FACTOR"] = str(cache_factor)
|
if action == "start":
|
||||||
|
start(configfile)
|
||||||
worker_configfiles = []
|
elif action == "stop":
|
||||||
if options.worker:
|
stop(pidfile)
|
||||||
start_stop_synapse = False
|
elif action == "restart":
|
||||||
worker_configfile = options.worker
|
stop(pidfile)
|
||||||
if not os.path.exists(worker_configfile):
|
start(configfile)
|
||||||
write(
|
else:
|
||||||
"No worker config found at %r" % (worker_configfile,),
|
sys.stderr.write("Usage: %s [start|stop|restart] [configfile]\n" % (sys.argv[0],))
|
||||||
stream=sys.stderr,
|
sys.exit(1)
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
worker_configfiles.append(worker_configfile)
|
|
||||||
|
|
||||||
if options.all_processes:
|
|
||||||
worker_configdir = options.all_processes
|
|
||||||
if not os.path.isdir(worker_configdir):
|
|
||||||
write(
|
|
||||||
"No worker config directory found at %r" % (worker_configdir,),
|
|
||||||
stream=sys.stderr,
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
worker_configfiles.extend(sorted(glob.glob(
|
|
||||||
os.path.join(worker_configdir, "*.yaml")
|
|
||||||
)))
|
|
||||||
|
|
||||||
workers = []
|
|
||||||
for worker_configfile in worker_configfiles:
|
|
||||||
with open(worker_configfile) as stream:
|
|
||||||
worker_config = yaml.load(stream)
|
|
||||||
worker_app = worker_config["worker_app"]
|
|
||||||
worker_pidfile = worker_config["worker_pid_file"]
|
|
||||||
worker_daemonize = worker_config["worker_daemonize"]
|
|
||||||
assert worker_daemonize # TODO print something more user friendly
|
|
||||||
worker_cache_factor = worker_config.get("synctl_cache_factor")
|
|
||||||
workers.append(Worker(
|
|
||||||
worker_app, worker_configfile, worker_pidfile, worker_cache_factor,
|
|
||||||
))
|
|
||||||
|
|
||||||
action = options.action
|
|
||||||
|
|
||||||
if action == "stop" or action == "restart":
|
|
||||||
for worker in workers:
|
|
||||||
stop(worker.pidfile, worker.app)
|
|
||||||
|
|
||||||
if start_stop_synapse:
|
|
||||||
stop(pidfile, "synapse.app.homeserver")
|
|
||||||
|
|
||||||
# TODO: Wait for synapse to actually shutdown before starting it again
|
|
||||||
|
|
||||||
if action == "start" or action == "restart":
|
|
||||||
if start_stop_synapse:
|
|
||||||
start(configfile)
|
|
||||||
|
|
||||||
for worker in workers:
|
|
||||||
if worker.cache_factor:
|
|
||||||
os.environ["SYNAPSE_CACHE_FACTOR"] = str(worker.cache_factor)
|
|
||||||
|
|
||||||
start_worker(worker.app, configfile, worker.configfile)
|
|
||||||
|
|
||||||
if cache_factor:
|
|
||||||
os.environ["SYNAPSE_CACHE_FACTOR"] = str(cache_factor)
|
|
||||||
else:
|
|
||||||
os.environ.pop("SYNAPSE_CACHE_FACTOR", None)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -14,8 +14,6 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
from synapse.api.constants import EventTypes
|
from synapse.api.constants import EventTypes
|
||||||
|
|
||||||
from twisted.internet import defer
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -81,7 +79,7 @@ class ApplicationService(object):
|
|||||||
NS_LIST = [NS_USERS, NS_ALIASES, NS_ROOMS]
|
NS_LIST = [NS_USERS, NS_ALIASES, NS_ROOMS]
|
||||||
|
|
||||||
def __init__(self, token, url=None, namespaces=None, hs_token=None,
|
def __init__(self, token, url=None, namespaces=None, hs_token=None,
|
||||||
sender=None, id=None, protocols=None, rate_limited=True):
|
sender=None, id=None):
|
||||||
self.token = token
|
self.token = token
|
||||||
self.url = url
|
self.url = url
|
||||||
self.hs_token = hs_token
|
self.hs_token = hs_token
|
||||||
@@ -89,17 +87,6 @@ class ApplicationService(object):
|
|||||||
self.namespaces = self._check_namespaces(namespaces)
|
self.namespaces = self._check_namespaces(namespaces)
|
||||||
self.id = id
|
self.id = id
|
||||||
|
|
||||||
if "|" in self.id:
|
|
||||||
raise Exception("application service ID cannot contain '|' character")
|
|
||||||
|
|
||||||
# .protocols is a publicly visible field
|
|
||||||
if protocols:
|
|
||||||
self.protocols = set(protocols)
|
|
||||||
else:
|
|
||||||
self.protocols = set()
|
|
||||||
|
|
||||||
self.rate_limited = rate_limited
|
|
||||||
|
|
||||||
def _check_namespaces(self, namespaces):
|
def _check_namespaces(self, namespaces):
|
||||||
# Sanity check that it is of the form:
|
# Sanity check that it is of the form:
|
||||||
# {
|
# {
|
||||||
@@ -151,66 +138,65 @@ class ApplicationService(object):
|
|||||||
return regex_obj["exclusive"]
|
return regex_obj["exclusive"]
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
def _matches_user(self, event, member_list):
|
||||||
def _matches_user(self, event, store):
|
if (hasattr(event, "sender") and
|
||||||
if not event:
|
self.is_interested_in_user(event.sender)):
|
||||||
defer.returnValue(False)
|
return True
|
||||||
|
|
||||||
if self.is_interested_in_user(event.sender):
|
|
||||||
defer.returnValue(True)
|
|
||||||
# also check m.room.member state key
|
# also check m.room.member state key
|
||||||
if (event.type == EventTypes.Member and
|
if (hasattr(event, "type") and event.type == EventTypes.Member
|
||||||
self.is_interested_in_user(event.state_key)):
|
and hasattr(event, "state_key")
|
||||||
defer.returnValue(True)
|
and self.is_interested_in_user(event.state_key)):
|
||||||
|
return True
|
||||||
if not store:
|
|
||||||
defer.returnValue(False)
|
|
||||||
|
|
||||||
member_list = yield store.get_users_in_room(event.room_id)
|
|
||||||
|
|
||||||
# check joined member events
|
# check joined member events
|
||||||
for user_id in member_list:
|
for user_id in member_list:
|
||||||
if self.is_interested_in_user(user_id):
|
if self.is_interested_in_user(user_id):
|
||||||
defer.returnValue(True)
|
return True
|
||||||
defer.returnValue(False)
|
return False
|
||||||
|
|
||||||
def _matches_room_id(self, event):
|
def _matches_room_id(self, event):
|
||||||
if hasattr(event, "room_id"):
|
if hasattr(event, "room_id"):
|
||||||
return self.is_interested_in_room(event.room_id)
|
return self.is_interested_in_room(event.room_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
def _matches_aliases(self, event, alias_list):
|
||||||
def _matches_aliases(self, event, store):
|
|
||||||
if not store or not event:
|
|
||||||
defer.returnValue(False)
|
|
||||||
|
|
||||||
alias_list = yield store.get_aliases_for_room(event.room_id)
|
|
||||||
for alias in alias_list:
|
for alias in alias_list:
|
||||||
if self.is_interested_in_alias(alias):
|
if self.is_interested_in_alias(alias):
|
||||||
defer.returnValue(True)
|
return True
|
||||||
defer.returnValue(False)
|
return False
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
def is_interested(self, event, restrict_to=None, aliases_for_event=None,
|
||||||
def is_interested(self, event, store=None):
|
member_list=None):
|
||||||
"""Check if this service is interested in this event.
|
"""Check if this service is interested in this event.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
event(Event): The event to check.
|
event(Event): The event to check.
|
||||||
store(DataStore)
|
restrict_to(str): The namespace to restrict regex tests to.
|
||||||
|
aliases_for_event(list): A list of all the known room aliases for
|
||||||
|
this event.
|
||||||
|
member_list(list): A list of all joined user_ids in this room.
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if this service would like to know about this event.
|
bool: True if this service would like to know about this event.
|
||||||
"""
|
"""
|
||||||
# Do cheap checks first
|
if aliases_for_event is None:
|
||||||
if self._matches_room_id(event):
|
aliases_for_event = []
|
||||||
defer.returnValue(True)
|
if member_list is None:
|
||||||
|
member_list = []
|
||||||
|
|
||||||
if (yield self._matches_aliases(event, store)):
|
if restrict_to and restrict_to not in ApplicationService.NS_LIST:
|
||||||
defer.returnValue(True)
|
# this is a programming error, so fail early and raise a general
|
||||||
|
# exception
|
||||||
|
raise Exception("Unexpected restrict_to value: %s". restrict_to)
|
||||||
|
|
||||||
if (yield self._matches_user(event, store)):
|
if not restrict_to:
|
||||||
defer.returnValue(True)
|
return (self._matches_user(event, member_list)
|
||||||
|
or self._matches_aliases(event, aliases_for_event)
|
||||||
defer.returnValue(False)
|
or self._matches_room_id(event))
|
||||||
|
elif restrict_to == ApplicationService.NS_ALIASES:
|
||||||
|
return self._matches_aliases(event, aliases_for_event)
|
||||||
|
elif restrict_to == ApplicationService.NS_ROOMS:
|
||||||
|
return self._matches_room_id(event)
|
||||||
|
elif restrict_to == ApplicationService.NS_USERS:
|
||||||
|
return self._matches_user(event, member_list)
|
||||||
|
|
||||||
def is_interested_in_user(self, user_id):
|
def is_interested_in_user(self, user_id):
|
||||||
return (
|
return (
|
||||||
@@ -230,17 +216,11 @@ class ApplicationService(object):
|
|||||||
or user_id == self.sender
|
or user_id == self.sender
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_interested_in_protocol(self, protocol):
|
|
||||||
return protocol in self.protocols
|
|
||||||
|
|
||||||
def is_exclusive_alias(self, alias):
|
def is_exclusive_alias(self, alias):
|
||||||
return self._is_exclusive(ApplicationService.NS_ALIASES, alias)
|
return self._is_exclusive(ApplicationService.NS_ALIASES, alias)
|
||||||
|
|
||||||
def is_exclusive_room(self, room_id):
|
def is_exclusive_room(self, room_id):
|
||||||
return self._is_exclusive(ApplicationService.NS_ROOMS, room_id)
|
return self._is_exclusive(ApplicationService.NS_ROOMS, room_id)
|
||||||
|
|
||||||
def is_rate_limited(self):
|
|
||||||
return self.rate_limited
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "ApplicationService: %s" % (self.__dict__,)
|
return "ApplicationService: %s" % (self.__dict__,)
|
||||||
|
|||||||
@@ -14,12 +14,9 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.constants import ThirdPartyEntityKind
|
|
||||||
from synapse.api.errors import CodeMessageException
|
from synapse.api.errors import CodeMessageException
|
||||||
from synapse.http.client import SimpleHttpClient
|
from synapse.http.client import SimpleHttpClient
|
||||||
from synapse.events.utils import serialize_event
|
from synapse.events.utils import serialize_event
|
||||||
from synapse.util.caches.response_cache import ResponseCache
|
|
||||||
from synapse.types import ThirdPartyInstanceID
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import urllib
|
import urllib
|
||||||
@@ -27,42 +24,6 @@ import urllib
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
HOUR_IN_MS = 60 * 60 * 1000
|
|
||||||
|
|
||||||
|
|
||||||
APP_SERVICE_PREFIX = "/_matrix/app/unstable"
|
|
||||||
|
|
||||||
|
|
||||||
def _is_valid_3pe_metadata(info):
|
|
||||||
if "instances" not in info:
|
|
||||||
return False
|
|
||||||
if not isinstance(info["instances"], list):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _is_valid_3pe_result(r, field):
|
|
||||||
if not isinstance(r, dict):
|
|
||||||
return False
|
|
||||||
|
|
||||||
for k in (field, "protocol"):
|
|
||||||
if k not in r:
|
|
||||||
return False
|
|
||||||
if not isinstance(r[k], str):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if "fields" not in r:
|
|
||||||
return False
|
|
||||||
fields = r["fields"]
|
|
||||||
if not isinstance(fields, dict):
|
|
||||||
return False
|
|
||||||
for k in fields.keys():
|
|
||||||
if not isinstance(fields[k], str):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationServiceApi(SimpleHttpClient):
|
class ApplicationServiceApi(SimpleHttpClient):
|
||||||
"""This class manages HS -> AS communications, including querying and
|
"""This class manages HS -> AS communications, including querying and
|
||||||
pushing.
|
pushing.
|
||||||
@@ -72,12 +33,8 @@ class ApplicationServiceApi(SimpleHttpClient):
|
|||||||
super(ApplicationServiceApi, self).__init__(hs)
|
super(ApplicationServiceApi, self).__init__(hs)
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
|
|
||||||
self.protocol_meta_cache = ResponseCache(hs, timeout_ms=HOUR_IN_MS)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def query_user(self, service, user_id):
|
def query_user(self, service, user_id):
|
||||||
if service.url is None:
|
|
||||||
defer.returnValue(False)
|
|
||||||
uri = service.url + ("/users/%s" % urllib.quote(user_id))
|
uri = service.url + ("/users/%s" % urllib.quote(user_id))
|
||||||
response = None
|
response = None
|
||||||
try:
|
try:
|
||||||
@@ -97,8 +54,6 @@ class ApplicationServiceApi(SimpleHttpClient):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def query_alias(self, service, alias):
|
def query_alias(self, service, alias):
|
||||||
if service.url is None:
|
|
||||||
defer.returnValue(False)
|
|
||||||
uri = service.url + ("/rooms/%s" % urllib.quote(alias))
|
uri = service.url + ("/rooms/%s" % urllib.quote(alias))
|
||||||
response = None
|
response = None
|
||||||
try:
|
try:
|
||||||
@@ -116,91 +71,8 @@ class ApplicationServiceApi(SimpleHttpClient):
|
|||||||
logger.warning("query_alias to %s threw exception %s", uri, ex)
|
logger.warning("query_alias to %s threw exception %s", uri, ex)
|
||||||
defer.returnValue(False)
|
defer.returnValue(False)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def query_3pe(self, service, kind, protocol, fields):
|
|
||||||
if kind == ThirdPartyEntityKind.USER:
|
|
||||||
required_field = "userid"
|
|
||||||
elif kind == ThirdPartyEntityKind.LOCATION:
|
|
||||||
required_field = "alias"
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
"Unrecognised 'kind' argument %r to query_3pe()", kind
|
|
||||||
)
|
|
||||||
if service.url is None:
|
|
||||||
defer.returnValue([])
|
|
||||||
|
|
||||||
uri = "%s%s/thirdparty/%s/%s" % (
|
|
||||||
service.url,
|
|
||||||
APP_SERVICE_PREFIX,
|
|
||||||
kind,
|
|
||||||
urllib.quote(protocol)
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
response = yield self.get_json(uri, fields)
|
|
||||||
if not isinstance(response, list):
|
|
||||||
logger.warning(
|
|
||||||
"query_3pe to %s returned an invalid response %r",
|
|
||||||
uri, response
|
|
||||||
)
|
|
||||||
defer.returnValue([])
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
for r in response:
|
|
||||||
if _is_valid_3pe_result(r, field=required_field):
|
|
||||||
ret.append(r)
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
"query_3pe to %s returned an invalid result %r",
|
|
||||||
uri, r
|
|
||||||
)
|
|
||||||
|
|
||||||
defer.returnValue(ret)
|
|
||||||
except Exception as ex:
|
|
||||||
logger.warning("query_3pe to %s threw exception %s", uri, ex)
|
|
||||||
defer.returnValue([])
|
|
||||||
|
|
||||||
def get_3pe_protocol(self, service, protocol):
|
|
||||||
if service.url is None:
|
|
||||||
defer.returnValue({})
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _get():
|
|
||||||
uri = "%s%s/thirdparty/protocol/%s" % (
|
|
||||||
service.url,
|
|
||||||
APP_SERVICE_PREFIX,
|
|
||||||
urllib.quote(protocol)
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
info = yield self.get_json(uri, {})
|
|
||||||
|
|
||||||
if not _is_valid_3pe_metadata(info):
|
|
||||||
logger.warning("query_3pe_protocol to %s did not return a"
|
|
||||||
" valid result", uri)
|
|
||||||
defer.returnValue(None)
|
|
||||||
|
|
||||||
for instance in info.get("instances", []):
|
|
||||||
network_id = instance.get("network_id", None)
|
|
||||||
if network_id is not None:
|
|
||||||
instance["instance_id"] = ThirdPartyInstanceID(
|
|
||||||
service.id, network_id,
|
|
||||||
).to_string()
|
|
||||||
|
|
||||||
defer.returnValue(info)
|
|
||||||
except Exception as ex:
|
|
||||||
logger.warning("query_3pe_protocol to %s threw exception %s",
|
|
||||||
uri, ex)
|
|
||||||
defer.returnValue(None)
|
|
||||||
|
|
||||||
key = (service.id, protocol)
|
|
||||||
return self.protocol_meta_cache.get(key) or (
|
|
||||||
self.protocol_meta_cache.set(key, _get())
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def push_bulk(self, service, events, txn_id=None):
|
def push_bulk(self, service, events, txn_id=None):
|
||||||
if service.url is None:
|
|
||||||
defer.returnValue(True)
|
|
||||||
|
|
||||||
events = self._serialize(events)
|
events = self._serialize(events)
|
||||||
|
|
||||||
if txn_id is None:
|
if txn_id is None:
|
||||||
|
|||||||
@@ -48,35 +48,32 @@ UP & quit +---------- YES SUCCESS
|
|||||||
This is all tied together by the AppServiceScheduler which DIs the required
|
This is all tied together by the AppServiceScheduler which DIs the required
|
||||||
components.
|
components.
|
||||||
"""
|
"""
|
||||||
from twisted.internet import defer
|
|
||||||
|
|
||||||
from synapse.appservice import ApplicationServiceState
|
from synapse.appservice import ApplicationServiceState
|
||||||
from synapse.util.logcontext import preserve_fn
|
from twisted.internet import defer
|
||||||
from synapse.util.metrics import Measure
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ApplicationServiceScheduler(object):
|
class AppServiceScheduler(object):
|
||||||
""" Public facing API for this module. Does the required DI to tie the
|
""" Public facing API for this module. Does the required DI to tie the
|
||||||
components together. This also serves as the "event_pool", which in this
|
components together. This also serves as the "event_pool", which in this
|
||||||
case is a simple array.
|
case is a simple array.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, clock, store, as_api):
|
||||||
self.clock = hs.get_clock()
|
self.clock = clock
|
||||||
self.store = hs.get_datastore()
|
self.store = store
|
||||||
self.as_api = hs.get_application_service_api()
|
self.as_api = as_api
|
||||||
|
|
||||||
def create_recoverer(service, callback):
|
def create_recoverer(service, callback):
|
||||||
return _Recoverer(self.clock, self.store, self.as_api, service, callback)
|
return _Recoverer(clock, store, as_api, service, callback)
|
||||||
|
|
||||||
self.txn_ctrl = _TransactionController(
|
self.txn_ctrl = _TransactionController(
|
||||||
self.clock, self.store, self.as_api, create_recoverer
|
clock, store, as_api, create_recoverer
|
||||||
)
|
)
|
||||||
self.queuer = _ServiceQueuer(self.txn_ctrl, self.clock)
|
self.queuer = _ServiceQueuer(self.txn_ctrl)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def start(self):
|
def start(self):
|
||||||
@@ -97,36 +94,38 @@ class _ServiceQueuer(object):
|
|||||||
this schedules any other events in the queue to run.
|
this schedules any other events in the queue to run.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, txn_ctrl, clock):
|
def __init__(self, txn_ctrl):
|
||||||
self.queued_events = {} # dict of {service_id: [events]}
|
self.queued_events = {} # dict of {service_id: [events]}
|
||||||
self.requests_in_flight = set()
|
self.pending_requests = {} # dict of {service_id: Deferred}
|
||||||
self.txn_ctrl = txn_ctrl
|
self.txn_ctrl = txn_ctrl
|
||||||
self.clock = clock
|
|
||||||
|
|
||||||
def enqueue(self, service, event):
|
def enqueue(self, service, event):
|
||||||
# if this service isn't being sent something
|
# if this service isn't being sent something
|
||||||
self.queued_events.setdefault(service.id, []).append(event)
|
if not self.pending_requests.get(service.id):
|
||||||
preserve_fn(self._send_request)(service)
|
self._send_request(service, [event])
|
||||||
|
else:
|
||||||
|
# add to queue for this service
|
||||||
|
if service.id not in self.queued_events:
|
||||||
|
self.queued_events[service.id] = []
|
||||||
|
self.queued_events[service.id].append(event)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
def _send_request(self, service, events):
|
||||||
def _send_request(self, service):
|
# send request and add callbacks
|
||||||
if service.id in self.requests_in_flight:
|
d = self.txn_ctrl.send(service, events)
|
||||||
return
|
d.addBoth(self._on_request_finish)
|
||||||
|
d.addErrback(self._on_request_fail)
|
||||||
|
self.pending_requests[service.id] = d
|
||||||
|
|
||||||
self.requests_in_flight.add(service.id)
|
def _on_request_finish(self, service):
|
||||||
try:
|
self.pending_requests[service.id] = None
|
||||||
while True:
|
# if there are queued events, then send them.
|
||||||
events = self.queued_events.pop(service.id, [])
|
if (service.id in self.queued_events
|
||||||
if not events:
|
and len(self.queued_events[service.id]) > 0):
|
||||||
return
|
self._send_request(service, self.queued_events[service.id])
|
||||||
|
self.queued_events[service.id] = []
|
||||||
|
|
||||||
with Measure(self.clock, "servicequeuer.send"):
|
def _on_request_fail(self, err):
|
||||||
try:
|
logger.error("AS request failed: %s", err)
|
||||||
yield self.txn_ctrl.send(service, events)
|
|
||||||
except:
|
|
||||||
logger.exception("AS request failed")
|
|
||||||
finally:
|
|
||||||
self.requests_in_flight.discard(service.id)
|
|
||||||
|
|
||||||
|
|
||||||
class _TransactionController(object):
|
class _TransactionController(object):
|
||||||
@@ -150,12 +149,14 @@ class _TransactionController(object):
|
|||||||
if service_is_up:
|
if service_is_up:
|
||||||
sent = yield txn.send(self.as_api)
|
sent = yield txn.send(self.as_api)
|
||||||
if sent:
|
if sent:
|
||||||
yield txn.complete(self.store)
|
txn.complete(self.store)
|
||||||
else:
|
else:
|
||||||
preserve_fn(self._start_recoverer)(service)
|
self._start_recoverer(service)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
preserve_fn(self._start_recoverer)(service)
|
self._start_recoverer(service)
|
||||||
|
# request has finished
|
||||||
|
defer.returnValue(service)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_recovered(self, recoverer):
|
def on_recovered(self, recoverer):
|
||||||
|
|||||||
@@ -64,12 +64,11 @@ class Config(object):
|
|||||||
if isinstance(value, int) or isinstance(value, long):
|
if isinstance(value, int) or isinstance(value, long):
|
||||||
return value
|
return value
|
||||||
second = 1000
|
second = 1000
|
||||||
minute = 60 * second
|
hour = 60 * 60 * second
|
||||||
hour = 60 * minute
|
|
||||||
day = 24 * hour
|
day = 24 * hour
|
||||||
week = 7 * day
|
week = 7 * day
|
||||||
year = 365 * day
|
year = 365 * day
|
||||||
sizes = {"s": second, "m": minute, "h": hour, "d": day, "w": week, "y": year}
|
sizes = {"s": second, "h": hour, "d": day, "w": week, "y": year}
|
||||||
size = 1
|
size = 1
|
||||||
suffix = value[-1]
|
suffix = value[-1]
|
||||||
if suffix in sizes:
|
if suffix in sizes:
|
||||||
@@ -158,40 +157,9 @@ class Config(object):
|
|||||||
return default_config, config
|
return default_config, config
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_config(cls, description, argv):
|
def load_config(cls, description, argv, generate_section=None):
|
||||||
config_parser = argparse.ArgumentParser(
|
|
||||||
description=description,
|
|
||||||
)
|
|
||||||
config_parser.add_argument(
|
|
||||||
"-c", "--config-path",
|
|
||||||
action="append",
|
|
||||||
metavar="CONFIG_FILE",
|
|
||||||
help="Specify config file. Can be given multiple times and"
|
|
||||||
" may specify directories containing *.yaml files."
|
|
||||||
)
|
|
||||||
|
|
||||||
config_parser.add_argument(
|
|
||||||
"--keys-directory",
|
|
||||||
metavar="DIRECTORY",
|
|
||||||
help="Where files such as certs and signing keys are stored when"
|
|
||||||
" their location is given explicitly in the config."
|
|
||||||
" Defaults to the directory containing the last config file",
|
|
||||||
)
|
|
||||||
|
|
||||||
config_args = config_parser.parse_args(argv)
|
|
||||||
|
|
||||||
config_files = find_config_files(search_paths=config_args.config_path)
|
|
||||||
|
|
||||||
obj = cls()
|
obj = cls()
|
||||||
obj.read_config_files(
|
|
||||||
config_files,
|
|
||||||
keys_directory=config_args.keys_directory,
|
|
||||||
generate_keys=False,
|
|
||||||
)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def load_or_generate_config(cls, description, argv):
|
|
||||||
config_parser = argparse.ArgumentParser(add_help=False)
|
config_parser = argparse.ArgumentParser(add_help=False)
|
||||||
config_parser.add_argument(
|
config_parser.add_argument(
|
||||||
"-c", "--config-path",
|
"-c", "--config-path",
|
||||||
@@ -208,7 +176,7 @@ class Config(object):
|
|||||||
config_parser.add_argument(
|
config_parser.add_argument(
|
||||||
"--report-stats",
|
"--report-stats",
|
||||||
action="store",
|
action="store",
|
||||||
help="Whether the generated config reports anonymized usage statistics",
|
help="Stuff",
|
||||||
choices=["yes", "no"]
|
choices=["yes", "no"]
|
||||||
)
|
)
|
||||||
config_parser.add_argument(
|
config_parser.add_argument(
|
||||||
@@ -229,11 +197,36 @@ class Config(object):
|
|||||||
)
|
)
|
||||||
config_args, remaining_args = config_parser.parse_known_args(argv)
|
config_args, remaining_args = config_parser.parse_known_args(argv)
|
||||||
|
|
||||||
config_files = find_config_files(search_paths=config_args.config_path)
|
|
||||||
|
|
||||||
generate_keys = config_args.generate_keys
|
generate_keys = config_args.generate_keys
|
||||||
|
|
||||||
obj = cls()
|
config_files = []
|
||||||
|
if config_args.config_path:
|
||||||
|
for config_path in config_args.config_path:
|
||||||
|
if os.path.isdir(config_path):
|
||||||
|
# We accept specifying directories as config paths, we search
|
||||||
|
# inside that directory for all files matching *.yaml, and then
|
||||||
|
# we apply them in *sorted* order.
|
||||||
|
files = []
|
||||||
|
for entry in os.listdir(config_path):
|
||||||
|
entry_path = os.path.join(config_path, entry)
|
||||||
|
if not os.path.isfile(entry_path):
|
||||||
|
print (
|
||||||
|
"Found subdirectory in config directory: %r. IGNORING."
|
||||||
|
) % (entry_path, )
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not entry.endswith(".yaml"):
|
||||||
|
print (
|
||||||
|
"Found file in config directory that does not"
|
||||||
|
" end in '.yaml': %r. IGNORING."
|
||||||
|
) % (entry_path, )
|
||||||
|
continue
|
||||||
|
|
||||||
|
files.append(entry_path)
|
||||||
|
|
||||||
|
config_files.extend(sorted(files))
|
||||||
|
else:
|
||||||
|
config_files.append(config_path)
|
||||||
|
|
||||||
if config_args.generate_config:
|
if config_args.generate_config:
|
||||||
if config_args.report_stats is None:
|
if config_args.report_stats is None:
|
||||||
@@ -306,43 +299,28 @@ class Config(object):
|
|||||||
" -c CONFIG-FILE\""
|
" -c CONFIG-FILE\""
|
||||||
)
|
)
|
||||||
|
|
||||||
obj.read_config_files(
|
if config_args.keys_directory:
|
||||||
config_files,
|
config_dir_path = config_args.keys_directory
|
||||||
keys_directory=config_args.keys_directory,
|
else:
|
||||||
generate_keys=generate_keys,
|
config_dir_path = os.path.dirname(config_args.config_path[-1])
|
||||||
)
|
config_dir_path = os.path.abspath(config_dir_path)
|
||||||
|
|
||||||
if generate_keys:
|
|
||||||
return None
|
|
||||||
|
|
||||||
obj.invoke_all("read_arguments", args)
|
|
||||||
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def read_config_files(self, config_files, keys_directory=None,
|
|
||||||
generate_keys=False):
|
|
||||||
if not keys_directory:
|
|
||||||
keys_directory = os.path.dirname(config_files[-1])
|
|
||||||
|
|
||||||
config_dir_path = os.path.abspath(keys_directory)
|
|
||||||
|
|
||||||
specified_config = {}
|
specified_config = {}
|
||||||
for config_file in config_files:
|
for config_file in config_files:
|
||||||
yaml_config = self.read_config_file(config_file)
|
yaml_config = cls.read_config_file(config_file)
|
||||||
specified_config.update(yaml_config)
|
specified_config.update(yaml_config)
|
||||||
|
|
||||||
if "server_name" not in specified_config:
|
if "server_name" not in specified_config:
|
||||||
raise ConfigError(MISSING_SERVER_NAME)
|
raise ConfigError(MISSING_SERVER_NAME)
|
||||||
|
|
||||||
server_name = specified_config["server_name"]
|
server_name = specified_config["server_name"]
|
||||||
_, config = self.generate_config(
|
_, config = obj.generate_config(
|
||||||
config_dir_path=config_dir_path,
|
config_dir_path=config_dir_path,
|
||||||
server_name=server_name,
|
server_name=server_name,
|
||||||
is_generating_file=False,
|
is_generating_file=False,
|
||||||
)
|
)
|
||||||
config.pop("log_config")
|
config.pop("log_config")
|
||||||
config.update(specified_config)
|
config.update(specified_config)
|
||||||
|
|
||||||
if "report_stats" not in config:
|
if "report_stats" not in config:
|
||||||
raise ConfigError(
|
raise ConfigError(
|
||||||
MISSING_REPORT_STATS_CONFIG_INSTRUCTIONS + "\n" +
|
MISSING_REPORT_STATS_CONFIG_INSTRUCTIONS + "\n" +
|
||||||
@@ -350,51 +328,11 @@ class Config(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if generate_keys:
|
if generate_keys:
|
||||||
self.invoke_all("generate_files", config)
|
obj.invoke_all("generate_files", config)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.invoke_all("read_config", config)
|
obj.invoke_all("read_config", config)
|
||||||
|
|
||||||
|
obj.invoke_all("read_arguments", args)
|
||||||
|
|
||||||
def find_config_files(search_paths):
|
return obj
|
||||||
"""Finds config files using a list of search paths. If a path is a file
|
|
||||||
then that file path is added to the list. If a search path is a directory
|
|
||||||
then all the "*.yaml" files in that directory are added to the list in
|
|
||||||
sorted order.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
search_paths(list(str)): A list of paths to search.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list(str): A list of file paths.
|
|
||||||
"""
|
|
||||||
|
|
||||||
config_files = []
|
|
||||||
if search_paths:
|
|
||||||
for config_path in search_paths:
|
|
||||||
if os.path.isdir(config_path):
|
|
||||||
# We accept specifying directories as config paths, we search
|
|
||||||
# inside that directory for all files matching *.yaml, and then
|
|
||||||
# we apply them in *sorted* order.
|
|
||||||
files = []
|
|
||||||
for entry in os.listdir(config_path):
|
|
||||||
entry_path = os.path.join(config_path, entry)
|
|
||||||
if not os.path.isfile(entry_path):
|
|
||||||
print (
|
|
||||||
"Found subdirectory in config directory: %r. IGNORING."
|
|
||||||
) % (entry_path, )
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not entry.endswith(".yaml"):
|
|
||||||
print (
|
|
||||||
"Found file in config directory that does not"
|
|
||||||
" end in '.yaml': %r. IGNORING."
|
|
||||||
) % (entry_path, )
|
|
||||||
continue
|
|
||||||
|
|
||||||
files.append(entry_path)
|
|
||||||
|
|
||||||
config_files.extend(sorted(files))
|
|
||||||
else:
|
|
||||||
config_files.append(config_path)
|
|
||||||
return config_files
|
|
||||||
|
|||||||
@@ -12,153 +12,16 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from ._base import Config, ConfigError
|
from ._base import Config
|
||||||
|
|
||||||
from synapse.appservice import ApplicationService
|
|
||||||
from synapse.types import UserID
|
|
||||||
|
|
||||||
import urllib
|
|
||||||
import yaml
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class AppServiceConfig(Config):
|
class AppServiceConfig(Config):
|
||||||
|
|
||||||
def read_config(self, config):
|
def read_config(self, config):
|
||||||
self.app_service_config_files = config.get("app_service_config_files", [])
|
self.app_service_config_files = config.get("app_service_config_files", [])
|
||||||
self.notify_appservices = config.get("notify_appservices", True)
|
|
||||||
|
|
||||||
def default_config(cls, **kwargs):
|
def default_config(cls, **kwargs):
|
||||||
return """\
|
return """\
|
||||||
# A list of application service config file to use
|
# A list of application service config file to use
|
||||||
app_service_config_files: []
|
app_service_config_files: []
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def load_appservices(hostname, config_files):
|
|
||||||
"""Returns a list of Application Services from the config files."""
|
|
||||||
if not isinstance(config_files, list):
|
|
||||||
logger.warning(
|
|
||||||
"Expected %s to be a list of AS config files.", config_files
|
|
||||||
)
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Dicts of value -> filename
|
|
||||||
seen_as_tokens = {}
|
|
||||||
seen_ids = {}
|
|
||||||
|
|
||||||
appservices = []
|
|
||||||
|
|
||||||
for config_file in config_files:
|
|
||||||
try:
|
|
||||||
with open(config_file, 'r') as f:
|
|
||||||
appservice = _load_appservice(
|
|
||||||
hostname, yaml.load(f), config_file
|
|
||||||
)
|
|
||||||
if appservice.id in seen_ids:
|
|
||||||
raise ConfigError(
|
|
||||||
"Cannot reuse ID across application services: "
|
|
||||||
"%s (files: %s, %s)" % (
|
|
||||||
appservice.id, config_file, seen_ids[appservice.id],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
seen_ids[appservice.id] = config_file
|
|
||||||
if appservice.token in seen_as_tokens:
|
|
||||||
raise ConfigError(
|
|
||||||
"Cannot reuse as_token across application services: "
|
|
||||||
"%s (files: %s, %s)" % (
|
|
||||||
appservice.token,
|
|
||||||
config_file,
|
|
||||||
seen_as_tokens[appservice.token],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
seen_as_tokens[appservice.token] = config_file
|
|
||||||
logger.info("Loaded application service: %s", appservice)
|
|
||||||
appservices.append(appservice)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Failed to load appservice from '%s'", config_file)
|
|
||||||
logger.exception(e)
|
|
||||||
raise
|
|
||||||
return appservices
|
|
||||||
|
|
||||||
|
|
||||||
def _load_appservice(hostname, as_info, config_filename):
|
|
||||||
required_string_fields = [
|
|
||||||
"id", "as_token", "hs_token", "sender_localpart"
|
|
||||||
]
|
|
||||||
for field in required_string_fields:
|
|
||||||
if not isinstance(as_info.get(field), basestring):
|
|
||||||
raise KeyError("Required string field: '%s' (%s)" % (
|
|
||||||
field, config_filename,
|
|
||||||
))
|
|
||||||
|
|
||||||
# 'url' must either be a string or explicitly null, not missing
|
|
||||||
# to avoid accidentally turning off push for ASes.
|
|
||||||
if (not isinstance(as_info.get("url"), basestring) and
|
|
||||||
as_info.get("url", "") is not None):
|
|
||||||
raise KeyError(
|
|
||||||
"Required string field or explicit null: 'url' (%s)" % (config_filename,)
|
|
||||||
)
|
|
||||||
|
|
||||||
localpart = as_info["sender_localpart"]
|
|
||||||
if urllib.quote(localpart) != localpart:
|
|
||||||
raise ValueError(
|
|
||||||
"sender_localpart needs characters which are not URL encoded."
|
|
||||||
)
|
|
||||||
user = UserID(localpart, hostname)
|
|
||||||
user_id = user.to_string()
|
|
||||||
|
|
||||||
# Rate limiting for users of this AS is on by default (excludes sender)
|
|
||||||
rate_limited = True
|
|
||||||
if isinstance(as_info.get("rate_limited"), bool):
|
|
||||||
rate_limited = as_info.get("rate_limited")
|
|
||||||
|
|
||||||
# namespace checks
|
|
||||||
if not isinstance(as_info.get("namespaces"), dict):
|
|
||||||
raise KeyError("Requires 'namespaces' object.")
|
|
||||||
for ns in ApplicationService.NS_LIST:
|
|
||||||
# specific namespaces are optional
|
|
||||||
if ns in as_info["namespaces"]:
|
|
||||||
# expect a list of dicts with exclusive and regex keys
|
|
||||||
for regex_obj in as_info["namespaces"][ns]:
|
|
||||||
if not isinstance(regex_obj, dict):
|
|
||||||
raise ValueError(
|
|
||||||
"Expected namespace entry in %s to be an object,"
|
|
||||||
" but got %s", ns, regex_obj
|
|
||||||
)
|
|
||||||
if not isinstance(regex_obj.get("regex"), basestring):
|
|
||||||
raise ValueError(
|
|
||||||
"Missing/bad type 'regex' key in %s", regex_obj
|
|
||||||
)
|
|
||||||
if not isinstance(regex_obj.get("exclusive"), bool):
|
|
||||||
raise ValueError(
|
|
||||||
"Missing/bad type 'exclusive' key in %s", regex_obj
|
|
||||||
)
|
|
||||||
# protocols check
|
|
||||||
protocols = as_info.get("protocols")
|
|
||||||
if protocols:
|
|
||||||
# Because strings are lists in python
|
|
||||||
if isinstance(protocols, str) or not isinstance(protocols, list):
|
|
||||||
raise KeyError("Optional 'protocols' must be a list if present.")
|
|
||||||
for p in protocols:
|
|
||||||
if not isinstance(p, str):
|
|
||||||
raise KeyError("Bad value for 'protocols' item")
|
|
||||||
|
|
||||||
if as_info["url"] is None:
|
|
||||||
logger.info(
|
|
||||||
"(%s) Explicitly empty 'url' provided. This application service"
|
|
||||||
" will not receive events or queries.",
|
|
||||||
config_filename,
|
|
||||||
)
|
|
||||||
return ApplicationService(
|
|
||||||
token=as_info["as_token"],
|
|
||||||
url=as_info["url"],
|
|
||||||
namespaces=as_info["namespaces"],
|
|
||||||
hs_token=as_info["hs_token"],
|
|
||||||
sender=user_id,
|
|
||||||
id=as_info["id"],
|
|
||||||
protocols=protocols,
|
|
||||||
rate_limited=rate_limited
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ class CaptchaConfig(Config):
|
|||||||
def default_config(self, **kwargs):
|
def default_config(self, **kwargs):
|
||||||
return """\
|
return """\
|
||||||
## Captcha ##
|
## Captcha ##
|
||||||
# See docs/CAPTCHA_SETUP for full details of configuring this.
|
|
||||||
|
|
||||||
# This Home Server's ReCAPTCHA public key.
|
# This Home Server's ReCAPTCHA public key.
|
||||||
recaptcha_public_key: "YOUR_PUBLIC_KEY"
|
recaptcha_public_key: "YOUR_PUBLIC_KEY"
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
# This file can't be called email.py because if it is, we cannot:
|
|
||||||
import email.utils
|
|
||||||
|
|
||||||
from ._base import Config
|
|
||||||
|
|
||||||
|
|
||||||
class EmailConfig(Config):
|
|
||||||
def read_config(self, config):
|
|
||||||
self.email_enable_notifs = False
|
|
||||||
|
|
||||||
email_config = config.get("email", {})
|
|
||||||
self.email_enable_notifs = email_config.get("enable_notifs", False)
|
|
||||||
|
|
||||||
if self.email_enable_notifs:
|
|
||||||
# make sure we can import the required deps
|
|
||||||
import jinja2
|
|
||||||
import bleach
|
|
||||||
# prevent unused warnings
|
|
||||||
jinja2
|
|
||||||
bleach
|
|
||||||
|
|
||||||
required = [
|
|
||||||
"smtp_host",
|
|
||||||
"smtp_port",
|
|
||||||
"notif_from",
|
|
||||||
"template_dir",
|
|
||||||
"notif_template_html",
|
|
||||||
"notif_template_text",
|
|
||||||
]
|
|
||||||
|
|
||||||
missing = []
|
|
||||||
for k in required:
|
|
||||||
if k not in email_config:
|
|
||||||
missing.append(k)
|
|
||||||
|
|
||||||
if (len(missing) > 0):
|
|
||||||
raise RuntimeError(
|
|
||||||
"email.enable_notifs is True but required keys are missing: %s" %
|
|
||||||
(", ".join(["email." + k for k in missing]),)
|
|
||||||
)
|
|
||||||
|
|
||||||
if config.get("public_baseurl") is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
"email.enable_notifs is True but no public_baseurl is set"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.email_smtp_host = email_config["smtp_host"]
|
|
||||||
self.email_smtp_port = email_config["smtp_port"]
|
|
||||||
self.email_notif_from = email_config["notif_from"]
|
|
||||||
self.email_template_dir = email_config["template_dir"]
|
|
||||||
self.email_notif_template_html = email_config["notif_template_html"]
|
|
||||||
self.email_notif_template_text = email_config["notif_template_text"]
|
|
||||||
self.email_notif_for_new_users = email_config.get(
|
|
||||||
"notif_for_new_users", True
|
|
||||||
)
|
|
||||||
self.email_riot_base_url = email_config.get(
|
|
||||||
"riot_base_url", None
|
|
||||||
)
|
|
||||||
if "app_name" in email_config:
|
|
||||||
self.email_app_name = email_config["app_name"]
|
|
||||||
else:
|
|
||||||
self.email_app_name = "Matrix"
|
|
||||||
|
|
||||||
# make sure it's valid
|
|
||||||
parsed = email.utils.parseaddr(self.email_notif_from)
|
|
||||||
if parsed[1] == '':
|
|
||||||
raise RuntimeError("Invalid notif_from address")
|
|
||||||
else:
|
|
||||||
self.email_enable_notifs = False
|
|
||||||
# Not much point setting defaults for the rest: it would be an
|
|
||||||
# error for them to be used.
|
|
||||||
|
|
||||||
def default_config(self, config_dir_path, server_name, **kwargs):
|
|
||||||
return """
|
|
||||||
# Enable sending emails for notification events
|
|
||||||
# Defining a custom URL for Riot is only needed if email notifications
|
|
||||||
# should contain links to a self-hosted installation of Riot; when set
|
|
||||||
# the "app_name" setting is ignored.
|
|
||||||
#email:
|
|
||||||
# enable_notifs: false
|
|
||||||
# smtp_host: "localhost"
|
|
||||||
# smtp_port: 25
|
|
||||||
# notif_from: "Your Friendly %(app)s Home Server <noreply@example.com>"
|
|
||||||
# app_name: Matrix
|
|
||||||
# template_dir: res/templates
|
|
||||||
# notif_template_html: notif_mail.html
|
|
||||||
# notif_template_text: notif_mail.txt
|
|
||||||
# notif_for_new_users: True
|
|
||||||
# riot_base_url: "http://localhost/riot"
|
|
||||||
"""
|
|
||||||
@@ -30,17 +30,14 @@ from .saml2 import SAML2Config
|
|||||||
from .cas import CasConfig
|
from .cas import CasConfig
|
||||||
from .password import PasswordConfig
|
from .password import PasswordConfig
|
||||||
from .jwt import JWTConfig
|
from .jwt import JWTConfig
|
||||||
from .password_auth_providers import PasswordAuthProviderConfig
|
from .ldap import LDAPConfig
|
||||||
from .emailconfig import EmailConfig
|
|
||||||
from .workers import WorkerConfig
|
|
||||||
|
|
||||||
|
|
||||||
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
||||||
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
|
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
|
||||||
VoipConfig, RegistrationConfig, MetricsConfig, ApiConfig,
|
VoipConfig, RegistrationConfig, MetricsConfig, ApiConfig,
|
||||||
AppServiceConfig, KeyConfig, SAML2Config, CasConfig,
|
AppServiceConfig, KeyConfig, SAML2Config, CasConfig,
|
||||||
JWTConfig, PasswordConfig, EmailConfig,
|
JWTConfig, LDAPConfig, PasswordConfig,):
|
||||||
WorkerConfig, PasswordAuthProviderConfig,):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,16 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from ._base import Config, ConfigError
|
from ._base import Config
|
||||||
|
|
||||||
|
|
||||||
MISSING_JWT = (
|
|
||||||
"""Missing jwt library. This is required for jwt login.
|
|
||||||
|
|
||||||
Install by running:
|
|
||||||
pip install pyjwt
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class JWTConfig(Config):
|
class JWTConfig(Config):
|
||||||
@@ -32,12 +23,6 @@ class JWTConfig(Config):
|
|||||||
self.jwt_enabled = jwt_config.get("enabled", False)
|
self.jwt_enabled = jwt_config.get("enabled", False)
|
||||||
self.jwt_secret = jwt_config["secret"]
|
self.jwt_secret = jwt_config["secret"]
|
||||||
self.jwt_algorithm = jwt_config["algorithm"]
|
self.jwt_algorithm = jwt_config["algorithm"]
|
||||||
|
|
||||||
try:
|
|
||||||
import jwt
|
|
||||||
jwt # To stop unused lint.
|
|
||||||
except ImportError:
|
|
||||||
raise ConfigError(MISSING_JWT)
|
|
||||||
else:
|
else:
|
||||||
self.jwt_enabled = False
|
self.jwt_enabled = False
|
||||||
self.jwt_secret = None
|
self.jwt_secret = None
|
||||||
@@ -45,8 +30,6 @@ class JWTConfig(Config):
|
|||||||
|
|
||||||
def default_config(self, **kwargs):
|
def default_config(self, **kwargs):
|
||||||
return """\
|
return """\
|
||||||
# The JWT needs to contain a globally unique "sub" (subject) claim.
|
|
||||||
#
|
|
||||||
# jwt_config:
|
# jwt_config:
|
||||||
# enabled: true
|
# enabled: true
|
||||||
# secret: "a secret"
|
# secret: "a secret"
|
||||||
|
|||||||
@@ -57,8 +57,6 @@ class KeyConfig(Config):
|
|||||||
seed = self.signing_key[0].seed
|
seed = self.signing_key[0].seed
|
||||||
self.macaroon_secret_key = hashlib.sha256(seed)
|
self.macaroon_secret_key = hashlib.sha256(seed)
|
||||||
|
|
||||||
self.expire_access_token = config.get("expire_access_token", False)
|
|
||||||
|
|
||||||
def default_config(self, config_dir_path, server_name, is_generating_file=False,
|
def default_config(self, config_dir_path, server_name, is_generating_file=False,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
base_key_name = os.path.join(config_dir_path, server_name)
|
base_key_name = os.path.join(config_dir_path, server_name)
|
||||||
@@ -71,9 +69,6 @@ class KeyConfig(Config):
|
|||||||
return """\
|
return """\
|
||||||
macaroon_secret_key: "%(macaroon_secret_key)s"
|
macaroon_secret_key: "%(macaroon_secret_key)s"
|
||||||
|
|
||||||
# Used to enable access token expiration.
|
|
||||||
expire_access_token: False
|
|
||||||
|
|
||||||
## Signing Keys ##
|
## Signing Keys ##
|
||||||
|
|
||||||
# Path to the signing key to sign messages with
|
# Path to the signing key to sign messages with
|
||||||
|
|||||||
52
synapse/config/ldap.py
Normal file
52
synapse/config/ldap.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 Niklas Riekenbrauck
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
|
||||||
|
|
||||||
|
class LDAPConfig(Config):
|
||||||
|
def read_config(self, config):
|
||||||
|
ldap_config = config.get("ldap_config", None)
|
||||||
|
if ldap_config:
|
||||||
|
self.ldap_enabled = ldap_config.get("enabled", False)
|
||||||
|
self.ldap_server = ldap_config["server"]
|
||||||
|
self.ldap_port = ldap_config["port"]
|
||||||
|
self.ldap_tls = ldap_config.get("tls", False)
|
||||||
|
self.ldap_search_base = ldap_config["search_base"]
|
||||||
|
self.ldap_search_property = ldap_config["search_property"]
|
||||||
|
self.ldap_email_property = ldap_config["email_property"]
|
||||||
|
self.ldap_full_name_property = ldap_config["full_name_property"]
|
||||||
|
else:
|
||||||
|
self.ldap_enabled = False
|
||||||
|
self.ldap_server = None
|
||||||
|
self.ldap_port = None
|
||||||
|
self.ldap_tls = False
|
||||||
|
self.ldap_search_base = None
|
||||||
|
self.ldap_search_property = None
|
||||||
|
self.ldap_email_property = None
|
||||||
|
self.ldap_full_name_property = None
|
||||||
|
|
||||||
|
def default_config(self, **kwargs):
|
||||||
|
return """\
|
||||||
|
# ldap_config:
|
||||||
|
# enabled: true
|
||||||
|
# server: "ldap://localhost"
|
||||||
|
# port: 389
|
||||||
|
# tls: false
|
||||||
|
# search_base: "ou=Users,dc=example,dc=com"
|
||||||
|
# search_property: "cn"
|
||||||
|
# email_property: "email"
|
||||||
|
# full_name_property: "givenName"
|
||||||
|
"""
|
||||||
@@ -15,13 +15,14 @@
|
|||||||
|
|
||||||
from ._base import Config
|
from ._base import Config
|
||||||
from synapse.util.logcontext import LoggingContextFilter
|
from synapse.util.logcontext import LoggingContextFilter
|
||||||
from twisted.logger import globalLogBeginner, STDLibLogObserver
|
from twisted.python.log import PythonLoggingObserver
|
||||||
import logging
|
import logging
|
||||||
import logging.config
|
import logging.config
|
||||||
import yaml
|
import yaml
|
||||||
from string import Template
|
from string import Template
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
|
from synapse.util.debug import debug_deferreds
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_LOG_CONFIG = Template("""
|
DEFAULT_LOG_CONFIG = Template("""
|
||||||
@@ -49,7 +50,6 @@ handlers:
|
|||||||
console:
|
console:
|
||||||
class: logging.StreamHandler
|
class: logging.StreamHandler
|
||||||
formatter: precise
|
formatter: precise
|
||||||
filters: [context]
|
|
||||||
|
|
||||||
loggers:
|
loggers:
|
||||||
synapse:
|
synapse:
|
||||||
@@ -70,6 +70,8 @@ class LoggingConfig(Config):
|
|||||||
self.verbosity = config.get("verbose", 0)
|
self.verbosity = config.get("verbose", 0)
|
||||||
self.log_config = self.abspath(config.get("log_config"))
|
self.log_config = self.abspath(config.get("log_config"))
|
||||||
self.log_file = self.abspath(config.get("log_file"))
|
self.log_file = self.abspath(config.get("log_file"))
|
||||||
|
if config.get("full_twisted_stacktraces"):
|
||||||
|
debug_deferreds()
|
||||||
|
|
||||||
def default_config(self, config_dir_path, server_name, **kwargs):
|
def default_config(self, config_dir_path, server_name, **kwargs):
|
||||||
log_file = self.abspath("homeserver.log")
|
log_file = self.abspath("homeserver.log")
|
||||||
@@ -85,6 +87,11 @@ class LoggingConfig(Config):
|
|||||||
|
|
||||||
# A yaml python logging config file
|
# A yaml python logging config file
|
||||||
log_config: "%(log_config)s"
|
log_config: "%(log_config)s"
|
||||||
|
|
||||||
|
# Stop twisted from discarding the stack traces of exceptions in
|
||||||
|
# deferreds by waiting a reactor tick before running a deferred's
|
||||||
|
# callbacks.
|
||||||
|
# full_twisted_stacktraces: true
|
||||||
""" % locals()
|
""" % locals()
|
||||||
|
|
||||||
def read_arguments(self, args):
|
def read_arguments(self, args):
|
||||||
@@ -119,68 +126,54 @@ class LoggingConfig(Config):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def setup_logging(self):
|
def setup_logging(self):
|
||||||
setup_logging(self.log_config, self.log_file, self.verbosity)
|
log_format = (
|
||||||
|
"%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s"
|
||||||
|
" - %(message)s"
|
||||||
|
)
|
||||||
|
if self.log_config is None:
|
||||||
|
|
||||||
|
level = logging.INFO
|
||||||
|
level_for_storage = logging.INFO
|
||||||
|
if self.verbosity:
|
||||||
|
level = logging.DEBUG
|
||||||
|
if self.verbosity > 1:
|
||||||
|
level_for_storage = logging.DEBUG
|
||||||
|
|
||||||
def setup_logging(log_config=None, log_file=None, verbosity=None):
|
# FIXME: we need a logging.WARN for a -q quiet option
|
||||||
log_format = (
|
logger = logging.getLogger('')
|
||||||
"%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s"
|
logger.setLevel(level)
|
||||||
" - %(message)s"
|
|
||||||
)
|
|
||||||
if log_config is None:
|
|
||||||
|
|
||||||
level = logging.INFO
|
logging.getLogger('synapse.storage').setLevel(level_for_storage)
|
||||||
level_for_storage = logging.INFO
|
|
||||||
if verbosity:
|
|
||||||
level = logging.DEBUG
|
|
||||||
if verbosity > 1:
|
|
||||||
level_for_storage = logging.DEBUG
|
|
||||||
|
|
||||||
# FIXME: we need a logging.WARN for a -q quiet option
|
formatter = logging.Formatter(log_format)
|
||||||
logger = logging.getLogger('')
|
if self.log_file:
|
||||||
logger.setLevel(level)
|
# TODO: Customisable file size / backup count
|
||||||
|
handler = logging.handlers.RotatingFileHandler(
|
||||||
|
self.log_file, maxBytes=(1000 * 1000 * 100), backupCount=3
|
||||||
|
)
|
||||||
|
|
||||||
logging.getLogger('synapse.storage').setLevel(level_for_storage)
|
def sighup(signum, stack):
|
||||||
|
logger.info("Closing log file due to SIGHUP")
|
||||||
|
handler.doRollover()
|
||||||
|
logger.info("Opened new log file due to SIGHUP")
|
||||||
|
|
||||||
formatter = logging.Formatter(log_format)
|
# TODO(paul): obviously this is a terrible mechanism for
|
||||||
if log_file:
|
# stealing SIGHUP, because it means no other part of synapse
|
||||||
# TODO: Customisable file size / backup count
|
# can use it instead. If we want to catch SIGHUP anywhere
|
||||||
handler = logging.handlers.RotatingFileHandler(
|
# else as well, I'd suggest we find a nicer way to broadcast
|
||||||
log_file, maxBytes=(1000 * 1000 * 100), backupCount=3
|
# it around.
|
||||||
)
|
if getattr(signal, "SIGHUP"):
|
||||||
|
signal.signal(signal.SIGHUP, sighup)
|
||||||
|
else:
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
|
||||||
def sighup(signum, stack):
|
handler.addFilter(LoggingContextFilter(request=""))
|
||||||
logger.info("Closing log file due to SIGHUP")
|
|
||||||
handler.doRollover()
|
|
||||||
logger.info("Opened new log file due to SIGHUP")
|
|
||||||
|
|
||||||
# TODO(paul): obviously this is a terrible mechanism for
|
logger.addHandler(handler)
|
||||||
# stealing SIGHUP, because it means no other part of synapse
|
|
||||||
# can use it instead. If we want to catch SIGHUP anywhere
|
|
||||||
# else as well, I'd suggest we find a nicer way to broadcast
|
|
||||||
# it around.
|
|
||||||
if getattr(signal, "SIGHUP"):
|
|
||||||
signal.signal(signal.SIGHUP, sighup)
|
|
||||||
else:
|
else:
|
||||||
handler = logging.StreamHandler()
|
with open(self.log_config, 'r') as f:
|
||||||
handler.setFormatter(formatter)
|
logging.config.dictConfig(yaml.load(f))
|
||||||
|
|
||||||
handler.addFilter(LoggingContextFilter(request=""))
|
observer = PythonLoggingObserver()
|
||||||
|
observer.start()
|
||||||
logger.addHandler(handler)
|
|
||||||
else:
|
|
||||||
with open(log_config, 'r') as f:
|
|
||||||
logging.config.dictConfig(yaml.load(f))
|
|
||||||
|
|
||||||
# It's critical to point twisted's internal logging somewhere, otherwise it
|
|
||||||
# stacks up and leaks kup to 64K object;
|
|
||||||
# see: https://twistedmatrix.com/trac/ticket/8164
|
|
||||||
#
|
|
||||||
# Routing to the python logging framework could be a performance problem if
|
|
||||||
# the handlers blocked for a long time as python.logging is a blocking API
|
|
||||||
# see https://twistedmatrix.com/documents/current/core/howto/logger.html
|
|
||||||
# filed as https://github.com/matrix-org/synapse/issues/1727
|
|
||||||
#
|
|
||||||
# However this may not be too much of a problem if we are just writing to a file.
|
|
||||||
observer = STDLibLogObserver()
|
|
||||||
globalLogBeginner.beginLoggingTo([observer])
|
|
||||||
|
|||||||
@@ -23,14 +23,10 @@ class PasswordConfig(Config):
|
|||||||
def read_config(self, config):
|
def read_config(self, config):
|
||||||
password_config = config.get("password_config", {})
|
password_config = config.get("password_config", {})
|
||||||
self.password_enabled = password_config.get("enabled", True)
|
self.password_enabled = password_config.get("enabled", True)
|
||||||
self.password_pepper = password_config.get("pepper", "")
|
|
||||||
|
|
||||||
def default_config(self, config_dir_path, server_name, **kwargs):
|
def default_config(self, config_dir_path, server_name, **kwargs):
|
||||||
return """
|
return """
|
||||||
# Enable password for login.
|
# Enable password for login.
|
||||||
password_config:
|
password_config:
|
||||||
enabled: true
|
enabled: true
|
||||||
# Uncomment and change to a secret random string for extra security.
|
|
||||||
# DO NOT CHANGE THIS AFTER INITIAL SETUP!
|
|
||||||
#pepper: ""
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2016 Openmarket
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from ._base import Config, ConfigError
|
|
||||||
|
|
||||||
import importlib
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordAuthProviderConfig(Config):
|
|
||||||
def read_config(self, config):
|
|
||||||
self.password_providers = []
|
|
||||||
|
|
||||||
# We want to be backwards compatible with the old `ldap_config`
|
|
||||||
# param.
|
|
||||||
ldap_config = config.get("ldap_config", {})
|
|
||||||
self.ldap_enabled = ldap_config.get("enabled", False)
|
|
||||||
if self.ldap_enabled:
|
|
||||||
from ldap_auth_provider import LdapAuthProvider
|
|
||||||
parsed_config = LdapAuthProvider.parse_config(ldap_config)
|
|
||||||
self.password_providers.append((LdapAuthProvider, parsed_config))
|
|
||||||
|
|
||||||
providers = config.get("password_providers", [])
|
|
||||||
for provider in providers:
|
|
||||||
# This is for backwards compat when the ldap auth provider resided
|
|
||||||
# in this package.
|
|
||||||
if provider['module'] == "synapse.util.ldap_auth_provider.LdapAuthProvider":
|
|
||||||
from ldap_auth_provider import LdapAuthProvider
|
|
||||||
provider_class = LdapAuthProvider
|
|
||||||
else:
|
|
||||||
# We need to import the module, and then pick the class out of
|
|
||||||
# that, so we split based on the last dot.
|
|
||||||
module, clz = provider['module'].rsplit(".", 1)
|
|
||||||
module = importlib.import_module(module)
|
|
||||||
provider_class = getattr(module, clz)
|
|
||||||
|
|
||||||
try:
|
|
||||||
provider_config = provider_class.parse_config(provider["config"])
|
|
||||||
except Exception as e:
|
|
||||||
raise ConfigError(
|
|
||||||
"Failed to parse config for %r: %r" % (provider['module'], e)
|
|
||||||
)
|
|
||||||
self.password_providers.append((provider_class, provider_config))
|
|
||||||
|
|
||||||
def default_config(self, **kwargs):
|
|
||||||
return """\
|
|
||||||
# password_providers:
|
|
||||||
# - module: "ldap_auth_provider.LdapAuthProvider"
|
|
||||||
# config:
|
|
||||||
# enabled: true
|
|
||||||
# uri: "ldap://ldap.example.com:389"
|
|
||||||
# start_tls: true
|
|
||||||
# base: "ou=users,dc=example,dc=com"
|
|
||||||
# attributes:
|
|
||||||
# uid: "cn"
|
|
||||||
# mail: "email"
|
|
||||||
# name: "givenName"
|
|
||||||
# #bind_dn:
|
|
||||||
# #bind_password:
|
|
||||||
# #filter: "(objectClass=posixAccount)"
|
|
||||||
"""
|
|
||||||
@@ -13,25 +13,9 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from ._base import Config, ConfigError
|
from ._base import Config
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
|
|
||||||
MISSING_NETADDR = (
|
|
||||||
"Missing netaddr library. This is required for URL preview API."
|
|
||||||
)
|
|
||||||
|
|
||||||
MISSING_LXML = (
|
|
||||||
"""Missing lxml library. This is required for URL preview API.
|
|
||||||
|
|
||||||
Install by running:
|
|
||||||
pip install lxml
|
|
||||||
|
|
||||||
Requires libxslt1-dev system package.
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
ThumbnailRequirement = namedtuple(
|
ThumbnailRequirement = namedtuple(
|
||||||
"ThumbnailRequirement", ["width", "height", "method", "media_type"]
|
"ThumbnailRequirement", ["width", "height", "method", "media_type"]
|
||||||
)
|
)
|
||||||
@@ -39,7 +23,7 @@ ThumbnailRequirement = namedtuple(
|
|||||||
|
|
||||||
def parse_thumbnail_requirements(thumbnail_sizes):
|
def parse_thumbnail_requirements(thumbnail_sizes):
|
||||||
""" Takes a list of dictionaries with "width", "height", and "method" keys
|
""" Takes a list of dictionaries with "width", "height", and "method" keys
|
||||||
and creates a map from image media types to the thumbnail size, thumbnailing
|
and creates a map from image media types to the thumbnail size, thumnailing
|
||||||
method, and thumbnail media type to precalculate
|
method, and thumbnail media type to precalculate
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -69,44 +53,12 @@ class ContentRepositoryConfig(Config):
|
|||||||
def read_config(self, config):
|
def read_config(self, config):
|
||||||
self.max_upload_size = self.parse_size(config["max_upload_size"])
|
self.max_upload_size = self.parse_size(config["max_upload_size"])
|
||||||
self.max_image_pixels = self.parse_size(config["max_image_pixels"])
|
self.max_image_pixels = self.parse_size(config["max_image_pixels"])
|
||||||
self.max_spider_size = self.parse_size(config["max_spider_size"])
|
|
||||||
self.media_store_path = self.ensure_directory(config["media_store_path"])
|
self.media_store_path = self.ensure_directory(config["media_store_path"])
|
||||||
self.uploads_path = self.ensure_directory(config["uploads_path"])
|
self.uploads_path = self.ensure_directory(config["uploads_path"])
|
||||||
self.dynamic_thumbnails = config["dynamic_thumbnails"]
|
self.dynamic_thumbnails = config["dynamic_thumbnails"]
|
||||||
self.thumbnail_requirements = parse_thumbnail_requirements(
|
self.thumbnail_requirements = parse_thumbnail_requirements(
|
||||||
config["thumbnail_sizes"]
|
config["thumbnail_sizes"]
|
||||||
)
|
)
|
||||||
self.url_preview_enabled = config.get("url_preview_enabled", False)
|
|
||||||
if self.url_preview_enabled:
|
|
||||||
try:
|
|
||||||
import lxml
|
|
||||||
lxml # To stop unused lint.
|
|
||||||
except ImportError:
|
|
||||||
raise ConfigError(MISSING_LXML)
|
|
||||||
|
|
||||||
try:
|
|
||||||
from netaddr import IPSet
|
|
||||||
except ImportError:
|
|
||||||
raise ConfigError(MISSING_NETADDR)
|
|
||||||
|
|
||||||
if "url_preview_ip_range_blacklist" in config:
|
|
||||||
self.url_preview_ip_range_blacklist = IPSet(
|
|
||||||
config["url_preview_ip_range_blacklist"]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ConfigError(
|
|
||||||
"For security, you must specify an explicit target IP address "
|
|
||||||
"blacklist in url_preview_ip_range_blacklist for url previewing "
|
|
||||||
"to work"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.url_preview_ip_range_whitelist = IPSet(
|
|
||||||
config.get("url_preview_ip_range_whitelist", ())
|
|
||||||
)
|
|
||||||
|
|
||||||
self.url_preview_url_blacklist = config.get(
|
|
||||||
"url_preview_url_blacklist", ()
|
|
||||||
)
|
|
||||||
|
|
||||||
def default_config(self, **kwargs):
|
def default_config(self, **kwargs):
|
||||||
media_store = self.default_path("media_store")
|
media_store = self.default_path("media_store")
|
||||||
@@ -128,7 +80,7 @@ class ContentRepositoryConfig(Config):
|
|||||||
# the resolution requested by the client. If true then whenever
|
# the resolution requested by the client. If true then whenever
|
||||||
# a new resolution is requested by the client the server will
|
# a new resolution is requested by the client the server will
|
||||||
# generate a new thumbnail. If false the server will pick a thumbnail
|
# generate a new thumbnail. If false the server will pick a thumbnail
|
||||||
# from a precalculated list.
|
# from a precalcualted list.
|
||||||
dynamic_thumbnails: false
|
dynamic_thumbnails: false
|
||||||
|
|
||||||
# List of thumbnail to precalculate when an image is uploaded.
|
# List of thumbnail to precalculate when an image is uploaded.
|
||||||
@@ -148,73 +100,4 @@ class ContentRepositoryConfig(Config):
|
|||||||
- width: 800
|
- width: 800
|
||||||
height: 600
|
height: 600
|
||||||
method: scale
|
method: scale
|
||||||
|
|
||||||
# Is the preview URL API enabled? If enabled, you *must* specify
|
|
||||||
# an explicit url_preview_ip_range_blacklist of IPs that the spider is
|
|
||||||
# denied from accessing.
|
|
||||||
url_preview_enabled: False
|
|
||||||
|
|
||||||
# List of IP address CIDR ranges that the URL preview spider is denied
|
|
||||||
# from accessing. There are no defaults: you must explicitly
|
|
||||||
# specify a list for URL previewing to work. You should specify any
|
|
||||||
# internal services in your network that you do not want synapse to try
|
|
||||||
# to connect to, otherwise anyone in any Matrix room could cause your
|
|
||||||
# synapse to issue arbitrary GET requests to your internal services,
|
|
||||||
# causing serious security issues.
|
|
||||||
#
|
|
||||||
# url_preview_ip_range_blacklist:
|
|
||||||
# - '127.0.0.0/8'
|
|
||||||
# - '10.0.0.0/8'
|
|
||||||
# - '172.16.0.0/12'
|
|
||||||
# - '192.168.0.0/16'
|
|
||||||
# - '100.64.0.0/10'
|
|
||||||
# - '169.254.0.0/16'
|
|
||||||
#
|
|
||||||
# List of IP address CIDR ranges that the URL preview spider is allowed
|
|
||||||
# to access even if they are specified in url_preview_ip_range_blacklist.
|
|
||||||
# This is useful for specifying exceptions to wide-ranging blacklisted
|
|
||||||
# target IP ranges - e.g. for enabling URL previews for a specific private
|
|
||||||
# website only visible in your network.
|
|
||||||
#
|
|
||||||
# url_preview_ip_range_whitelist:
|
|
||||||
# - '192.168.1.1'
|
|
||||||
|
|
||||||
# Optional list of URL matches that the URL preview spider is
|
|
||||||
# denied from accessing. You should use url_preview_ip_range_blacklist
|
|
||||||
# in preference to this, otherwise someone could define a public DNS
|
|
||||||
# entry that points to a private IP address and circumvent the blacklist.
|
|
||||||
# This is more useful if you know there is an entire shape of URL that
|
|
||||||
# you know that will never want synapse to try to spider.
|
|
||||||
#
|
|
||||||
# Each list entry is a dictionary of url component attributes as returned
|
|
||||||
# by urlparse.urlsplit as applied to the absolute form of the URL. See
|
|
||||||
# https://docs.python.org/2/library/urlparse.html#urlparse.urlsplit
|
|
||||||
# The values of the dictionary are treated as an filename match pattern
|
|
||||||
# applied to that component of URLs, unless they start with a ^ in which
|
|
||||||
# case they are treated as a regular expression match. If all the
|
|
||||||
# specified component matches for a given list item succeed, the URL is
|
|
||||||
# blacklisted.
|
|
||||||
#
|
|
||||||
# url_preview_url_blacklist:
|
|
||||||
# # blacklist any URL with a username in its URI
|
|
||||||
# - username: '*'
|
|
||||||
#
|
|
||||||
# # blacklist all *.google.com URLs
|
|
||||||
# - netloc: 'google.com'
|
|
||||||
# - netloc: '*.google.com'
|
|
||||||
#
|
|
||||||
# # blacklist all plain HTTP URLs
|
|
||||||
# - scheme: 'http'
|
|
||||||
#
|
|
||||||
# # blacklist http(s)://www.acme.com/foo
|
|
||||||
# - netloc: 'www.acme.com'
|
|
||||||
# path: '/foo'
|
|
||||||
#
|
|
||||||
# # blacklist any URL with a literal IPv4 address
|
|
||||||
# - netloc: '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'
|
|
||||||
|
|
||||||
# The largest allowed URL preview spidering size in bytes
|
|
||||||
max_spider_size: "10M"
|
|
||||||
|
|
||||||
|
|
||||||
""" % locals()
|
""" % locals()
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from ._base import Config, ConfigError
|
from ._base import Config
|
||||||
|
|
||||||
|
|
||||||
class ServerConfig(Config):
|
class ServerConfig(Config):
|
||||||
@@ -27,32 +27,10 @@ class ServerConfig(Config):
|
|||||||
self.daemonize = config.get("daemonize")
|
self.daemonize = config.get("daemonize")
|
||||||
self.print_pidfile = config.get("print_pidfile")
|
self.print_pidfile = config.get("print_pidfile")
|
||||||
self.user_agent_suffix = config.get("user_agent_suffix")
|
self.user_agent_suffix = config.get("user_agent_suffix")
|
||||||
self.use_frozen_dicts = config.get("use_frozen_dicts", False)
|
self.use_frozen_dicts = config.get("use_frozen_dicts", True)
|
||||||
self.public_baseurl = config.get("public_baseurl")
|
|
||||||
|
|
||||||
# Whether to send federation traffic out in this process. This only
|
|
||||||
# applies to some federation traffic, and so shouldn't be used to
|
|
||||||
# "disable" federation
|
|
||||||
self.send_federation = config.get("send_federation", True)
|
|
||||||
|
|
||||||
if self.public_baseurl is not None:
|
|
||||||
if self.public_baseurl[-1] != '/':
|
|
||||||
self.public_baseurl += '/'
|
|
||||||
self.start_pushers = config.get("start_pushers", True)
|
|
||||||
|
|
||||||
self.listeners = config.get("listeners", [])
|
self.listeners = config.get("listeners", [])
|
||||||
|
|
||||||
for listener in self.listeners:
|
|
||||||
bind_address = listener.pop("bind_address", None)
|
|
||||||
bind_addresses = listener.setdefault("bind_addresses", [])
|
|
||||||
|
|
||||||
if bind_address:
|
|
||||||
bind_addresses.append(bind_address)
|
|
||||||
elif not bind_addresses:
|
|
||||||
bind_addresses.append('')
|
|
||||||
|
|
||||||
self.gc_thresholds = read_gc_thresholds(config.get("gc_thresholds", None))
|
|
||||||
|
|
||||||
bind_port = config.get("bind_port")
|
bind_port = config.get("bind_port")
|
||||||
if bind_port:
|
if bind_port:
|
||||||
self.listeners = []
|
self.listeners = []
|
||||||
@@ -63,7 +41,7 @@ class ServerConfig(Config):
|
|||||||
|
|
||||||
self.listeners.append({
|
self.listeners.append({
|
||||||
"port": bind_port,
|
"port": bind_port,
|
||||||
"bind_addresses": [bind_host],
|
"bind_address": bind_host,
|
||||||
"tls": True,
|
"tls": True,
|
||||||
"type": "http",
|
"type": "http",
|
||||||
"resources": [
|
"resources": [
|
||||||
@@ -82,7 +60,7 @@ class ServerConfig(Config):
|
|||||||
if unsecure_port:
|
if unsecure_port:
|
||||||
self.listeners.append({
|
self.listeners.append({
|
||||||
"port": unsecure_port,
|
"port": unsecure_port,
|
||||||
"bind_addresses": [bind_host],
|
"bind_address": bind_host,
|
||||||
"tls": False,
|
"tls": False,
|
||||||
"type": "http",
|
"type": "http",
|
||||||
"resources": [
|
"resources": [
|
||||||
@@ -101,7 +79,7 @@ class ServerConfig(Config):
|
|||||||
if manhole:
|
if manhole:
|
||||||
self.listeners.append({
|
self.listeners.append({
|
||||||
"port": manhole,
|
"port": manhole,
|
||||||
"bind_addresses": ["127.0.0.1"],
|
"bind_address": "127.0.0.1",
|
||||||
"type": "manhole",
|
"type": "manhole",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -109,7 +87,7 @@ class ServerConfig(Config):
|
|||||||
if metrics_port:
|
if metrics_port:
|
||||||
self.listeners.append({
|
self.listeners.append({
|
||||||
"port": metrics_port,
|
"port": metrics_port,
|
||||||
"bind_addresses": [config.get("metrics_bind_host", "127.0.0.1")],
|
"bind_address": config.get("metrics_bind_host", "127.0.0.1"),
|
||||||
"tls": False,
|
"tls": False,
|
||||||
"type": "http",
|
"type": "http",
|
||||||
"resources": [
|
"resources": [
|
||||||
@@ -120,6 +98,26 @@ class ServerConfig(Config):
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Attempt to guess the content_addr for the v0 content repostitory
|
||||||
|
content_addr = config.get("content_addr")
|
||||||
|
if not content_addr:
|
||||||
|
for listener in self.listeners:
|
||||||
|
if listener["type"] == "http" and not listener.get("tls", False):
|
||||||
|
unsecure_port = listener["port"]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Could not determine 'content_addr'")
|
||||||
|
|
||||||
|
host = self.server_name
|
||||||
|
if ':' not in host:
|
||||||
|
host = "%s:%d" % (host, unsecure_port)
|
||||||
|
else:
|
||||||
|
host = host.split(':')[0]
|
||||||
|
host = "%s:%d" % (host, unsecure_port)
|
||||||
|
content_addr = "http://%s" % (host,)
|
||||||
|
|
||||||
|
self.content_addr = content_addr
|
||||||
|
|
||||||
def default_config(self, server_name, **kwargs):
|
def default_config(self, server_name, **kwargs):
|
||||||
if ":" in server_name:
|
if ":" in server_name:
|
||||||
bind_port = int(server_name.split(":")[1])
|
bind_port = int(server_name.split(":")[1])
|
||||||
@@ -144,17 +142,11 @@ class ServerConfig(Config):
|
|||||||
# Whether to serve a web client from the HTTP/HTTPS root resource.
|
# Whether to serve a web client from the HTTP/HTTPS root resource.
|
||||||
web_client: True
|
web_client: True
|
||||||
|
|
||||||
# The public-facing base URL for the client API (not including _matrix/...)
|
|
||||||
# public_baseurl: https://example.com:8448/
|
|
||||||
|
|
||||||
# Set the soft limit on the number of file descriptors synapse can use
|
# Set the soft limit on the number of file descriptors synapse can use
|
||||||
# Zero is used to indicate synapse should set the soft limit to the
|
# Zero is used to indicate synapse should set the soft limit to the
|
||||||
# hard limit.
|
# hard limit.
|
||||||
soft_file_limit: 0
|
soft_file_limit: 0
|
||||||
|
|
||||||
# The GC threshold parameters to pass to `gc.set_threshold`, if defined
|
|
||||||
# gc_thresholds: [700, 10, 10]
|
|
||||||
|
|
||||||
# List of ports that Synapse should listen on, their purpose and their
|
# List of ports that Synapse should listen on, their purpose and their
|
||||||
# configuration.
|
# configuration.
|
||||||
listeners:
|
listeners:
|
||||||
@@ -164,14 +156,9 @@ class ServerConfig(Config):
|
|||||||
# The port to listen for HTTPS requests on.
|
# The port to listen for HTTPS requests on.
|
||||||
port: %(bind_port)s
|
port: %(bind_port)s
|
||||||
|
|
||||||
# Local addresses to listen on.
|
# Local interface to listen on.
|
||||||
# This will listen on all IPv4 addresses by default.
|
# The empty string will cause synapse to listen on all interfaces.
|
||||||
bind_addresses:
|
bind_address: ''
|
||||||
- '0.0.0.0'
|
|
||||||
# Uncomment to listen on all IPv6 interfaces
|
|
||||||
# N.B: On at least Linux this will also listen on all IPv4
|
|
||||||
# addresses, so you will need to comment out the line above.
|
|
||||||
# - '::'
|
|
||||||
|
|
||||||
# This is a 'http' listener, allows us to specify 'resources'.
|
# This is a 'http' listener, allows us to specify 'resources'.
|
||||||
type: http
|
type: http
|
||||||
@@ -202,7 +189,7 @@ class ServerConfig(Config):
|
|||||||
# For when matrix traffic passes through loadbalancer that unwraps TLS.
|
# For when matrix traffic passes through loadbalancer that unwraps TLS.
|
||||||
- port: %(unsecure_port)s
|
- port: %(unsecure_port)s
|
||||||
tls: false
|
tls: false
|
||||||
bind_addresses: ['0.0.0.0']
|
bind_address: ''
|
||||||
type: http
|
type: http
|
||||||
|
|
||||||
x_forwarded: false
|
x_forwarded: false
|
||||||
@@ -241,20 +228,3 @@ class ServerConfig(Config):
|
|||||||
type=int,
|
type=int,
|
||||||
help="Turn on the twisted telnet manhole"
|
help="Turn on the twisted telnet manhole"
|
||||||
" service on the given port.")
|
" service on the given port.")
|
||||||
|
|
||||||
|
|
||||||
def read_gc_thresholds(thresholds):
|
|
||||||
"""Reads the three integer thresholds for garbage collection. Ensures that
|
|
||||||
the thresholds are integers if thresholds are supplied.
|
|
||||||
"""
|
|
||||||
if thresholds is None:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
assert len(thresholds) == 3
|
|
||||||
return (
|
|
||||||
int(thresholds[0]), int(thresholds[1]), int(thresholds[2]),
|
|
||||||
)
|
|
||||||
except:
|
|
||||||
raise ConfigError(
|
|
||||||
"Value of `gc_threshold` must be a list of three integers if set"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ from OpenSSL import crypto
|
|||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from hashlib import sha256
|
|
||||||
from unpaddedbase64 import encode_base64
|
|
||||||
|
|
||||||
GENERATE_DH_PARAMS = False
|
GENERATE_DH_PARAMS = False
|
||||||
|
|
||||||
|
|
||||||
@@ -45,19 +42,6 @@ class TlsConfig(Config):
|
|||||||
config.get("tls_dh_params_path"), "tls_dh_params"
|
config.get("tls_dh_params_path"), "tls_dh_params"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.tls_fingerprints = config["tls_fingerprints"]
|
|
||||||
|
|
||||||
# Check that our own certificate is included in the list of fingerprints
|
|
||||||
# and include it if it is not.
|
|
||||||
x509_certificate_bytes = crypto.dump_certificate(
|
|
||||||
crypto.FILETYPE_ASN1,
|
|
||||||
self.tls_certificate
|
|
||||||
)
|
|
||||||
sha256_fingerprint = encode_base64(sha256(x509_certificate_bytes).digest())
|
|
||||||
sha256_fingerprints = set(f["sha256"] for f in self.tls_fingerprints)
|
|
||||||
if sha256_fingerprint not in sha256_fingerprints:
|
|
||||||
self.tls_fingerprints.append({u"sha256": sha256_fingerprint})
|
|
||||||
|
|
||||||
# This config option applies to non-federation HTTP clients
|
# This config option applies to non-federation HTTP clients
|
||||||
# (e.g. for talking to recaptcha, identity servers, and such)
|
# (e.g. for talking to recaptcha, identity servers, and such)
|
||||||
# It should never be used in production, and is intended for
|
# It should never be used in production, and is intended for
|
||||||
@@ -89,28 +73,6 @@ class TlsConfig(Config):
|
|||||||
|
|
||||||
# Don't bind to the https port
|
# Don't bind to the https port
|
||||||
no_tls: False
|
no_tls: False
|
||||||
|
|
||||||
# List of allowed TLS fingerprints for this server to publish along
|
|
||||||
# with the signing keys for this server. Other matrix servers that
|
|
||||||
# make HTTPS requests to this server will check that the TLS
|
|
||||||
# certificates returned by this server match one of the fingerprints.
|
|
||||||
#
|
|
||||||
# Synapse automatically adds its the fingerprint of its own certificate
|
|
||||||
# to the list. So if federation traffic is handle directly by synapse
|
|
||||||
# then no modification to the list is required.
|
|
||||||
#
|
|
||||||
# If synapse is run behind a load balancer that handles the TLS then it
|
|
||||||
# will be necessary to add the fingerprints of the certificates used by
|
|
||||||
# the loadbalancers to this list if they are different to the one
|
|
||||||
# synapse is using.
|
|
||||||
#
|
|
||||||
# Homeservers are permitted to cache the list of TLS fingerprints
|
|
||||||
# returned in the key responses up to the "valid_until_ts" returned in
|
|
||||||
# key. It may be necessary to publish the fingerprints of a new
|
|
||||||
# certificate and wait until the "valid_until_ts" of the previous key
|
|
||||||
# responses have passed before deploying it.
|
|
||||||
tls_fingerprints: []
|
|
||||||
# tls_fingerprints: [{"sha256": "<base64_encoded_sha256_fingerprint>"}]
|
|
||||||
""" % locals()
|
""" % locals()
|
||||||
|
|
||||||
def read_tls_certificate(self, cert_path):
|
def read_tls_certificate(self, cert_path):
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ class VoipConfig(Config):
|
|||||||
|
|
||||||
def read_config(self, config):
|
def read_config(self, config):
|
||||||
self.turn_uris = config.get("turn_uris", [])
|
self.turn_uris = config.get("turn_uris", [])
|
||||||
self.turn_shared_secret = config.get("turn_shared_secret")
|
self.turn_shared_secret = config["turn_shared_secret"]
|
||||||
self.turn_username = config.get("turn_username")
|
|
||||||
self.turn_password = config.get("turn_password")
|
|
||||||
self.turn_user_lifetime = self.parse_duration(config["turn_user_lifetime"])
|
self.turn_user_lifetime = self.parse_duration(config["turn_user_lifetime"])
|
||||||
|
|
||||||
def default_config(self, **kwargs):
|
def default_config(self, **kwargs):
|
||||||
@@ -34,11 +32,6 @@ class VoipConfig(Config):
|
|||||||
# The shared secret used to compute passwords for the TURN server
|
# The shared secret used to compute passwords for the TURN server
|
||||||
turn_shared_secret: "YOUR_SHARED_SECRET"
|
turn_shared_secret: "YOUR_SHARED_SECRET"
|
||||||
|
|
||||||
# The Username and password if the TURN server needs them and
|
|
||||||
# does not use a token
|
|
||||||
#turn_username: "TURNSERVER_USERNAME"
|
|
||||||
#turn_password: "TURNSERVER_PASSWORD"
|
|
||||||
|
|
||||||
# How long generated TURN credentials last
|
# How long generated TURN credentials last
|
||||||
turn_user_lifetime: "1h"
|
turn_user_lifetime: "1h"
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2016 matrix.org
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from ._base import Config
|
|
||||||
|
|
||||||
|
|
||||||
class WorkerConfig(Config):
|
|
||||||
"""The workers are processes run separately to the main synapse process.
|
|
||||||
They have their own pid_file and listener configuration. They use the
|
|
||||||
replication_url to talk to the main synapse process."""
|
|
||||||
|
|
||||||
def read_config(self, config):
|
|
||||||
self.worker_app = config.get("worker_app")
|
|
||||||
self.worker_listeners = config.get("worker_listeners")
|
|
||||||
self.worker_daemonize = config.get("worker_daemonize")
|
|
||||||
self.worker_pid_file = config.get("worker_pid_file")
|
|
||||||
self.worker_log_file = config.get("worker_log_file")
|
|
||||||
self.worker_log_config = config.get("worker_log_config")
|
|
||||||
self.worker_replication_url = config.get("worker_replication_url")
|
|
||||||
|
|
||||||
if self.worker_listeners:
|
|
||||||
for listener in self.worker_listeners:
|
|
||||||
bind_address = listener.pop("bind_address", None)
|
|
||||||
bind_addresses = listener.setdefault("bind_addresses", [])
|
|
||||||
|
|
||||||
if bind_address:
|
|
||||||
bind_addresses.append(bind_address)
|
|
||||||
elif not bind_addresses:
|
|
||||||
bind_addresses.append('')
|
|
||||||
@@ -77,12 +77,10 @@ class SynapseKeyClientProtocol(HTTPClient):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.remote_key = defer.Deferred()
|
self.remote_key = defer.Deferred()
|
||||||
self.host = None
|
self.host = None
|
||||||
self._peer = None
|
|
||||||
|
|
||||||
def connectionMade(self):
|
def connectionMade(self):
|
||||||
self._peer = self.transport.getPeer()
|
self.host = self.transport.getHost()
|
||||||
logger.debug("Connected to %s", self._peer)
|
logger.debug("Connected to %s", self.host)
|
||||||
|
|
||||||
self.sendCommand(b"GET", self.path)
|
self.sendCommand(b"GET", self.path)
|
||||||
if self.host:
|
if self.host:
|
||||||
self.sendHeader(b"Host", self.host)
|
self.sendHeader(b"Host", self.host)
|
||||||
@@ -126,10 +124,7 @@ class SynapseKeyClientProtocol(HTTPClient):
|
|||||||
self.timer.cancel()
|
self.timer.cancel()
|
||||||
|
|
||||||
def on_timeout(self):
|
def on_timeout(self):
|
||||||
logger.debug(
|
logger.debug("Timeout waiting for response from %s", self.host)
|
||||||
"Timeout waiting for response from %s: %s",
|
|
||||||
self.host, self._peer,
|
|
||||||
)
|
|
||||||
self.errback(IOError("Timeout waiting for response"))
|
self.errback(IOError("Timeout waiting for response"))
|
||||||
self.transport.abortConnection()
|
self.transport.abortConnection()
|
||||||
|
|
||||||
@@ -138,5 +133,4 @@ class SynapseKeyClientFactory(Factory):
|
|||||||
def protocol(self):
|
def protocol(self):
|
||||||
protocol = SynapseKeyClientProtocol()
|
protocol = SynapseKeyClientProtocol()
|
||||||
protocol.path = self.path
|
protocol.path = self.path
|
||||||
protocol.host = self.host
|
|
||||||
return protocol
|
return protocol
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ from synapse.util.logcontext import (
|
|||||||
preserve_context_over_deferred, preserve_context_over_fn, PreserveLoggingContext,
|
preserve_context_over_deferred, preserve_context_over_fn, PreserveLoggingContext,
|
||||||
preserve_fn
|
preserve_fn
|
||||||
)
|
)
|
||||||
from synapse.util.metrics import Measure
|
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
@@ -45,25 +44,7 @@ import logging
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
VerifyKeyRequest = namedtuple("VerifyRequest", (
|
KeyGroup = namedtuple("KeyGroup", ("server_name", "group_id", "key_ids"))
|
||||||
"server_name", "key_ids", "json_object", "deferred"
|
|
||||||
))
|
|
||||||
"""
|
|
||||||
A request for a verify key to verify a JSON object.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
server_name(str): The name of the server to verify against.
|
|
||||||
key_ids(set(str)): The set of key_ids to that could be used to verify the
|
|
||||||
JSON object
|
|
||||||
json_object(dict): The JSON object to verify.
|
|
||||||
deferred(twisted.internet.defer.Deferred):
|
|
||||||
A deferred (server_name, key_id, verify_key) tuple that resolves when
|
|
||||||
a verify key has been fetched
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class KeyLookupError(ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Keyring(object):
|
class Keyring(object):
|
||||||
@@ -93,32 +74,39 @@ class Keyring(object):
|
|||||||
list of deferreds indicating success or failure to verify each
|
list of deferreds indicating success or failure to verify each
|
||||||
json object's signature for the given server_name.
|
json object's signature for the given server_name.
|
||||||
"""
|
"""
|
||||||
verify_requests = []
|
group_id_to_json = {}
|
||||||
|
group_id_to_group = {}
|
||||||
|
group_ids = []
|
||||||
|
|
||||||
|
next_group_id = 0
|
||||||
|
deferreds = {}
|
||||||
|
|
||||||
for server_name, json_object in server_and_json:
|
for server_name, json_object in server_and_json:
|
||||||
logger.debug("Verifying for %s", server_name)
|
logger.debug("Verifying for %s", server_name)
|
||||||
|
group_id = next_group_id
|
||||||
|
next_group_id += 1
|
||||||
|
group_ids.append(group_id)
|
||||||
|
|
||||||
key_ids = signature_ids(json_object, server_name)
|
key_ids = signature_ids(json_object, server_name)
|
||||||
if not key_ids:
|
if not key_ids:
|
||||||
deferred = defer.fail(SynapseError(
|
deferreds[group_id] = defer.fail(SynapseError(
|
||||||
400,
|
400,
|
||||||
"Not signed with a supported algorithm",
|
"Not signed with a supported algorithm",
|
||||||
Codes.UNAUTHORIZED,
|
Codes.UNAUTHORIZED,
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
deferred = defer.Deferred()
|
deferreds[group_id] = defer.Deferred()
|
||||||
|
|
||||||
verify_request = VerifyKeyRequest(
|
group = KeyGroup(server_name, group_id, key_ids)
|
||||||
server_name, key_ids, json_object, deferred
|
|
||||||
)
|
|
||||||
|
|
||||||
verify_requests.append(verify_request)
|
group_id_to_group[group_id] = group
|
||||||
|
group_id_to_json[group_id] = json_object
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def handle_key_deferred(verify_request):
|
def handle_key_deferred(group, deferred):
|
||||||
server_name = verify_request.server_name
|
server_name = group.server_name
|
||||||
try:
|
try:
|
||||||
_, key_id, verify_key = yield verify_request.deferred
|
_, _, key_id, verify_key = yield deferred
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"Got IOError when downloading keys for %s: %s %s",
|
"Got IOError when downloading keys for %s: %s %s",
|
||||||
@@ -140,7 +128,7 @@ class Keyring(object):
|
|||||||
Codes.UNAUTHORIZED,
|
Codes.UNAUTHORIZED,
|
||||||
)
|
)
|
||||||
|
|
||||||
json_object = verify_request.json_object
|
json_object = group_id_to_json[group.group_id]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
verify_signed_json(json_object, server_name, verify_key)
|
verify_signed_json(json_object, server_name, verify_key)
|
||||||
@@ -169,34 +157,36 @@ class Keyring(object):
|
|||||||
|
|
||||||
# Actually start fetching keys.
|
# Actually start fetching keys.
|
||||||
wait_on_deferred.addBoth(
|
wait_on_deferred.addBoth(
|
||||||
lambda _: self.get_server_verify_keys(verify_requests)
|
lambda _: self.get_server_verify_keys(group_id_to_group, deferreds)
|
||||||
)
|
)
|
||||||
|
|
||||||
# When we've finished fetching all the keys for a given server_name,
|
# When we've finished fetching all the keys for a given server_name,
|
||||||
# resolve the deferred passed to `wait_for_previous_lookups` so that
|
# resolve the deferred passed to `wait_for_previous_lookups` so that
|
||||||
# any lookups waiting will proceed.
|
# any lookups waiting will proceed.
|
||||||
server_to_request_ids = {}
|
server_to_gids = {}
|
||||||
|
|
||||||
def remove_deferreds(res, server_name, verify_request):
|
def remove_deferreds(res, server_name, group_id):
|
||||||
request_id = id(verify_request)
|
server_to_gids[server_name].discard(group_id)
|
||||||
server_to_request_ids[server_name].discard(request_id)
|
if not server_to_gids[server_name]:
|
||||||
if not server_to_request_ids[server_name]:
|
|
||||||
d = server_to_deferred.pop(server_name, None)
|
d = server_to_deferred.pop(server_name, None)
|
||||||
if d:
|
if d:
|
||||||
d.callback(None)
|
d.callback(None)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
for verify_request in verify_requests:
|
for g_id, deferred in deferreds.items():
|
||||||
server_name = verify_request.server_name
|
server_name = group_id_to_group[g_id].server_name
|
||||||
request_id = id(verify_request)
|
server_to_gids.setdefault(server_name, set()).add(g_id)
|
||||||
server_to_request_ids.setdefault(server_name, set()).add(request_id)
|
deferred.addBoth(remove_deferreds, server_name, g_id)
|
||||||
deferred.addBoth(remove_deferreds, server_name, verify_request)
|
|
||||||
|
|
||||||
# Pass those keys to handle_key_deferred so that the json object
|
# Pass those keys to handle_key_deferred so that the json object
|
||||||
# signatures can be verified
|
# signatures can be verified
|
||||||
return [
|
return [
|
||||||
preserve_context_over_fn(handle_key_deferred, verify_request)
|
preserve_context_over_fn(
|
||||||
for verify_request in verify_requests
|
handle_key_deferred,
|
||||||
|
group_id_to_group[g_id],
|
||||||
|
deferreds[g_id],
|
||||||
|
)
|
||||||
|
for g_id in group_ids
|
||||||
]
|
]
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@@ -230,7 +220,7 @@ class Keyring(object):
|
|||||||
|
|
||||||
d.addBoth(rm, server_name)
|
d.addBoth(rm, server_name)
|
||||||
|
|
||||||
def get_server_verify_keys(self, verify_requests):
|
def get_server_verify_keys(self, group_id_to_group, group_id_to_deferred):
|
||||||
"""Takes a dict of KeyGroups and tries to find at least one key for
|
"""Takes a dict of KeyGroups and tries to find at least one key for
|
||||||
each group.
|
each group.
|
||||||
"""
|
"""
|
||||||
@@ -244,79 +234,76 @@ class Keyring(object):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def do_iterations():
|
def do_iterations():
|
||||||
with Measure(self.clock, "get_server_verify_keys"):
|
merged_results = {}
|
||||||
merged_results = {}
|
|
||||||
|
|
||||||
missing_keys = {}
|
missing_keys = {}
|
||||||
for verify_request in verify_requests:
|
for group in group_id_to_group.values():
|
||||||
missing_keys.setdefault(verify_request.server_name, set()).update(
|
missing_keys.setdefault(group.server_name, set()).update(
|
||||||
verify_request.key_ids
|
group.key_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
for fn in key_fetch_fns:
|
||||||
|
results = yield fn(missing_keys.items())
|
||||||
|
merged_results.update(results)
|
||||||
|
|
||||||
|
# We now need to figure out which groups we have keys for
|
||||||
|
# and which we don't
|
||||||
|
missing_groups = {}
|
||||||
|
for group in group_id_to_group.values():
|
||||||
|
for key_id in group.key_ids:
|
||||||
|
if key_id in merged_results[group.server_name]:
|
||||||
|
with PreserveLoggingContext():
|
||||||
|
group_id_to_deferred[group.group_id].callback((
|
||||||
|
group.group_id,
|
||||||
|
group.server_name,
|
||||||
|
key_id,
|
||||||
|
merged_results[group.server_name][key_id],
|
||||||
|
))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
missing_groups.setdefault(
|
||||||
|
group.server_name, []
|
||||||
|
).append(group)
|
||||||
|
|
||||||
|
if not missing_groups:
|
||||||
|
break
|
||||||
|
|
||||||
|
missing_keys = {
|
||||||
|
server_name: set(
|
||||||
|
key_id for group in groups for key_id in group.key_ids
|
||||||
)
|
)
|
||||||
|
for server_name, groups in missing_groups.items()
|
||||||
|
}
|
||||||
|
|
||||||
for fn in key_fetch_fns:
|
for group in missing_groups.values():
|
||||||
results = yield fn(missing_keys.items())
|
group_id_to_deferred[group.group_id].errback(SynapseError(
|
||||||
merged_results.update(results)
|
401,
|
||||||
|
"No key for %s with id %s" % (
|
||||||
# We now need to figure out which verify requests we have keys
|
group.server_name, group.key_ids,
|
||||||
# for and which we don't
|
),
|
||||||
missing_keys = {}
|
Codes.UNAUTHORIZED,
|
||||||
requests_missing_keys = []
|
))
|
||||||
for verify_request in verify_requests:
|
|
||||||
server_name = verify_request.server_name
|
|
||||||
result_keys = merged_results[server_name]
|
|
||||||
|
|
||||||
if verify_request.deferred.called:
|
|
||||||
# We've already called this deferred, which probably
|
|
||||||
# means that we've already found a key for it.
|
|
||||||
continue
|
|
||||||
|
|
||||||
for key_id in verify_request.key_ids:
|
|
||||||
if key_id in result_keys:
|
|
||||||
with PreserveLoggingContext():
|
|
||||||
verify_request.deferred.callback((
|
|
||||||
server_name,
|
|
||||||
key_id,
|
|
||||||
result_keys[key_id],
|
|
||||||
))
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# The else block is only reached if the loop above
|
|
||||||
# doesn't break.
|
|
||||||
missing_keys.setdefault(server_name, set()).update(
|
|
||||||
verify_request.key_ids
|
|
||||||
)
|
|
||||||
requests_missing_keys.append(verify_request)
|
|
||||||
|
|
||||||
if not missing_keys:
|
|
||||||
break
|
|
||||||
|
|
||||||
for verify_request in requests_missing_keys.values():
|
|
||||||
verify_request.deferred.errback(SynapseError(
|
|
||||||
401,
|
|
||||||
"No key for %s with id %s" % (
|
|
||||||
verify_request.server_name, verify_request.key_ids,
|
|
||||||
),
|
|
||||||
Codes.UNAUTHORIZED,
|
|
||||||
))
|
|
||||||
|
|
||||||
def on_err(err):
|
def on_err(err):
|
||||||
for verify_request in verify_requests:
|
for deferred in group_id_to_deferred.values():
|
||||||
if not verify_request.deferred.called:
|
if not deferred.called:
|
||||||
verify_request.deferred.errback(err)
|
deferred.errback(err)
|
||||||
|
|
||||||
do_iterations().addErrback(on_err)
|
do_iterations().addErrback(on_err)
|
||||||
|
|
||||||
|
return group_id_to_deferred
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_keys_from_store(self, server_name_and_key_ids):
|
def get_keys_from_store(self, server_name_and_key_ids):
|
||||||
res = yield preserve_context_over_deferred(defer.gatherResults(
|
res = yield defer.gatherResults(
|
||||||
[
|
[
|
||||||
preserve_fn(self.store.get_server_verify_keys)(
|
self.store.get_server_verify_keys(
|
||||||
server_name, key_ids
|
server_name, key_ids
|
||||||
).addCallback(lambda ks, server: (server, ks), server_name)
|
).addCallback(lambda ks, server: (server, ks), server_name)
|
||||||
for server_name, key_ids in server_name_and_key_ids
|
for server_name, key_ids in server_name_and_key_ids
|
||||||
],
|
],
|
||||||
consumeErrors=True,
|
consumeErrors=True,
|
||||||
)).addErrback(unwrapFirstError)
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
defer.returnValue(dict(res))
|
defer.returnValue(dict(res))
|
||||||
|
|
||||||
@@ -337,13 +324,13 @@ class Keyring(object):
|
|||||||
)
|
)
|
||||||
defer.returnValue({})
|
defer.returnValue({})
|
||||||
|
|
||||||
results = yield preserve_context_over_deferred(defer.gatherResults(
|
results = yield defer.gatherResults(
|
||||||
[
|
[
|
||||||
preserve_fn(get_key)(p_name, p_keys)
|
get_key(p_name, p_keys)
|
||||||
for p_name, p_keys in self.perspective_servers.items()
|
for p_name, p_keys in self.perspective_servers.items()
|
||||||
],
|
],
|
||||||
consumeErrors=True,
|
consumeErrors=True,
|
||||||
)).addErrback(unwrapFirstError)
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
union_of_keys = {}
|
union_of_keys = {}
|
||||||
for result in results:
|
for result in results:
|
||||||
@@ -369,7 +356,7 @@ class Keyring(object):
|
|||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Unable to get key %r for %r directly: %s %s",
|
"Unable to getting key %r for %r directly: %s %s",
|
||||||
key_ids, server_name,
|
key_ids, server_name,
|
||||||
type(e).__name__, str(e.message),
|
type(e).__name__, str(e.message),
|
||||||
)
|
)
|
||||||
@@ -383,13 +370,13 @@ class Keyring(object):
|
|||||||
|
|
||||||
defer.returnValue(keys)
|
defer.returnValue(keys)
|
||||||
|
|
||||||
results = yield preserve_context_over_deferred(defer.gatherResults(
|
results = yield defer.gatherResults(
|
||||||
[
|
[
|
||||||
preserve_fn(get_key)(server_name, key_ids)
|
get_key(server_name, key_ids)
|
||||||
for server_name, key_ids in server_name_and_key_ids
|
for server_name, key_ids in server_name_and_key_ids
|
||||||
],
|
],
|
||||||
consumeErrors=True,
|
consumeErrors=True,
|
||||||
)).addErrback(unwrapFirstError)
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
merged = {}
|
merged = {}
|
||||||
for result in results:
|
for result in results:
|
||||||
@@ -431,7 +418,7 @@ class Keyring(object):
|
|||||||
for response in responses:
|
for response in responses:
|
||||||
if (u"signatures" not in response
|
if (u"signatures" not in response
|
||||||
or perspective_name not in response[u"signatures"]):
|
or perspective_name not in response[u"signatures"]):
|
||||||
raise KeyLookupError(
|
raise ValueError(
|
||||||
"Key response not signed by perspective server"
|
"Key response not signed by perspective server"
|
||||||
" %r" % (perspective_name,)
|
" %r" % (perspective_name,)
|
||||||
)
|
)
|
||||||
@@ -454,21 +441,21 @@ class Keyring(object):
|
|||||||
list(response[u"signatures"][perspective_name]),
|
list(response[u"signatures"][perspective_name]),
|
||||||
list(perspective_keys)
|
list(perspective_keys)
|
||||||
)
|
)
|
||||||
raise KeyLookupError(
|
raise ValueError(
|
||||||
"Response not signed with a known key for perspective"
|
"Response not signed with a known key for perspective"
|
||||||
" server %r" % (perspective_name,)
|
" server %r" % (perspective_name,)
|
||||||
)
|
)
|
||||||
|
|
||||||
processed_response = yield self.process_v2_response(
|
processed_response = yield self.process_v2_response(
|
||||||
perspective_name, response, only_from_server=False
|
perspective_name, response
|
||||||
)
|
)
|
||||||
|
|
||||||
for server_name, response_keys in processed_response.items():
|
for server_name, response_keys in processed_response.items():
|
||||||
keys.setdefault(server_name, {}).update(response_keys)
|
keys.setdefault(server_name, {}).update(response_keys)
|
||||||
|
|
||||||
yield preserve_context_over_deferred(defer.gatherResults(
|
yield defer.gatherResults(
|
||||||
[
|
[
|
||||||
preserve_fn(self.store_keys)(
|
self.store_keys(
|
||||||
server_name=server_name,
|
server_name=server_name,
|
||||||
from_server=perspective_name,
|
from_server=perspective_name,
|
||||||
verify_keys=response_keys,
|
verify_keys=response_keys,
|
||||||
@@ -476,7 +463,7 @@ class Keyring(object):
|
|||||||
for server_name, response_keys in keys.items()
|
for server_name, response_keys in keys.items()
|
||||||
],
|
],
|
||||||
consumeErrors=True
|
consumeErrors=True
|
||||||
)).addErrback(unwrapFirstError)
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
defer.returnValue(keys)
|
defer.returnValue(keys)
|
||||||
|
|
||||||
@@ -497,10 +484,10 @@ class Keyring(object):
|
|||||||
|
|
||||||
if (u"signatures" not in response
|
if (u"signatures" not in response
|
||||||
or server_name not in response[u"signatures"]):
|
or server_name not in response[u"signatures"]):
|
||||||
raise KeyLookupError("Key response not signed by remote server")
|
raise ValueError("Key response not signed by remote server")
|
||||||
|
|
||||||
if "tls_fingerprints" not in response:
|
if "tls_fingerprints" not in response:
|
||||||
raise KeyLookupError("Key response missing TLS fingerprints")
|
raise ValueError("Key response missing TLS fingerprints")
|
||||||
|
|
||||||
certificate_bytes = crypto.dump_certificate(
|
certificate_bytes = crypto.dump_certificate(
|
||||||
crypto.FILETYPE_ASN1, tls_certificate
|
crypto.FILETYPE_ASN1, tls_certificate
|
||||||
@@ -514,7 +501,7 @@ class Keyring(object):
|
|||||||
response_sha256_fingerprints.add(fingerprint[u"sha256"])
|
response_sha256_fingerprints.add(fingerprint[u"sha256"])
|
||||||
|
|
||||||
if sha256_fingerprint_b64 not in response_sha256_fingerprints:
|
if sha256_fingerprint_b64 not in response_sha256_fingerprints:
|
||||||
raise KeyLookupError("TLS certificate not allowed by fingerprints")
|
raise ValueError("TLS certificate not allowed by fingerprints")
|
||||||
|
|
||||||
response_keys = yield self.process_v2_response(
|
response_keys = yield self.process_v2_response(
|
||||||
from_server=server_name,
|
from_server=server_name,
|
||||||
@@ -524,7 +511,7 @@ class Keyring(object):
|
|||||||
|
|
||||||
keys.update(response_keys)
|
keys.update(response_keys)
|
||||||
|
|
||||||
yield preserve_context_over_deferred(defer.gatherResults(
|
yield defer.gatherResults(
|
||||||
[
|
[
|
||||||
preserve_fn(self.store_keys)(
|
preserve_fn(self.store_keys)(
|
||||||
server_name=key_server_name,
|
server_name=key_server_name,
|
||||||
@@ -534,13 +521,13 @@ class Keyring(object):
|
|||||||
for key_server_name, verify_keys in keys.items()
|
for key_server_name, verify_keys in keys.items()
|
||||||
],
|
],
|
||||||
consumeErrors=True
|
consumeErrors=True
|
||||||
)).addErrback(unwrapFirstError)
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
defer.returnValue(keys)
|
defer.returnValue(keys)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def process_v2_response(self, from_server, response_json,
|
def process_v2_response(self, from_server, response_json,
|
||||||
requested_ids=[], only_from_server=True):
|
requested_ids=[]):
|
||||||
time_now_ms = self.clock.time_msec()
|
time_now_ms = self.clock.time_msec()
|
||||||
response_keys = {}
|
response_keys = {}
|
||||||
verify_keys = {}
|
verify_keys = {}
|
||||||
@@ -564,16 +551,9 @@ class Keyring(object):
|
|||||||
|
|
||||||
results = {}
|
results = {}
|
||||||
server_name = response_json["server_name"]
|
server_name = response_json["server_name"]
|
||||||
if only_from_server:
|
|
||||||
if server_name != from_server:
|
|
||||||
raise KeyLookupError(
|
|
||||||
"Expected a response for server %r not %r" % (
|
|
||||||
from_server, server_name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
for key_id in response_json["signatures"].get(server_name, {}):
|
for key_id in response_json["signatures"].get(server_name, {}):
|
||||||
if key_id not in response_json["verify_keys"]:
|
if key_id not in response_json["verify_keys"]:
|
||||||
raise KeyLookupError(
|
raise ValueError(
|
||||||
"Key response must include verification keys for all"
|
"Key response must include verification keys for all"
|
||||||
" signatures"
|
" signatures"
|
||||||
)
|
)
|
||||||
@@ -600,7 +580,7 @@ class Keyring(object):
|
|||||||
response_keys.update(verify_keys)
|
response_keys.update(verify_keys)
|
||||||
response_keys.update(old_verify_keys)
|
response_keys.update(old_verify_keys)
|
||||||
|
|
||||||
yield preserve_context_over_deferred(defer.gatherResults(
|
yield defer.gatherResults(
|
||||||
[
|
[
|
||||||
preserve_fn(self.store.store_server_keys_json)(
|
preserve_fn(self.store.store_server_keys_json)(
|
||||||
server_name=server_name,
|
server_name=server_name,
|
||||||
@@ -613,7 +593,7 @@ class Keyring(object):
|
|||||||
for key_id in updated_key_ids
|
for key_id in updated_key_ids
|
||||||
],
|
],
|
||||||
consumeErrors=True,
|
consumeErrors=True,
|
||||||
)).addErrback(unwrapFirstError)
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
results[server_name] = response_keys
|
results[server_name] = response_keys
|
||||||
|
|
||||||
@@ -641,15 +621,15 @@ class Keyring(object):
|
|||||||
|
|
||||||
if ("signatures" not in response
|
if ("signatures" not in response
|
||||||
or server_name not in response["signatures"]):
|
or server_name not in response["signatures"]):
|
||||||
raise KeyLookupError("Key response not signed by remote server")
|
raise ValueError("Key response not signed by remote server")
|
||||||
|
|
||||||
if "tls_certificate" not in response:
|
if "tls_certificate" not in response:
|
||||||
raise KeyLookupError("Key response missing TLS certificate")
|
raise ValueError("Key response missing TLS certificate")
|
||||||
|
|
||||||
tls_certificate_b64 = response["tls_certificate"]
|
tls_certificate_b64 = response["tls_certificate"]
|
||||||
|
|
||||||
if encode_base64(x509_certificate_bytes) != tls_certificate_b64:
|
if encode_base64(x509_certificate_bytes) != tls_certificate_b64:
|
||||||
raise KeyLookupError("TLS certificate doesn't match")
|
raise ValueError("TLS certificate doesn't match")
|
||||||
|
|
||||||
# Cache the result in the datastore.
|
# Cache the result in the datastore.
|
||||||
|
|
||||||
@@ -665,7 +645,7 @@ class Keyring(object):
|
|||||||
|
|
||||||
for key_id in response["signatures"][server_name]:
|
for key_id in response["signatures"][server_name]:
|
||||||
if key_id not in response["verify_keys"]:
|
if key_id not in response["verify_keys"]:
|
||||||
raise KeyLookupError(
|
raise ValueError(
|
||||||
"Key response must include verification keys for all"
|
"Key response must include verification keys for all"
|
||||||
" signatures"
|
" signatures"
|
||||||
)
|
)
|
||||||
@@ -702,7 +682,7 @@ class Keyring(object):
|
|||||||
A deferred that completes when the keys are stored.
|
A deferred that completes when the keys are stored.
|
||||||
"""
|
"""
|
||||||
# TODO(markjh): Store whether the keys have expired.
|
# TODO(markjh): Store whether the keys have expired.
|
||||||
yield preserve_context_over_deferred(defer.gatherResults(
|
yield defer.gatherResults(
|
||||||
[
|
[
|
||||||
preserve_fn(self.store.store_server_verify_key)(
|
preserve_fn(self.store.store_server_verify_key)(
|
||||||
server_name, server_name, key.time_added, key
|
server_name, server_name, key.time_added, key
|
||||||
@@ -710,4 +690,4 @@ class Keyring(object):
|
|||||||
for key_id, key in verify_keys.items()
|
for key_id, key in verify_keys.items()
|
||||||
],
|
],
|
||||||
consumeErrors=True,
|
consumeErrors=True,
|
||||||
)).addErrback(unwrapFirstError)
|
).addErrback(unwrapFirstError)
|
||||||
|
|||||||
@@ -1,678 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 - 2016 OpenMarket Ltd
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from canonicaljson import encode_canonical_json
|
|
||||||
from signedjson.key import decode_verify_key_bytes
|
|
||||||
from signedjson.sign import verify_signed_json, SignatureVerifyException
|
|
||||||
from unpaddedbase64 import decode_base64
|
|
||||||
|
|
||||||
from synapse.api.constants import EventTypes, Membership, JoinRules
|
|
||||||
from synapse.api.errors import AuthError, SynapseError, EventSizeError
|
|
||||||
from synapse.types import UserID, get_domain_from_id
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def check(event, auth_events, do_sig_check=True, do_size_check=True):
|
|
||||||
""" Checks if this event is correctly authed.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: the event being checked.
|
|
||||||
auth_events (dict: event-key -> event): the existing room state.
|
|
||||||
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the auth checks pass.
|
|
||||||
"""
|
|
||||||
if do_size_check:
|
|
||||||
_check_size_limits(event)
|
|
||||||
|
|
||||||
if not hasattr(event, "room_id"):
|
|
||||||
raise AuthError(500, "Event has no room_id: %s" % event)
|
|
||||||
|
|
||||||
if do_sig_check:
|
|
||||||
sender_domain = get_domain_from_id(event.sender)
|
|
||||||
event_id_domain = get_domain_from_id(event.event_id)
|
|
||||||
|
|
||||||
is_invite_via_3pid = (
|
|
||||||
event.type == EventTypes.Member
|
|
||||||
and event.membership == Membership.INVITE
|
|
||||||
and "third_party_invite" in event.content
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check the sender's domain has signed the event
|
|
||||||
if not event.signatures.get(sender_domain):
|
|
||||||
# We allow invites via 3pid to have a sender from a different
|
|
||||||
# HS, as the sender must match the sender of the original
|
|
||||||
# 3pid invite. This is checked further down with the
|
|
||||||
# other dedicated membership checks.
|
|
||||||
if not is_invite_via_3pid:
|
|
||||||
raise AuthError(403, "Event not signed by sender's server")
|
|
||||||
|
|
||||||
# Check the event_id's domain has signed the event
|
|
||||||
if not event.signatures.get(event_id_domain):
|
|
||||||
raise AuthError(403, "Event not signed by sending server")
|
|
||||||
|
|
||||||
if auth_events is None:
|
|
||||||
# Oh, we don't know what the state of the room was, so we
|
|
||||||
# are trusting that this is allowed (at least for now)
|
|
||||||
logger.warn("Trusting event: %s", event.event_id)
|
|
||||||
return True
|
|
||||||
|
|
||||||
if event.type == EventTypes.Create:
|
|
||||||
room_id_domain = get_domain_from_id(event.room_id)
|
|
||||||
if room_id_domain != sender_domain:
|
|
||||||
raise AuthError(
|
|
||||||
403,
|
|
||||||
"Creation event's room_id domain does not match sender's"
|
|
||||||
)
|
|
||||||
# FIXME
|
|
||||||
return True
|
|
||||||
|
|
||||||
creation_event = auth_events.get((EventTypes.Create, ""), None)
|
|
||||||
|
|
||||||
if not creation_event:
|
|
||||||
raise SynapseError(
|
|
||||||
403,
|
|
||||||
"Room %r does not exist" % (event.room_id,)
|
|
||||||
)
|
|
||||||
|
|
||||||
creating_domain = get_domain_from_id(event.room_id)
|
|
||||||
originating_domain = get_domain_from_id(event.sender)
|
|
||||||
if creating_domain != originating_domain:
|
|
||||||
if not _can_federate(event, auth_events):
|
|
||||||
raise AuthError(
|
|
||||||
403,
|
|
||||||
"This room has been marked as unfederatable."
|
|
||||||
)
|
|
||||||
|
|
||||||
# FIXME: Temp hack
|
|
||||||
if event.type == EventTypes.Aliases:
|
|
||||||
if not event.is_state():
|
|
||||||
raise AuthError(
|
|
||||||
403,
|
|
||||||
"Alias event must be a state event",
|
|
||||||
)
|
|
||||||
if not event.state_key:
|
|
||||||
raise AuthError(
|
|
||||||
403,
|
|
||||||
"Alias event must have non-empty state_key"
|
|
||||||
)
|
|
||||||
sender_domain = get_domain_from_id(event.sender)
|
|
||||||
if event.state_key != sender_domain:
|
|
||||||
raise AuthError(
|
|
||||||
403,
|
|
||||||
"Alias event's state_key does not match sender's domain"
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
|
||||||
logger.debug(
|
|
||||||
"Auth events: %s",
|
|
||||||
[a.event_id for a in auth_events.values()]
|
|
||||||
)
|
|
||||||
|
|
||||||
if event.type == EventTypes.Member:
|
|
||||||
allowed = _is_membership_change_allowed(
|
|
||||||
event, auth_events
|
|
||||||
)
|
|
||||||
if allowed:
|
|
||||||
logger.debug("Allowing! %s", event)
|
|
||||||
else:
|
|
||||||
logger.debug("Denying! %s", event)
|
|
||||||
return allowed
|
|
||||||
|
|
||||||
_check_event_sender_in_room(event, auth_events)
|
|
||||||
|
|
||||||
# Special case to allow m.room.third_party_invite events wherever
|
|
||||||
# a user is allowed to issue invites. Fixes
|
|
||||||
# https://github.com/vector-im/vector-web/issues/1208 hopefully
|
|
||||||
if event.type == EventTypes.ThirdPartyInvite:
|
|
||||||
user_level = get_user_power_level(event.user_id, auth_events)
|
|
||||||
invite_level = _get_named_level(auth_events, "invite", 0)
|
|
||||||
|
|
||||||
if user_level < invite_level:
|
|
||||||
raise AuthError(
|
|
||||||
403, (
|
|
||||||
"You cannot issue a third party invite for %s." %
|
|
||||||
(event.content.display_name,)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
_can_send_event(event, auth_events)
|
|
||||||
|
|
||||||
if event.type == EventTypes.PowerLevels:
|
|
||||||
_check_power_levels(event, auth_events)
|
|
||||||
|
|
||||||
if event.type == EventTypes.Redaction:
|
|
||||||
check_redaction(event, auth_events)
|
|
||||||
|
|
||||||
logger.debug("Allowing! %s", event)
|
|
||||||
|
|
||||||
|
|
||||||
def _check_size_limits(event):
|
|
||||||
def too_big(field):
|
|
||||||
raise EventSizeError("%s too large" % (field,))
|
|
||||||
|
|
||||||
if len(event.user_id) > 255:
|
|
||||||
too_big("user_id")
|
|
||||||
if len(event.room_id) > 255:
|
|
||||||
too_big("room_id")
|
|
||||||
if event.is_state() and len(event.state_key) > 255:
|
|
||||||
too_big("state_key")
|
|
||||||
if len(event.type) > 255:
|
|
||||||
too_big("type")
|
|
||||||
if len(event.event_id) > 255:
|
|
||||||
too_big("event_id")
|
|
||||||
if len(encode_canonical_json(event.get_pdu_json())) > 65536:
|
|
||||||
too_big("event")
|
|
||||||
|
|
||||||
|
|
||||||
def _can_federate(event, auth_events):
|
|
||||||
creation_event = auth_events.get((EventTypes.Create, ""))
|
|
||||||
|
|
||||||
return creation_event.content.get("m.federate", True) is True
|
|
||||||
|
|
||||||
|
|
||||||
def _is_membership_change_allowed(event, auth_events):
|
|
||||||
membership = event.content["membership"]
|
|
||||||
|
|
||||||
# Check if this is the room creator joining:
|
|
||||||
if len(event.prev_events) == 1 and Membership.JOIN == membership:
|
|
||||||
# Get room creation event:
|
|
||||||
key = (EventTypes.Create, "", )
|
|
||||||
create = auth_events.get(key)
|
|
||||||
if create and event.prev_events[0][0] == create.event_id:
|
|
||||||
if create.content["creator"] == event.state_key:
|
|
||||||
return True
|
|
||||||
|
|
||||||
target_user_id = event.state_key
|
|
||||||
|
|
||||||
creating_domain = get_domain_from_id(event.room_id)
|
|
||||||
target_domain = get_domain_from_id(target_user_id)
|
|
||||||
if creating_domain != target_domain:
|
|
||||||
if not _can_federate(event, auth_events):
|
|
||||||
raise AuthError(
|
|
||||||
403,
|
|
||||||
"This room has been marked as unfederatable."
|
|
||||||
)
|
|
||||||
|
|
||||||
# get info about the caller
|
|
||||||
key = (EventTypes.Member, event.user_id, )
|
|
||||||
caller = auth_events.get(key)
|
|
||||||
|
|
||||||
caller_in_room = caller and caller.membership == Membership.JOIN
|
|
||||||
caller_invited = caller and caller.membership == Membership.INVITE
|
|
||||||
|
|
||||||
# get info about the target
|
|
||||||
key = (EventTypes.Member, target_user_id, )
|
|
||||||
target = auth_events.get(key)
|
|
||||||
|
|
||||||
target_in_room = target and target.membership == Membership.JOIN
|
|
||||||
target_banned = target and target.membership == Membership.BAN
|
|
||||||
|
|
||||||
key = (EventTypes.JoinRules, "", )
|
|
||||||
join_rule_event = auth_events.get(key)
|
|
||||||
if join_rule_event:
|
|
||||||
join_rule = join_rule_event.content.get(
|
|
||||||
"join_rule", JoinRules.INVITE
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
join_rule = JoinRules.INVITE
|
|
||||||
|
|
||||||
user_level = get_user_power_level(event.user_id, auth_events)
|
|
||||||
target_level = get_user_power_level(
|
|
||||||
target_user_id, auth_events
|
|
||||||
)
|
|
||||||
|
|
||||||
# FIXME (erikj): What should we do here as the default?
|
|
||||||
ban_level = _get_named_level(auth_events, "ban", 50)
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
"_is_membership_change_allowed: %s",
|
|
||||||
{
|
|
||||||
"caller_in_room": caller_in_room,
|
|
||||||
"caller_invited": caller_invited,
|
|
||||||
"target_banned": target_banned,
|
|
||||||
"target_in_room": target_in_room,
|
|
||||||
"membership": membership,
|
|
||||||
"join_rule": join_rule,
|
|
||||||
"target_user_id": target_user_id,
|
|
||||||
"event.user_id": event.user_id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if Membership.INVITE == membership and "third_party_invite" in event.content:
|
|
||||||
if not _verify_third_party_invite(event, auth_events):
|
|
||||||
raise AuthError(403, "You are not invited to this room.")
|
|
||||||
if target_banned:
|
|
||||||
raise AuthError(
|
|
||||||
403, "%s is banned from the room" % (target_user_id,)
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
if Membership.JOIN != membership:
|
|
||||||
if (caller_invited
|
|
||||||
and Membership.LEAVE == membership
|
|
||||||
and target_user_id == event.user_id):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if not caller_in_room: # caller isn't joined
|
|
||||||
raise AuthError(
|
|
||||||
403,
|
|
||||||
"%s not in room %s." % (event.user_id, event.room_id,)
|
|
||||||
)
|
|
||||||
|
|
||||||
if Membership.INVITE == membership:
|
|
||||||
# TODO (erikj): We should probably handle this more intelligently
|
|
||||||
# PRIVATE join rules.
|
|
||||||
|
|
||||||
# Invites are valid iff caller is in the room and target isn't.
|
|
||||||
if target_banned:
|
|
||||||
raise AuthError(
|
|
||||||
403, "%s is banned from the room" % (target_user_id,)
|
|
||||||
)
|
|
||||||
elif target_in_room: # the target is already in the room.
|
|
||||||
raise AuthError(403, "%s is already in the room." %
|
|
||||||
target_user_id)
|
|
||||||
else:
|
|
||||||
invite_level = _get_named_level(auth_events, "invite", 0)
|
|
||||||
|
|
||||||
if user_level < invite_level:
|
|
||||||
raise AuthError(
|
|
||||||
403, "You cannot invite user %s." % target_user_id
|
|
||||||
)
|
|
||||||
elif Membership.JOIN == membership:
|
|
||||||
# Joins are valid iff caller == target and they were:
|
|
||||||
# invited: They are accepting the invitation
|
|
||||||
# joined: It's a NOOP
|
|
||||||
if event.user_id != target_user_id:
|
|
||||||
raise AuthError(403, "Cannot force another user to join.")
|
|
||||||
elif target_banned:
|
|
||||||
raise AuthError(403, "You are banned from this room")
|
|
||||||
elif join_rule == JoinRules.PUBLIC:
|
|
||||||
pass
|
|
||||||
elif join_rule == JoinRules.INVITE:
|
|
||||||
if not caller_in_room and not caller_invited:
|
|
||||||
raise AuthError(403, "You are not invited to this room.")
|
|
||||||
else:
|
|
||||||
# TODO (erikj): may_join list
|
|
||||||
# TODO (erikj): private rooms
|
|
||||||
raise AuthError(403, "You are not allowed to join this room")
|
|
||||||
elif Membership.LEAVE == membership:
|
|
||||||
# TODO (erikj): Implement kicks.
|
|
||||||
if target_banned and user_level < ban_level:
|
|
||||||
raise AuthError(
|
|
||||||
403, "You cannot unban user &s." % (target_user_id,)
|
|
||||||
)
|
|
||||||
elif target_user_id != event.user_id:
|
|
||||||
kick_level = _get_named_level(auth_events, "kick", 50)
|
|
||||||
|
|
||||||
if user_level < kick_level or user_level <= target_level:
|
|
||||||
raise AuthError(
|
|
||||||
403, "You cannot kick user %s." % target_user_id
|
|
||||||
)
|
|
||||||
elif Membership.BAN == membership:
|
|
||||||
if user_level < ban_level or user_level <= target_level:
|
|
||||||
raise AuthError(403, "You don't have permission to ban")
|
|
||||||
else:
|
|
||||||
raise AuthError(500, "Unknown membership %s" % membership)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _check_event_sender_in_room(event, auth_events):
|
|
||||||
key = (EventTypes.Member, event.user_id, )
|
|
||||||
member_event = auth_events.get(key)
|
|
||||||
|
|
||||||
return _check_joined_room(
|
|
||||||
member_event,
|
|
||||||
event.user_id,
|
|
||||||
event.room_id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _check_joined_room(member, user_id, room_id):
|
|
||||||
if not member or member.membership != Membership.JOIN:
|
|
||||||
raise AuthError(403, "User %s not in room %s (%s)" % (
|
|
||||||
user_id, room_id, repr(member)
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
def get_send_level(etype, state_key, auth_events):
|
|
||||||
key = (EventTypes.PowerLevels, "", )
|
|
||||||
send_level_event = auth_events.get(key)
|
|
||||||
send_level = None
|
|
||||||
if send_level_event:
|
|
||||||
send_level = send_level_event.content.get("events", {}).get(
|
|
||||||
etype
|
|
||||||
)
|
|
||||||
if send_level is None:
|
|
||||||
if state_key is not None:
|
|
||||||
send_level = send_level_event.content.get(
|
|
||||||
"state_default", 50
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
send_level = send_level_event.content.get(
|
|
||||||
"events_default", 0
|
|
||||||
)
|
|
||||||
|
|
||||||
if send_level:
|
|
||||||
send_level = int(send_level)
|
|
||||||
else:
|
|
||||||
send_level = 0
|
|
||||||
|
|
||||||
return send_level
|
|
||||||
|
|
||||||
|
|
||||||
def _can_send_event(event, auth_events):
|
|
||||||
send_level = get_send_level(
|
|
||||||
event.type, event.get("state_key", None), auth_events
|
|
||||||
)
|
|
||||||
user_level = get_user_power_level(event.user_id, auth_events)
|
|
||||||
|
|
||||||
if user_level < send_level:
|
|
||||||
raise AuthError(
|
|
||||||
403,
|
|
||||||
"You don't have permission to post that to the room. " +
|
|
||||||
"user_level (%d) < send_level (%d)" % (user_level, send_level)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check state_key
|
|
||||||
if hasattr(event, "state_key"):
|
|
||||||
if event.state_key.startswith("@"):
|
|
||||||
if event.state_key != event.user_id:
|
|
||||||
raise AuthError(
|
|
||||||
403,
|
|
||||||
"You are not allowed to set others state"
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def check_redaction(event, auth_events):
|
|
||||||
"""Check whether the event sender is allowed to redact the target event.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the the sender is allowed to redact the target event if the
|
|
||||||
target event was created by them.
|
|
||||||
False if the sender is allowed to redact the target event with no
|
|
||||||
further checks.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
AuthError if the event sender is definitely not allowed to redact
|
|
||||||
the target event.
|
|
||||||
"""
|
|
||||||
user_level = get_user_power_level(event.user_id, auth_events)
|
|
||||||
|
|
||||||
redact_level = _get_named_level(auth_events, "redact", 50)
|
|
||||||
|
|
||||||
if user_level >= redact_level:
|
|
||||||
return False
|
|
||||||
|
|
||||||
redacter_domain = get_domain_from_id(event.event_id)
|
|
||||||
redactee_domain = get_domain_from_id(event.redacts)
|
|
||||||
if redacter_domain == redactee_domain:
|
|
||||||
return True
|
|
||||||
|
|
||||||
raise AuthError(
|
|
||||||
403,
|
|
||||||
"You don't have permission to redact events"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _check_power_levels(event, auth_events):
|
|
||||||
user_list = event.content.get("users", {})
|
|
||||||
# Validate users
|
|
||||||
for k, v in user_list.items():
|
|
||||||
try:
|
|
||||||
UserID.from_string(k)
|
|
||||||
except:
|
|
||||||
raise SynapseError(400, "Not a valid user_id: %s" % (k,))
|
|
||||||
|
|
||||||
try:
|
|
||||||
int(v)
|
|
||||||
except:
|
|
||||||
raise SynapseError(400, "Not a valid power level: %s" % (v,))
|
|
||||||
|
|
||||||
key = (event.type, event.state_key, )
|
|
||||||
current_state = auth_events.get(key)
|
|
||||||
|
|
||||||
if not current_state:
|
|
||||||
return
|
|
||||||
|
|
||||||
user_level = get_user_power_level(event.user_id, auth_events)
|
|
||||||
|
|
||||||
# Check other levels:
|
|
||||||
levels_to_check = [
|
|
||||||
("users_default", None),
|
|
||||||
("events_default", None),
|
|
||||||
("state_default", None),
|
|
||||||
("ban", None),
|
|
||||||
("redact", None),
|
|
||||||
("kick", None),
|
|
||||||
("invite", None),
|
|
||||||
]
|
|
||||||
|
|
||||||
old_list = current_state.content.get("users")
|
|
||||||
for user in set(old_list.keys() + user_list.keys()):
|
|
||||||
levels_to_check.append(
|
|
||||||
(user, "users")
|
|
||||||
)
|
|
||||||
|
|
||||||
old_list = current_state.content.get("events")
|
|
||||||
new_list = event.content.get("events")
|
|
||||||
for ev_id in set(old_list.keys() + new_list.keys()):
|
|
||||||
levels_to_check.append(
|
|
||||||
(ev_id, "events")
|
|
||||||
)
|
|
||||||
|
|
||||||
old_state = current_state.content
|
|
||||||
new_state = event.content
|
|
||||||
|
|
||||||
for level_to_check, dir in levels_to_check:
|
|
||||||
old_loc = old_state
|
|
||||||
new_loc = new_state
|
|
||||||
if dir:
|
|
||||||
old_loc = old_loc.get(dir, {})
|
|
||||||
new_loc = new_loc.get(dir, {})
|
|
||||||
|
|
||||||
if level_to_check in old_loc:
|
|
||||||
old_level = int(old_loc[level_to_check])
|
|
||||||
else:
|
|
||||||
old_level = None
|
|
||||||
|
|
||||||
if level_to_check in new_loc:
|
|
||||||
new_level = int(new_loc[level_to_check])
|
|
||||||
else:
|
|
||||||
new_level = None
|
|
||||||
|
|
||||||
if new_level is not None and old_level is not None:
|
|
||||||
if new_level == old_level:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if dir == "users" and level_to_check != event.user_id:
|
|
||||||
if old_level == user_level:
|
|
||||||
raise AuthError(
|
|
||||||
403,
|
|
||||||
"You don't have permission to remove ops level equal "
|
|
||||||
"to your own"
|
|
||||||
)
|
|
||||||
|
|
||||||
if old_level > user_level or new_level > user_level:
|
|
||||||
raise AuthError(
|
|
||||||
403,
|
|
||||||
"You don't have permission to add ops level greater "
|
|
||||||
"than your own"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_power_level_event(auth_events):
|
|
||||||
key = (EventTypes.PowerLevels, "", )
|
|
||||||
return auth_events.get(key)
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_power_level(user_id, auth_events):
|
|
||||||
power_level_event = _get_power_level_event(auth_events)
|
|
||||||
|
|
||||||
if power_level_event:
|
|
||||||
level = power_level_event.content.get("users", {}).get(user_id)
|
|
||||||
if not level:
|
|
||||||
level = power_level_event.content.get("users_default", 0)
|
|
||||||
|
|
||||||
if level is None:
|
|
||||||
return 0
|
|
||||||
else:
|
|
||||||
return int(level)
|
|
||||||
else:
|
|
||||||
key = (EventTypes.Create, "", )
|
|
||||||
create_event = auth_events.get(key)
|
|
||||||
if (create_event is not None and
|
|
||||||
create_event.content["creator"] == user_id):
|
|
||||||
return 100
|
|
||||||
else:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def _get_named_level(auth_events, name, default):
|
|
||||||
power_level_event = _get_power_level_event(auth_events)
|
|
||||||
|
|
||||||
if not power_level_event:
|
|
||||||
return default
|
|
||||||
|
|
||||||
level = power_level_event.content.get(name, None)
|
|
||||||
if level is not None:
|
|
||||||
return int(level)
|
|
||||||
else:
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
def _verify_third_party_invite(event, auth_events):
|
|
||||||
"""
|
|
||||||
Validates that the invite event is authorized by a previous third-party invite.
|
|
||||||
|
|
||||||
Checks that the public key, and keyserver, match those in the third party invite,
|
|
||||||
and that the invite event has a signature issued using that public key.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: The m.room.member join event being validated.
|
|
||||||
auth_events: All relevant previous context events which may be used
|
|
||||||
for authorization decisions.
|
|
||||||
|
|
||||||
Return:
|
|
||||||
True if the event fulfills the expectations of a previous third party
|
|
||||||
invite event.
|
|
||||||
"""
|
|
||||||
if "third_party_invite" not in event.content:
|
|
||||||
return False
|
|
||||||
if "signed" not in event.content["third_party_invite"]:
|
|
||||||
return False
|
|
||||||
signed = event.content["third_party_invite"]["signed"]
|
|
||||||
for key in {"mxid", "token"}:
|
|
||||||
if key not in signed:
|
|
||||||
return False
|
|
||||||
|
|
||||||
token = signed["token"]
|
|
||||||
|
|
||||||
invite_event = auth_events.get(
|
|
||||||
(EventTypes.ThirdPartyInvite, token,)
|
|
||||||
)
|
|
||||||
if not invite_event:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if invite_event.sender != event.sender:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if event.user_id != invite_event.user_id:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if signed["mxid"] != event.state_key:
|
|
||||||
return False
|
|
||||||
if signed["token"] != token:
|
|
||||||
return False
|
|
||||||
|
|
||||||
for public_key_object in get_public_keys(invite_event):
|
|
||||||
public_key = public_key_object["public_key"]
|
|
||||||
try:
|
|
||||||
for server, signature_block in signed["signatures"].items():
|
|
||||||
for key_name, encoded_signature in signature_block.items():
|
|
||||||
if not key_name.startswith("ed25519:"):
|
|
||||||
continue
|
|
||||||
verify_key = decode_verify_key_bytes(
|
|
||||||
key_name,
|
|
||||||
decode_base64(public_key)
|
|
||||||
)
|
|
||||||
verify_signed_json(signed, server, verify_key)
|
|
||||||
|
|
||||||
# We got the public key from the invite, so we know that the
|
|
||||||
# correct server signed the signed bundle.
|
|
||||||
# The caller is responsible for checking that the signing
|
|
||||||
# server has not revoked that public key.
|
|
||||||
return True
|
|
||||||
except (KeyError, SignatureVerifyException,):
|
|
||||||
continue
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_public_keys(invite_event):
|
|
||||||
public_keys = []
|
|
||||||
if "public_key" in invite_event.content:
|
|
||||||
o = {
|
|
||||||
"public_key": invite_event.content["public_key"],
|
|
||||||
}
|
|
||||||
if "key_validity_url" in invite_event.content:
|
|
||||||
o["key_validity_url"] = invite_event.content["key_validity_url"]
|
|
||||||
public_keys.append(o)
|
|
||||||
public_keys.extend(invite_event.content.get("public_keys", []))
|
|
||||||
return public_keys
|
|
||||||
|
|
||||||
|
|
||||||
def auth_types_for_event(event):
|
|
||||||
"""Given an event, return a list of (EventType, StateKey) that may be
|
|
||||||
needed to auth the event. The returned list may be a superset of what
|
|
||||||
would actually be required depending on the full state of the room.
|
|
||||||
|
|
||||||
Used to limit the number of events to fetch from the database to
|
|
||||||
actually auth the event.
|
|
||||||
"""
|
|
||||||
if event.type == EventTypes.Create:
|
|
||||||
return []
|
|
||||||
|
|
||||||
auth_types = []
|
|
||||||
|
|
||||||
auth_types.append((EventTypes.PowerLevels, "", ))
|
|
||||||
auth_types.append((EventTypes.Member, event.user_id, ))
|
|
||||||
auth_types.append((EventTypes.Create, "", ))
|
|
||||||
|
|
||||||
if event.type == EventTypes.Member:
|
|
||||||
membership = event.content["membership"]
|
|
||||||
if membership in [Membership.JOIN, Membership.INVITE]:
|
|
||||||
auth_types.append((EventTypes.JoinRules, "", ))
|
|
||||||
|
|
||||||
auth_types.append((EventTypes.Member, event.state_key, ))
|
|
||||||
|
|
||||||
if membership == Membership.INVITE:
|
|
||||||
if "third_party_invite" in event.content:
|
|
||||||
key = (
|
|
||||||
EventTypes.ThirdPartyInvite,
|
|
||||||
event.content["third_party_invite"]["signed"]["token"]
|
|
||||||
)
|
|
||||||
auth_types.append(key)
|
|
||||||
|
|
||||||
return auth_types
|
|
||||||
@@ -36,15 +36,6 @@ class _EventInternalMetadata(object):
|
|||||||
def is_invite_from_remote(self):
|
def is_invite_from_remote(self):
|
||||||
return getattr(self, "invite_from_remote", False)
|
return getattr(self, "invite_from_remote", False)
|
||||||
|
|
||||||
def get_send_on_behalf_of(self):
|
|
||||||
"""Whether this server should send the event on behalf of another server.
|
|
||||||
This is used by the federation "send_join" API to forward the initial join
|
|
||||||
event for a server in the room.
|
|
||||||
|
|
||||||
returns a str with the name of the server this event is sent on behalf of.
|
|
||||||
"""
|
|
||||||
return getattr(self, "send_on_behalf_of", None)
|
|
||||||
|
|
||||||
|
|
||||||
def _event_dict_property(key):
|
def _event_dict_property(key):
|
||||||
def getter(self):
|
def getter(self):
|
||||||
@@ -79,6 +70,7 @@ class EventBase(object):
|
|||||||
auth_events = _event_dict_property("auth_events")
|
auth_events = _event_dict_property("auth_events")
|
||||||
depth = _event_dict_property("depth")
|
depth = _event_dict_property("depth")
|
||||||
content = _event_dict_property("content")
|
content = _event_dict_property("content")
|
||||||
|
event_id = _event_dict_property("event_id")
|
||||||
hashes = _event_dict_property("hashes")
|
hashes = _event_dict_property("hashes")
|
||||||
origin = _event_dict_property("origin")
|
origin = _event_dict_property("origin")
|
||||||
origin_server_ts = _event_dict_property("origin_server_ts")
|
origin_server_ts = _event_dict_property("origin_server_ts")
|
||||||
@@ -87,6 +79,8 @@ class EventBase(object):
|
|||||||
redacts = _event_dict_property("redacts")
|
redacts = _event_dict_property("redacts")
|
||||||
room_id = _event_dict_property("room_id")
|
room_id = _event_dict_property("room_id")
|
||||||
sender = _event_dict_property("sender")
|
sender = _event_dict_property("sender")
|
||||||
|
state_key = _event_dict_property("state_key")
|
||||||
|
type = _event_dict_property("type")
|
||||||
user_id = _event_dict_property("sender")
|
user_id = _event_dict_property("sender")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -105,7 +99,7 @@ class EventBase(object):
|
|||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key, default):
|
||||||
return self._event_dict.get(key, default)
|
return self._event_dict.get(key, default)
|
||||||
|
|
||||||
def get_internal_metadata_dict(self):
|
def get_internal_metadata_dict(self):
|
||||||
@@ -159,11 +153,6 @@ class FrozenEvent(EventBase):
|
|||||||
else:
|
else:
|
||||||
frozen_dict = event_dict
|
frozen_dict = event_dict
|
||||||
|
|
||||||
self.event_id = event_dict["event_id"]
|
|
||||||
self.type = event_dict["type"]
|
|
||||||
if "state_key" in event_dict:
|
|
||||||
self.state_key = event_dict["state_key"]
|
|
||||||
|
|
||||||
super(FrozenEvent, self).__init__(
|
super(FrozenEvent, self).__init__(
|
||||||
frozen_dict,
|
frozen_dict,
|
||||||
signatures=signatures,
|
signatures=signatures,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from . import EventBase, FrozenEvent, _event_dict_property
|
from . import EventBase, FrozenEvent
|
||||||
|
|
||||||
from synapse.types import EventID
|
from synapse.types import EventID
|
||||||
|
|
||||||
@@ -34,10 +34,6 @@ class EventBuilder(EventBase):
|
|||||||
internal_metadata_dict=internal_metadata_dict,
|
internal_metadata_dict=internal_metadata_dict,
|
||||||
)
|
)
|
||||||
|
|
||||||
event_id = _event_dict_property("event_id")
|
|
||||||
state_key = _event_dict_property("state_key")
|
|
||||||
type = _event_dict_property("type")
|
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
return FrozenEvent.from_event(self)
|
return FrozenEvent.from_event(self)
|
||||||
|
|
||||||
|
|||||||
@@ -15,30 +15,9 @@
|
|||||||
|
|
||||||
|
|
||||||
class EventContext(object):
|
class EventContext(object):
|
||||||
__slots__ = [
|
|
||||||
"current_state_ids",
|
|
||||||
"prev_state_ids",
|
|
||||||
"state_group",
|
|
||||||
"rejected",
|
|
||||||
"push_actions",
|
|
||||||
"prev_group",
|
|
||||||
"delta_ids",
|
|
||||||
"prev_state_events",
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, current_state=None):
|
||||||
# The current state including the current event
|
self.current_state = current_state
|
||||||
self.current_state_ids = None
|
|
||||||
# The current state excluding the current event
|
|
||||||
self.prev_state_ids = None
|
|
||||||
self.state_group = None
|
self.state_group = None
|
||||||
|
|
||||||
self.rejected = False
|
self.rejected = False
|
||||||
self.push_actions = []
|
self.push_actions = []
|
||||||
|
|
||||||
# A previously persisted state group and a delta between that
|
|
||||||
# and this state.
|
|
||||||
self.prev_group = None
|
|
||||||
self.delta_ids = None
|
|
||||||
|
|
||||||
self.prev_state_events = None
|
|
||||||
|
|||||||
@@ -16,17 +16,6 @@
|
|||||||
from synapse.api.constants import EventTypes
|
from synapse.api.constants import EventTypes
|
||||||
from . import EventBase
|
from . import EventBase
|
||||||
|
|
||||||
from frozendict import frozendict
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
# Split strings on "." but not "\." This uses a negative lookbehind assertion for '\'
|
|
||||||
# (?<!stuff) matches if the current position in the string is not preceded
|
|
||||||
# by a match for 'stuff'.
|
|
||||||
# TODO: This is fast, but fails to handle "foo\\.bar" which should be treated as
|
|
||||||
# the literal fields "foo\" and "bar" but will instead be treated as "foo\\.bar"
|
|
||||||
SPLIT_FIELD_REGEX = re.compile(r'(?<!\\)\.')
|
|
||||||
|
|
||||||
|
|
||||||
def prune_event(event):
|
def prune_event(event):
|
||||||
""" Returns a pruned version of the given event, which removes all keys we
|
""" Returns a pruned version of the given event, which removes all keys we
|
||||||
@@ -99,8 +88,6 @@ def prune_event(event):
|
|||||||
|
|
||||||
if "age_ts" in event.unsigned:
|
if "age_ts" in event.unsigned:
|
||||||
allowed_fields["unsigned"]["age_ts"] = event.unsigned["age_ts"]
|
allowed_fields["unsigned"]["age_ts"] = event.unsigned["age_ts"]
|
||||||
if "replaces_state" in event.unsigned:
|
|
||||||
allowed_fields["unsigned"]["replaces_state"] = event.unsigned["replaces_state"]
|
|
||||||
|
|
||||||
return type(event)(
|
return type(event)(
|
||||||
allowed_fields,
|
allowed_fields,
|
||||||
@@ -108,83 +95,6 @@ def prune_event(event):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _copy_field(src, dst, field):
|
|
||||||
"""Copy the field in 'src' to 'dst'.
|
|
||||||
|
|
||||||
For example, if src={"foo":{"bar":5}} and dst={}, and field=["foo","bar"]
|
|
||||||
then dst={"foo":{"bar":5}}.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
src(dict): The dict to read from.
|
|
||||||
dst(dict): The dict to modify.
|
|
||||||
field(list<str>): List of keys to drill down to in 'src'.
|
|
||||||
"""
|
|
||||||
if len(field) == 0: # this should be impossible
|
|
||||||
return
|
|
||||||
if len(field) == 1: # common case e.g. 'origin_server_ts'
|
|
||||||
if field[0] in src:
|
|
||||||
dst[field[0]] = src[field[0]]
|
|
||||||
return
|
|
||||||
|
|
||||||
# Else is a nested field e.g. 'content.body'
|
|
||||||
# Pop the last field as that's the key to move across and we need the
|
|
||||||
# parent dict in order to access the data. Drill down to the right dict.
|
|
||||||
key_to_move = field.pop(-1)
|
|
||||||
sub_dict = src
|
|
||||||
for sub_field in field: # e.g. sub_field => "content"
|
|
||||||
if sub_field in sub_dict and type(sub_dict[sub_field]) in [dict, frozendict]:
|
|
||||||
sub_dict = sub_dict[sub_field]
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
if key_to_move not in sub_dict:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Insert the key into the output dictionary, creating nested objects
|
|
||||||
# as required. We couldn't do this any earlier or else we'd need to delete
|
|
||||||
# the empty objects if the key didn't exist.
|
|
||||||
sub_out_dict = dst
|
|
||||||
for sub_field in field:
|
|
||||||
sub_out_dict = sub_out_dict.setdefault(sub_field, {})
|
|
||||||
sub_out_dict[key_to_move] = sub_dict[key_to_move]
|
|
||||||
|
|
||||||
|
|
||||||
def only_fields(dictionary, fields):
|
|
||||||
"""Return a new dict with only the fields in 'dictionary' which are present
|
|
||||||
in 'fields'.
|
|
||||||
|
|
||||||
If there are no event fields specified then all fields are included.
|
|
||||||
The entries may include '.' charaters to indicate sub-fields.
|
|
||||||
So ['content.body'] will include the 'body' field of the 'content' object.
|
|
||||||
A literal '.' character in a field name may be escaped using a '\'.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
dictionary(dict): The dictionary to read from.
|
|
||||||
fields(list<str>): A list of fields to copy over. Only shallow refs are
|
|
||||||
taken.
|
|
||||||
Returns:
|
|
||||||
dict: A new dictionary with only the given fields. If fields was empty,
|
|
||||||
the same dictionary is returned.
|
|
||||||
"""
|
|
||||||
if len(fields) == 0:
|
|
||||||
return dictionary
|
|
||||||
|
|
||||||
# for each field, convert it:
|
|
||||||
# ["content.body.thing\.with\.dots"] => [["content", "body", "thing\.with\.dots"]]
|
|
||||||
split_fields = [SPLIT_FIELD_REGEX.split(f) for f in fields]
|
|
||||||
|
|
||||||
# for each element of the output array of arrays:
|
|
||||||
# remove escaping so we can use the right key names.
|
|
||||||
split_fields[:] = [
|
|
||||||
[f.replace(r'\.', r'.') for f in field_array] for field_array in split_fields
|
|
||||||
]
|
|
||||||
|
|
||||||
output = {}
|
|
||||||
for field_array in split_fields:
|
|
||||||
_copy_field(dictionary, output, field_array)
|
|
||||||
return output
|
|
||||||
|
|
||||||
|
|
||||||
def format_event_raw(d):
|
def format_event_raw(d):
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@@ -225,7 +135,7 @@ def format_event_for_client_v2_without_room_id(d):
|
|||||||
|
|
||||||
def serialize_event(e, time_now_ms, as_client_event=True,
|
def serialize_event(e, time_now_ms, as_client_event=True,
|
||||||
event_format=format_event_for_client_v1,
|
event_format=format_event_for_client_v1,
|
||||||
token_id=None, only_event_fields=None):
|
token_id=None):
|
||||||
# FIXME(erikj): To handle the case of presence events and the like
|
# FIXME(erikj): To handle the case of presence events and the like
|
||||||
if not isinstance(e, EventBase):
|
if not isinstance(e, EventBase):
|
||||||
return e
|
return e
|
||||||
@@ -252,12 +162,6 @@ def serialize_event(e, time_now_ms, as_client_event=True,
|
|||||||
d["unsigned"]["transaction_id"] = txn_id
|
d["unsigned"]["transaction_id"] = txn_id
|
||||||
|
|
||||||
if as_client_event:
|
if as_client_event:
|
||||||
d = event_format(d)
|
return event_format(d)
|
||||||
|
else:
|
||||||
if only_event_fields:
|
return d
|
||||||
if (not isinstance(only_event_fields, list) or
|
|
||||||
not all(isinstance(f, basestring) for f in only_event_fields)):
|
|
||||||
raise TypeError("only_event_fields must be a list of strings")
|
|
||||||
d = only_fields(d, only_event_fields)
|
|
||||||
|
|
||||||
return d
|
|
||||||
|
|||||||
@@ -17,9 +17,10 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from .replication import ReplicationLayer
|
from .replication import ReplicationLayer
|
||||||
|
from .transport.client import TransportLayerClient
|
||||||
|
|
||||||
|
|
||||||
def initialize_http_replication(hs):
|
def initialize_http_replication(homeserver):
|
||||||
transport = hs.get_federation_transport_client()
|
transport = TransportLayerClient(homeserver)
|
||||||
|
|
||||||
return ReplicationLayer(hs, transport)
|
return ReplicationLayer(homeserver, transport)
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ from synapse.crypto.event_signing import check_event_content_hash
|
|||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
|
|
||||||
from synapse.util import unwrapFirstError
|
from synapse.util import unwrapFirstError
|
||||||
from synapse.util.logcontext import preserve_fn, preserve_context_over_deferred
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -32,9 +31,6 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class FederationBase(object):
|
class FederationBase(object):
|
||||||
def __init__(self, hs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _check_sigs_and_hash_and_fetch(self, origin, pdus, outlier=False,
|
def _check_sigs_and_hash_and_fetch(self, origin, pdus, outlier=False,
|
||||||
include_none=False):
|
include_none=False):
|
||||||
@@ -103,10 +99,10 @@ class FederationBase(object):
|
|||||||
warn, pdu
|
warn, pdu
|
||||||
)
|
)
|
||||||
|
|
||||||
valid_pdus = yield preserve_context_over_deferred(defer.gatherResults(
|
valid_pdus = yield defer.gatherResults(
|
||||||
deferreds,
|
deferreds,
|
||||||
consumeErrors=True
|
consumeErrors=True
|
||||||
)).addErrback(unwrapFirstError)
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
if include_none:
|
if include_none:
|
||||||
defer.returnValue(valid_pdus)
|
defer.returnValue(valid_pdus)
|
||||||
@@ -130,7 +126,7 @@ class FederationBase(object):
|
|||||||
for pdu in pdus
|
for pdu in pdus
|
||||||
]
|
]
|
||||||
|
|
||||||
deferreds = preserve_fn(self.keyring.verify_json_objects_for_server)([
|
deferreds = self.keyring.verify_json_objects_for_server([
|
||||||
(p.origin, p.get_pdu_json())
|
(p.origin, p.get_pdu_json())
|
||||||
for p in redacted_pdus
|
for p in redacted_pdus
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from twisted.internet import defer
|
|||||||
|
|
||||||
from .federation_base import FederationBase
|
from .federation_base import FederationBase
|
||||||
from synapse.api.constants import Membership
|
from synapse.api.constants import Membership
|
||||||
|
from .units import Edu
|
||||||
|
|
||||||
from synapse.api.errors import (
|
from synapse.api.errors import (
|
||||||
CodeMessageException, HttpResponseException, SynapseError,
|
CodeMessageException, HttpResponseException, SynapseError,
|
||||||
@@ -25,8 +26,7 @@ from synapse.api.errors import (
|
|||||||
from synapse.util import unwrapFirstError
|
from synapse.util import unwrapFirstError
|
||||||
from synapse.util.caches.expiringcache import ExpiringCache
|
from synapse.util.caches.expiringcache import ExpiringCache
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
from synapse.util.logcontext import preserve_fn, preserve_context_over_deferred
|
from synapse.events import FrozenEvent
|
||||||
from synapse.events import FrozenEvent, builder
|
|
||||||
import synapse.metrics
|
import synapse.metrics
|
||||||
|
|
||||||
from synapse.util.retryutils import get_retry_limiter, NotRetryingDestination
|
from synapse.util.retryutils import get_retry_limiter, NotRetryingDestination
|
||||||
@@ -43,37 +43,14 @@ logger = logging.getLogger(__name__)
|
|||||||
# synapse.federation.federation_client is a silly name
|
# synapse.federation.federation_client is a silly name
|
||||||
metrics = synapse.metrics.get_metrics_for("synapse.federation.client")
|
metrics = synapse.metrics.get_metrics_for("synapse.federation.client")
|
||||||
|
|
||||||
|
sent_pdus_destination_dist = metrics.register_distribution("sent_pdu_destinations")
|
||||||
|
|
||||||
|
sent_edus_counter = metrics.register_counter("sent_edus")
|
||||||
|
|
||||||
sent_queries_counter = metrics.register_counter("sent_queries", labels=["type"])
|
sent_queries_counter = metrics.register_counter("sent_queries", labels=["type"])
|
||||||
|
|
||||||
|
|
||||||
PDU_RETRY_TIME_MS = 1 * 60 * 1000
|
|
||||||
|
|
||||||
|
|
||||||
class FederationClient(FederationBase):
|
class FederationClient(FederationBase):
|
||||||
def __init__(self, hs):
|
|
||||||
super(FederationClient, self).__init__(hs)
|
|
||||||
|
|
||||||
self.pdu_destination_tried = {}
|
|
||||||
self._clock.looping_call(
|
|
||||||
self._clear_tried_cache, 60 * 1000,
|
|
||||||
)
|
|
||||||
self.state = hs.get_state_handler()
|
|
||||||
|
|
||||||
def _clear_tried_cache(self):
|
|
||||||
"""Clear pdu_destination_tried cache"""
|
|
||||||
now = self._clock.time_msec()
|
|
||||||
|
|
||||||
old_dict = self.pdu_destination_tried
|
|
||||||
self.pdu_destination_tried = {}
|
|
||||||
|
|
||||||
for event_id, destination_dict in old_dict.items():
|
|
||||||
destination_dict = {
|
|
||||||
dest: time
|
|
||||||
for dest, time in destination_dict.items()
|
|
||||||
if time + PDU_RETRY_TIME_MS > now
|
|
||||||
}
|
|
||||||
if destination_dict:
|
|
||||||
self.pdu_destination_tried[event_id] = destination_dict
|
|
||||||
|
|
||||||
def start_get_pdu_cache(self):
|
def start_get_pdu_cache(self):
|
||||||
self._get_pdu_cache = ExpiringCache(
|
self._get_pdu_cache = ExpiringCache(
|
||||||
@@ -86,6 +63,55 @@ class FederationClient(FederationBase):
|
|||||||
|
|
||||||
self._get_pdu_cache.start()
|
self._get_pdu_cache.start()
|
||||||
|
|
||||||
|
@log_function
|
||||||
|
def send_pdu(self, pdu, destinations):
|
||||||
|
"""Informs the replication layer about a new PDU generated within the
|
||||||
|
home server that should be transmitted to others.
|
||||||
|
|
||||||
|
TODO: Figure out when we should actually resolve the deferred.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pdu (Pdu): The new Pdu.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Deferred: Completes when we have successfully processed the PDU
|
||||||
|
and replicated it to any interested remote home servers.
|
||||||
|
"""
|
||||||
|
order = self._order
|
||||||
|
self._order += 1
|
||||||
|
|
||||||
|
sent_pdus_destination_dist.inc_by(len(destinations))
|
||||||
|
|
||||||
|
logger.debug("[%s] transaction_layer.enqueue_pdu... ", pdu.event_id)
|
||||||
|
|
||||||
|
# TODO, add errback, etc.
|
||||||
|
self._transaction_queue.enqueue_pdu(pdu, destinations, order)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"[%s] transaction_layer.enqueue_pdu... done",
|
||||||
|
pdu.event_id
|
||||||
|
)
|
||||||
|
|
||||||
|
@log_function
|
||||||
|
def send_edu(self, destination, edu_type, content):
|
||||||
|
edu = Edu(
|
||||||
|
origin=self.server_name,
|
||||||
|
destination=destination,
|
||||||
|
edu_type=edu_type,
|
||||||
|
content=content,
|
||||||
|
)
|
||||||
|
|
||||||
|
sent_edus_counter.inc()
|
||||||
|
|
||||||
|
# TODO, add errback, etc.
|
||||||
|
self._transaction_queue.enqueue_edu(edu)
|
||||||
|
return defer.succeed(None)
|
||||||
|
|
||||||
|
@log_function
|
||||||
|
def send_failure(self, failure, destination):
|
||||||
|
self._transaction_queue.enqueue_failure(failure, destination)
|
||||||
|
return defer.succeed(None)
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def make_query(self, destination, query_type, args,
|
def make_query(self, destination, query_type, args,
|
||||||
retry_on_dns_fail=False):
|
retry_on_dns_fail=False):
|
||||||
@@ -110,7 +136,7 @@ class FederationClient(FederationBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def query_client_keys(self, destination, content, timeout):
|
def query_client_keys(self, destination, content):
|
||||||
"""Query device keys for a device hosted on a remote server.
|
"""Query device keys for a device hosted on a remote server.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -122,22 +148,10 @@ class FederationClient(FederationBase):
|
|||||||
response
|
response
|
||||||
"""
|
"""
|
||||||
sent_queries_counter.inc("client_device_keys")
|
sent_queries_counter.inc("client_device_keys")
|
||||||
return self.transport_layer.query_client_keys(
|
return self.transport_layer.query_client_keys(destination, content)
|
||||||
destination, content, timeout
|
|
||||||
)
|
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def query_user_devices(self, destination, user_id, timeout=30000):
|
def claim_client_keys(self, destination, content):
|
||||||
"""Query the device keys for a list of user ids hosted on a remote
|
|
||||||
server.
|
|
||||||
"""
|
|
||||||
sent_queries_counter.inc("user_devices")
|
|
||||||
return self.transport_layer.query_user_devices(
|
|
||||||
destination, user_id, timeout
|
|
||||||
)
|
|
||||||
|
|
||||||
@log_function
|
|
||||||
def claim_client_keys(self, destination, content, timeout):
|
|
||||||
"""Claims one-time keys for a device hosted on a remote server.
|
"""Claims one-time keys for a device hosted on a remote server.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -149,9 +163,7 @@ class FederationClient(FederationBase):
|
|||||||
response
|
response
|
||||||
"""
|
"""
|
||||||
sent_queries_counter.inc("client_one_time_keys")
|
sent_queries_counter.inc("client_one_time_keys")
|
||||||
return self.transport_layer.claim_client_keys(
|
return self.transport_layer.claim_client_keys(destination, content)
|
||||||
destination, content, timeout
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
@@ -186,10 +198,10 @@ class FederationClient(FederationBase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
# FIXME: We should handle signature failures more gracefully.
|
# FIXME: We should handle signature failures more gracefully.
|
||||||
pdus[:] = yield preserve_context_over_deferred(defer.gatherResults(
|
pdus[:] = yield defer.gatherResults(
|
||||||
self._check_sigs_and_hashes(pdus),
|
self._check_sigs_and_hashes(pdus),
|
||||||
consumeErrors=True,
|
consumeErrors=True,
|
||||||
)).addErrback(unwrapFirstError)
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
defer.returnValue(pdus)
|
defer.returnValue(pdus)
|
||||||
|
|
||||||
@@ -221,19 +233,12 @@ class FederationClient(FederationBase):
|
|||||||
# TODO: Rate limit the number of times we try and get the same event.
|
# TODO: Rate limit the number of times we try and get the same event.
|
||||||
|
|
||||||
if self._get_pdu_cache:
|
if self._get_pdu_cache:
|
||||||
ev = self._get_pdu_cache.get(event_id)
|
e = self._get_pdu_cache.get(event_id)
|
||||||
if ev:
|
if e:
|
||||||
defer.returnValue(ev)
|
defer.returnValue(e)
|
||||||
|
|
||||||
pdu_attempts = self.pdu_destination_tried.setdefault(event_id, {})
|
pdu = None
|
||||||
|
|
||||||
signed_pdu = None
|
|
||||||
for destination in destinations:
|
for destination in destinations:
|
||||||
now = self._clock.time_msec()
|
|
||||||
last_attempt = pdu_attempts.get(destination, 0)
|
|
||||||
if last_attempt + PDU_RETRY_TIME_MS > now:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
limiter = yield get_retry_limiter(
|
limiter = yield get_retry_limiter(
|
||||||
destination,
|
destination,
|
||||||
@@ -257,33 +262,39 @@ class FederationClient(FederationBase):
|
|||||||
pdu = pdu_list[0]
|
pdu = pdu_list[0]
|
||||||
|
|
||||||
# Check signatures are correct.
|
# Check signatures are correct.
|
||||||
signed_pdu = yield self._check_sigs_and_hashes([pdu])[0]
|
pdu = yield self._check_sigs_and_hashes([pdu])[0]
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
pdu_attempts[destination] = now
|
except SynapseError:
|
||||||
|
|
||||||
except SynapseError as e:
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Failed to get PDU %s from %s because %s",
|
"Failed to get PDU %s from %s because %s",
|
||||||
event_id, destination, e,
|
event_id, destination, e,
|
||||||
)
|
)
|
||||||
|
continue
|
||||||
|
except CodeMessageException as e:
|
||||||
|
if 400 <= e.code < 500:
|
||||||
|
raise
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Failed to get PDU %s from %s because %s",
|
||||||
|
event_id, destination, e,
|
||||||
|
)
|
||||||
|
continue
|
||||||
except NotRetryingDestination as e:
|
except NotRetryingDestination as e:
|
||||||
logger.info(e.message)
|
logger.info(e.message)
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pdu_attempts[destination] = now
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Failed to get PDU %s from %s because %s",
|
"Failed to get PDU %s from %s because %s",
|
||||||
event_id, destination, e,
|
event_id, destination, e,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self._get_pdu_cache is not None and signed_pdu:
|
if self._get_pdu_cache is not None and pdu:
|
||||||
self._get_pdu_cache[event_id] = signed_pdu
|
self._get_pdu_cache[event_id] = pdu
|
||||||
|
|
||||||
defer.returnValue(signed_pdu)
|
defer.returnValue(pdu)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
@@ -300,42 +311,6 @@ class FederationClient(FederationBase):
|
|||||||
Deferred: Results in a list of PDUs.
|
Deferred: Results in a list of PDUs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
|
||||||
# First we try and ask for just the IDs, as thats far quicker if
|
|
||||||
# we have most of the state and auth_chain already.
|
|
||||||
# However, this may 404 if the other side has an old synapse.
|
|
||||||
result = yield self.transport_layer.get_room_state_ids(
|
|
||||||
destination, room_id, event_id=event_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
state_event_ids = result["pdu_ids"]
|
|
||||||
auth_event_ids = result.get("auth_chain_ids", [])
|
|
||||||
|
|
||||||
fetched_events, failed_to_fetch = yield self.get_events(
|
|
||||||
[destination], room_id, set(state_event_ids + auth_event_ids)
|
|
||||||
)
|
|
||||||
|
|
||||||
if failed_to_fetch:
|
|
||||||
logger.warn("Failed to get %r", failed_to_fetch)
|
|
||||||
|
|
||||||
event_map = {
|
|
||||||
ev.event_id: ev for ev in fetched_events
|
|
||||||
}
|
|
||||||
|
|
||||||
pdus = [event_map[e_id] for e_id in state_event_ids if e_id in event_map]
|
|
||||||
auth_chain = [
|
|
||||||
event_map[e_id] for e_id in auth_event_ids if e_id in event_map
|
|
||||||
]
|
|
||||||
|
|
||||||
auth_chain.sort(key=lambda e: e.depth)
|
|
||||||
|
|
||||||
defer.returnValue((pdus, auth_chain))
|
|
||||||
except HttpResponseException as e:
|
|
||||||
if e.code == 400 or e.code == 404:
|
|
||||||
logger.info("Failed to use get_room_state_ids API, falling back")
|
|
||||||
else:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
result = yield self.transport_layer.get_room_state(
|
result = yield self.transport_layer.get_room_state(
|
||||||
destination, room_id, event_id=event_id,
|
destination, room_id, event_id=event_id,
|
||||||
)
|
)
|
||||||
@@ -349,95 +324,18 @@ class FederationClient(FederationBase):
|
|||||||
for p in result.get("auth_chain", [])
|
for p in result.get("auth_chain", [])
|
||||||
]
|
]
|
||||||
|
|
||||||
seen_events = yield self.store.get_events([
|
|
||||||
ev.event_id for ev in itertools.chain(pdus, auth_chain)
|
|
||||||
])
|
|
||||||
|
|
||||||
signed_pdus = yield self._check_sigs_and_hash_and_fetch(
|
signed_pdus = yield self._check_sigs_and_hash_and_fetch(
|
||||||
destination,
|
destination, pdus, outlier=True
|
||||||
[p for p in pdus if p.event_id not in seen_events],
|
|
||||||
outlier=True
|
|
||||||
)
|
|
||||||
signed_pdus.extend(
|
|
||||||
seen_events[p.event_id] for p in pdus if p.event_id in seen_events
|
|
||||||
)
|
)
|
||||||
|
|
||||||
signed_auth = yield self._check_sigs_and_hash_and_fetch(
|
signed_auth = yield self._check_sigs_and_hash_and_fetch(
|
||||||
destination,
|
destination, auth_chain, outlier=True
|
||||||
[p for p in auth_chain if p.event_id not in seen_events],
|
|
||||||
outlier=True
|
|
||||||
)
|
|
||||||
signed_auth.extend(
|
|
||||||
seen_events[p.event_id] for p in auth_chain if p.event_id in seen_events
|
|
||||||
)
|
)
|
||||||
|
|
||||||
signed_auth.sort(key=lambda e: e.depth)
|
signed_auth.sort(key=lambda e: e.depth)
|
||||||
|
|
||||||
defer.returnValue((signed_pdus, signed_auth))
|
defer.returnValue((signed_pdus, signed_auth))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def get_events(self, destinations, room_id, event_ids, return_local=True):
|
|
||||||
"""Fetch events from some remote destinations, checking if we already
|
|
||||||
have them.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
destinations (list)
|
|
||||||
room_id (str)
|
|
||||||
event_ids (list)
|
|
||||||
return_local (bool): Whether to include events we already have in
|
|
||||||
the DB in the returned list of events
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Deferred: A deferred resolving to a 2-tuple where the first is a list of
|
|
||||||
events and the second is a list of event ids that we failed to fetch.
|
|
||||||
"""
|
|
||||||
if return_local:
|
|
||||||
seen_events = yield self.store.get_events(event_ids, allow_rejected=True)
|
|
||||||
signed_events = seen_events.values()
|
|
||||||
else:
|
|
||||||
seen_events = yield self.store.have_events(event_ids)
|
|
||||||
signed_events = []
|
|
||||||
|
|
||||||
failed_to_fetch = set()
|
|
||||||
|
|
||||||
missing_events = set(event_ids)
|
|
||||||
for k in seen_events:
|
|
||||||
missing_events.discard(k)
|
|
||||||
|
|
||||||
if not missing_events:
|
|
||||||
defer.returnValue((signed_events, failed_to_fetch))
|
|
||||||
|
|
||||||
def random_server_list():
|
|
||||||
srvs = list(destinations)
|
|
||||||
random.shuffle(srvs)
|
|
||||||
return srvs
|
|
||||||
|
|
||||||
batch_size = 20
|
|
||||||
missing_events = list(missing_events)
|
|
||||||
for i in xrange(0, len(missing_events), batch_size):
|
|
||||||
batch = set(missing_events[i:i + batch_size])
|
|
||||||
|
|
||||||
deferreds = [
|
|
||||||
preserve_fn(self.get_pdu)(
|
|
||||||
destinations=random_server_list(),
|
|
||||||
event_id=e_id,
|
|
||||||
)
|
|
||||||
for e_id in batch
|
|
||||||
]
|
|
||||||
|
|
||||||
res = yield preserve_context_over_deferred(
|
|
||||||
defer.DeferredList(deferreds, consumeErrors=True)
|
|
||||||
)
|
|
||||||
for success, result in res:
|
|
||||||
if success and result:
|
|
||||||
signed_events.append(result)
|
|
||||||
batch.discard(result.event_id)
|
|
||||||
|
|
||||||
# We removed all events we successfully fetched from `batch`
|
|
||||||
failed_to_fetch.update(batch)
|
|
||||||
|
|
||||||
defer.returnValue((signed_events, failed_to_fetch))
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def get_event_auth(self, destination, room_id, event_id):
|
def get_event_auth(self, destination, room_id, event_id):
|
||||||
@@ -509,25 +407,18 @@ class FederationClient(FederationBase):
|
|||||||
if "prev_state" not in pdu_dict:
|
if "prev_state" not in pdu_dict:
|
||||||
pdu_dict["prev_state"] = []
|
pdu_dict["prev_state"] = []
|
||||||
|
|
||||||
ev = builder.EventBuilder(pdu_dict)
|
|
||||||
|
|
||||||
defer.returnValue(
|
defer.returnValue(
|
||||||
(destination, ev)
|
(destination, self.event_from_pdu_json(pdu_dict))
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
except CodeMessageException as e:
|
except CodeMessageException:
|
||||||
if not 500 <= e.code < 600:
|
raise
|
||||||
raise
|
|
||||||
else:
|
|
||||||
logger.warn(
|
|
||||||
"Failed to make_%s via %s: %s",
|
|
||||||
membership, destination, e.message
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"Failed to make_%s via %s: %s",
|
"Failed to make_%s via %s: %s",
|
||||||
membership, destination, e.message
|
membership, destination, e.message
|
||||||
)
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
raise RuntimeError("Failed to send to any server.")
|
raise RuntimeError("Failed to send to any server.")
|
||||||
|
|
||||||
@@ -599,14 +490,8 @@ class FederationClient(FederationBase):
|
|||||||
"auth_chain": signed_auth,
|
"auth_chain": signed_auth,
|
||||||
"origin": destination,
|
"origin": destination,
|
||||||
})
|
})
|
||||||
except CodeMessageException as e:
|
except CodeMessageException:
|
||||||
if not 500 <= e.code < 600:
|
raise
|
||||||
raise
|
|
||||||
else:
|
|
||||||
logger.exception(
|
|
||||||
"Failed to send_join via %s: %s",
|
|
||||||
destination, e.message
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Failed to send_join via %s: %s",
|
"Failed to send_join via %s: %s",
|
||||||
@@ -665,18 +550,6 @@ class FederationClient(FederationBase):
|
|||||||
|
|
||||||
raise RuntimeError("Failed to send to any server.")
|
raise RuntimeError("Failed to send to any server.")
|
||||||
|
|
||||||
def get_public_rooms(self, destination, limit=None, since_token=None,
|
|
||||||
search_filter=None, include_all_networks=False,
|
|
||||||
third_party_instance_id=None):
|
|
||||||
if destination == self.server_name:
|
|
||||||
return
|
|
||||||
|
|
||||||
return self.transport_layer.get_public_rooms(
|
|
||||||
destination, limit, since_token, search_filter,
|
|
||||||
include_all_networks=include_all_networks,
|
|
||||||
third_party_instance_id=third_party_instance_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def query_auth(self, destination, room_id, event_id, local_auth):
|
def query_auth(self, destination, room_id, event_id, local_auth):
|
||||||
"""
|
"""
|
||||||
@@ -719,7 +592,7 @@ class FederationClient(FederationBase):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_missing_events(self, destination, room_id, earliest_events_ids,
|
def get_missing_events(self, destination, room_id, earliest_events_ids,
|
||||||
latest_events, limit, min_depth, timeout):
|
latest_events, limit, min_depth):
|
||||||
"""Tries to fetch events we are missing. This is called when we receive
|
"""Tries to fetch events we are missing. This is called when we receive
|
||||||
an event without having received all of its ancestors.
|
an event without having received all of its ancestors.
|
||||||
|
|
||||||
@@ -733,7 +606,6 @@ class FederationClient(FederationBase):
|
|||||||
have all previous events for.
|
have all previous events for.
|
||||||
limit (int): Maximum number of events to return.
|
limit (int): Maximum number of events to return.
|
||||||
min_depth (int): Minimum depth of events tor return.
|
min_depth (int): Minimum depth of events tor return.
|
||||||
timeout (int): Max time to wait in ms
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
content = yield self.transport_layer.get_missing_events(
|
content = yield self.transport_layer.get_missing_events(
|
||||||
@@ -743,7 +615,6 @@ class FederationClient(FederationBase):
|
|||||||
latest_events=[e.event_id for e in latest_events],
|
latest_events=[e.event_id for e in latest_events],
|
||||||
limit=limit,
|
limit=limit,
|
||||||
min_depth=min_depth,
|
min_depth=min_depth,
|
||||||
timeout=timeout,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
events = [
|
events = [
|
||||||
@@ -754,6 +625,8 @@ class FederationClient(FederationBase):
|
|||||||
signed_events = yield self._check_sigs_and_hash_and_fetch(
|
signed_events = yield self._check_sigs_and_hash_and_fetch(
|
||||||
destination, events, outlier=False
|
destination, events, outlier=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
have_gotten_all_from_destination = True
|
||||||
except HttpResponseException as e:
|
except HttpResponseException as e:
|
||||||
if not e.code == 400:
|
if not e.code == 400:
|
||||||
raise
|
raise
|
||||||
@@ -761,6 +634,69 @@ class FederationClient(FederationBase):
|
|||||||
# We are probably hitting an old server that doesn't support
|
# We are probably hitting an old server that doesn't support
|
||||||
# get_missing_events
|
# get_missing_events
|
||||||
signed_events = []
|
signed_events = []
|
||||||
|
have_gotten_all_from_destination = False
|
||||||
|
|
||||||
|
if len(signed_events) >= limit:
|
||||||
|
defer.returnValue(signed_events)
|
||||||
|
|
||||||
|
servers = yield self.store.get_joined_hosts_for_room(room_id)
|
||||||
|
|
||||||
|
servers = set(servers)
|
||||||
|
servers.discard(self.server_name)
|
||||||
|
|
||||||
|
failed_to_fetch = set()
|
||||||
|
|
||||||
|
while len(signed_events) < limit:
|
||||||
|
# Are we missing any?
|
||||||
|
|
||||||
|
seen_events = set(earliest_events_ids)
|
||||||
|
seen_events.update(e.event_id for e in signed_events if e)
|
||||||
|
|
||||||
|
missing_events = {}
|
||||||
|
for e in itertools.chain(latest_events, signed_events):
|
||||||
|
if e.depth > min_depth:
|
||||||
|
missing_events.update({
|
||||||
|
e_id: e.depth for e_id, _ in e.prev_events
|
||||||
|
if e_id not in seen_events
|
||||||
|
and e_id not in failed_to_fetch
|
||||||
|
})
|
||||||
|
|
||||||
|
if not missing_events:
|
||||||
|
break
|
||||||
|
|
||||||
|
have_seen = yield self.store.have_events(missing_events)
|
||||||
|
|
||||||
|
for k in have_seen:
|
||||||
|
missing_events.pop(k, None)
|
||||||
|
|
||||||
|
if not missing_events:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Okay, we haven't gotten everything yet. Lets get them.
|
||||||
|
ordered_missing = sorted(missing_events.items(), key=lambda x: x[0])
|
||||||
|
|
||||||
|
if have_gotten_all_from_destination:
|
||||||
|
servers.discard(destination)
|
||||||
|
|
||||||
|
def random_server_list():
|
||||||
|
srvs = list(servers)
|
||||||
|
random.shuffle(srvs)
|
||||||
|
return srvs
|
||||||
|
|
||||||
|
deferreds = [
|
||||||
|
self.get_pdu(
|
||||||
|
destinations=random_server_list(),
|
||||||
|
event_id=e_id,
|
||||||
|
)
|
||||||
|
for e_id, depth in ordered_missing[:limit - len(signed_events)]
|
||||||
|
]
|
||||||
|
|
||||||
|
res = yield defer.DeferredList(deferreds, consumeErrors=True)
|
||||||
|
for (result, val), (e_id, _) in zip(res, ordered_missing):
|
||||||
|
if result and val:
|
||||||
|
signed_events.append(val)
|
||||||
|
else:
|
||||||
|
failed_to_fetch.add(e_id)
|
||||||
|
|
||||||
defer.returnValue(signed_events)
|
defer.returnValue(signed_events)
|
||||||
|
|
||||||
|
|||||||
@@ -19,14 +19,11 @@ from twisted.internet import defer
|
|||||||
from .federation_base import FederationBase
|
from .federation_base import FederationBase
|
||||||
from .units import Transaction, Edu
|
from .units import Transaction, Edu
|
||||||
|
|
||||||
from synapse.util.async import Linearizer
|
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
from synapse.util.caches.response_cache import ResponseCache
|
|
||||||
from synapse.events import FrozenEvent
|
from synapse.events import FrozenEvent
|
||||||
from synapse.types import get_domain_from_id
|
|
||||||
import synapse.metrics
|
import synapse.metrics
|
||||||
|
|
||||||
from synapse.api.errors import AuthError, FederationError, SynapseError
|
from synapse.api.errors import FederationError, SynapseError
|
||||||
|
|
||||||
from synapse.crypto.event_signing import compute_event_signature
|
from synapse.crypto.event_signing import compute_event_signature
|
||||||
|
|
||||||
@@ -47,18 +44,6 @@ received_queries_counter = metrics.register_counter("received_queries", labels=[
|
|||||||
|
|
||||||
|
|
||||||
class FederationServer(FederationBase):
|
class FederationServer(FederationBase):
|
||||||
def __init__(self, hs):
|
|
||||||
super(FederationServer, self).__init__(hs)
|
|
||||||
|
|
||||||
self.auth = hs.get_auth()
|
|
||||||
|
|
||||||
self._room_pdu_linearizer = Linearizer("fed_room_pdu")
|
|
||||||
self._server_linearizer = Linearizer("fed_server")
|
|
||||||
|
|
||||||
# We cache responses to state queries, as they take a while and often
|
|
||||||
# come in waves.
|
|
||||||
self._state_resp_cache = ResponseCache(hs, timeout_ms=30000)
|
|
||||||
|
|
||||||
def set_handler(self, handler):
|
def set_handler(self, handler):
|
||||||
"""Sets the handler that the replication layer will use to communicate
|
"""Sets the handler that the replication layer will use to communicate
|
||||||
receipt of new PDUs from other home servers. The required methods are
|
receipt of new PDUs from other home servers. The required methods are
|
||||||
@@ -98,14 +83,11 @@ class FederationServer(FederationBase):
|
|||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def on_backfill_request(self, origin, room_id, versions, limit):
|
def on_backfill_request(self, origin, room_id, versions, limit):
|
||||||
with (yield self._server_linearizer.queue((origin, room_id))):
|
pdus = yield self.handler.on_backfill_request(
|
||||||
pdus = yield self.handler.on_backfill_request(
|
origin, room_id, versions, limit
|
||||||
origin, room_id, versions, limit
|
)
|
||||||
)
|
|
||||||
|
|
||||||
res = self._transaction_from_pdus(pdus).get_dict()
|
defer.returnValue((200, self._transaction_from_pdus(pdus).get_dict()))
|
||||||
|
|
||||||
defer.returnValue((200, res))
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
@@ -133,7 +115,7 @@ class FederationServer(FederationBase):
|
|||||||
|
|
||||||
if response:
|
if response:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"[%s] We've already responded to this request",
|
"[%s] We've already responed to this request",
|
||||||
transaction.transaction_id
|
transaction.transaction_id
|
||||||
)
|
)
|
||||||
defer.returnValue(response)
|
defer.returnValue(response)
|
||||||
@@ -144,26 +126,6 @@ class FederationServer(FederationBase):
|
|||||||
results = []
|
results = []
|
||||||
|
|
||||||
for pdu in pdu_list:
|
for pdu in pdu_list:
|
||||||
# check that it's actually being sent from a valid destination to
|
|
||||||
# workaround bug #1753 in 0.18.5 and 0.18.6
|
|
||||||
if transaction.origin != get_domain_from_id(pdu.event_id):
|
|
||||||
if not (
|
|
||||||
pdu.type == 'm.room.member' and
|
|
||||||
pdu.content and
|
|
||||||
pdu.content.get("membership", None) == 'join' and
|
|
||||||
self.hs.is_mine_id(pdu.state_key)
|
|
||||||
):
|
|
||||||
logger.info(
|
|
||||||
"Discarding PDU %s from invalid origin %s",
|
|
||||||
pdu.event_id, transaction.origin
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
logger.info(
|
|
||||||
"Accepting join PDU %s from %s",
|
|
||||||
pdu.event_id, transaction.origin
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield self._handle_new_pdu(transaction.origin, pdu)
|
yield self._handle_new_pdu(transaction.origin, pdu)
|
||||||
results.append({})
|
results.append({})
|
||||||
@@ -209,64 +171,22 @@ class FederationServer(FederationBase):
|
|||||||
except SynapseError as e:
|
except SynapseError as e:
|
||||||
logger.info("Failed to handle edu %r: %r", edu_type, e)
|
logger.info("Failed to handle edu %r: %r", edu_type, e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Failed to handle edu %r", edu_type)
|
logger.exception("Failed to handle edu %r", edu_type, e)
|
||||||
else:
|
else:
|
||||||
logger.warn("Received EDU of type %s with no handler", edu_type)
|
logger.warn("Received EDU of type %s with no handler", edu_type)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def on_context_state_request(self, origin, room_id, event_id):
|
def on_context_state_request(self, origin, room_id, event_id):
|
||||||
if not event_id:
|
if event_id:
|
||||||
raise NotImplementedError("Specify an event")
|
pdus = yield self.handler.get_state_for_pdu(
|
||||||
|
origin, room_id, event_id,
|
||||||
|
)
|
||||||
|
auth_chain = yield self.store.get_auth_chain(
|
||||||
|
[pdu.event_id for pdu in pdus]
|
||||||
|
)
|
||||||
|
|
||||||
in_room = yield self.auth.check_host_in_room(room_id, origin)
|
for event in auth_chain:
|
||||||
if not in_room:
|
|
||||||
raise AuthError(403, "Host not in room.")
|
|
||||||
|
|
||||||
result = self._state_resp_cache.get((room_id, event_id))
|
|
||||||
if not result:
|
|
||||||
with (yield self._server_linearizer.queue((origin, room_id))):
|
|
||||||
resp = yield self._state_resp_cache.set(
|
|
||||||
(room_id, event_id),
|
|
||||||
self._on_context_state_request_compute(room_id, event_id)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
resp = yield result
|
|
||||||
|
|
||||||
defer.returnValue((200, resp))
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def on_state_ids_request(self, origin, room_id, event_id):
|
|
||||||
if not event_id:
|
|
||||||
raise NotImplementedError("Specify an event")
|
|
||||||
|
|
||||||
in_room = yield self.auth.check_host_in_room(room_id, origin)
|
|
||||||
if not in_room:
|
|
||||||
raise AuthError(403, "Host not in room.")
|
|
||||||
|
|
||||||
state_ids = yield self.handler.get_state_ids_for_pdu(
|
|
||||||
room_id, event_id,
|
|
||||||
)
|
|
||||||
auth_chain_ids = yield self.store.get_auth_chain_ids(state_ids)
|
|
||||||
|
|
||||||
defer.returnValue((200, {
|
|
||||||
"pdu_ids": state_ids,
|
|
||||||
"auth_chain_ids": auth_chain_ids,
|
|
||||||
}))
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _on_context_state_request_compute(self, room_id, event_id):
|
|
||||||
pdus = yield self.handler.get_state_for_pdu(
|
|
||||||
room_id, event_id,
|
|
||||||
)
|
|
||||||
auth_chain = yield self.store.get_auth_chain(
|
|
||||||
[pdu.event_id for pdu in pdus]
|
|
||||||
)
|
|
||||||
|
|
||||||
for event in auth_chain:
|
|
||||||
# We sign these again because there was a bug where we
|
|
||||||
# incorrectly signed things the first time round
|
|
||||||
if self.hs.is_mine_id(event.event_id):
|
|
||||||
event.signatures.update(
|
event.signatures.update(
|
||||||
compute_event_signature(
|
compute_event_signature(
|
||||||
event,
|
event,
|
||||||
@@ -274,11 +194,13 @@ class FederationServer(FederationBase):
|
|||||||
self.hs.config.signing_key[0]
|
self.hs.config.signing_key[0]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("Specify an event")
|
||||||
|
|
||||||
defer.returnValue({
|
defer.returnValue((200, {
|
||||||
"pdus": [pdu.get_pdu_json() for pdu in pdus],
|
"pdus": [pdu.get_pdu_json() for pdu in pdus],
|
||||||
"auth_chain": [pdu.get_pdu_json() for pdu in auth_chain],
|
"auth_chain": [pdu.get_pdu_json() for pdu in auth_chain],
|
||||||
})
|
}))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
@@ -352,16 +274,14 @@ class FederationServer(FederationBase):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_event_auth(self, origin, room_id, event_id):
|
def on_event_auth(self, origin, room_id, event_id):
|
||||||
with (yield self._server_linearizer.queue((origin, room_id))):
|
time_now = self._clock.time_msec()
|
||||||
time_now = self._clock.time_msec()
|
auth_pdus = yield self.handler.on_event_auth(event_id)
|
||||||
auth_pdus = yield self.handler.on_event_auth(event_id)
|
defer.returnValue((200, {
|
||||||
res = {
|
"auth_chain": [a.get_pdu_json(time_now) for a in auth_pdus],
|
||||||
"auth_chain": [a.get_pdu_json(time_now) for a in auth_pdus],
|
}))
|
||||||
}
|
|
||||||
defer.returnValue((200, res))
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_query_auth_request(self, origin, content, room_id, event_id):
|
def on_query_auth_request(self, origin, content, event_id):
|
||||||
"""
|
"""
|
||||||
Content is a dict with keys::
|
Content is a dict with keys::
|
||||||
auth_chain (list): A list of events that give the auth chain.
|
auth_chain (list): A list of events that give the auth chain.
|
||||||
@@ -380,44 +300,58 @@ class FederationServer(FederationBase):
|
|||||||
Returns:
|
Returns:
|
||||||
Deferred: Results in `dict` with the same format as `content`
|
Deferred: Results in `dict` with the same format as `content`
|
||||||
"""
|
"""
|
||||||
with (yield self._server_linearizer.queue((origin, room_id))):
|
auth_chain = [
|
||||||
auth_chain = [
|
self.event_from_pdu_json(e)
|
||||||
self.event_from_pdu_json(e)
|
for e in content["auth_chain"]
|
||||||
for e in content["auth_chain"]
|
]
|
||||||
]
|
|
||||||
|
|
||||||
signed_auth = yield self._check_sigs_and_hash_and_fetch(
|
signed_auth = yield self._check_sigs_and_hash_and_fetch(
|
||||||
origin, auth_chain, outlier=True
|
origin, auth_chain, outlier=True
|
||||||
)
|
)
|
||||||
|
|
||||||
ret = yield self.handler.on_query_auth(
|
ret = yield self.handler.on_query_auth(
|
||||||
origin,
|
origin,
|
||||||
event_id,
|
event_id,
|
||||||
signed_auth,
|
signed_auth,
|
||||||
content.get("rejects", []),
|
content.get("rejects", []),
|
||||||
content.get("missing", []),
|
content.get("missing", []),
|
||||||
)
|
)
|
||||||
|
|
||||||
time_now = self._clock.time_msec()
|
time_now = self._clock.time_msec()
|
||||||
send_content = {
|
send_content = {
|
||||||
"auth_chain": [
|
"auth_chain": [
|
||||||
e.get_pdu_json(time_now)
|
e.get_pdu_json(time_now)
|
||||||
for e in ret["auth_chain"]
|
for e in ret["auth_chain"]
|
||||||
],
|
],
|
||||||
"rejects": ret.get("rejects", []),
|
"rejects": ret.get("rejects", []),
|
||||||
"missing": ret.get("missing", []),
|
"missing": ret.get("missing", []),
|
||||||
}
|
}
|
||||||
|
|
||||||
defer.returnValue(
|
defer.returnValue(
|
||||||
(200, send_content)
|
(200, send_content)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def on_query_client_keys(self, origin, content):
|
def on_query_client_keys(self, origin, content):
|
||||||
return self.on_query_request("client_keys", content)
|
query = []
|
||||||
|
for user_id, device_ids in content.get("device_keys", {}).items():
|
||||||
|
if not device_ids:
|
||||||
|
query.append((user_id, None))
|
||||||
|
else:
|
||||||
|
for device_id in device_ids:
|
||||||
|
query.append((user_id, device_id))
|
||||||
|
|
||||||
def on_query_user_devices(self, origin, user_id):
|
results = yield self.store.get_e2e_device_keys(query)
|
||||||
return self.on_query_request("user_devices", user_id)
|
|
||||||
|
json_result = {}
|
||||||
|
for user_id, device_keys in results.items():
|
||||||
|
for device_id, json_bytes in device_keys.items():
|
||||||
|
json_result.setdefault(user_id, {})[device_id] = json.loads(
|
||||||
|
json_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue({"device_keys": json_result})
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
@@ -443,35 +377,16 @@ class FederationServer(FederationBase):
|
|||||||
@log_function
|
@log_function
|
||||||
def on_get_missing_events(self, origin, room_id, earliest_events,
|
def on_get_missing_events(self, origin, room_id, earliest_events,
|
||||||
latest_events, limit, min_depth):
|
latest_events, limit, min_depth):
|
||||||
with (yield self._server_linearizer.queue((origin, room_id))):
|
missing_events = yield self.handler.on_get_missing_events(
|
||||||
logger.info(
|
origin, room_id, earliest_events, latest_events, limit, min_depth
|
||||||
"on_get_missing_events: earliest_events: %r, latest_events: %r,"
|
)
|
||||||
" limit: %d, min_depth: %d",
|
|
||||||
earliest_events, latest_events, limit, min_depth
|
|
||||||
)
|
|
||||||
|
|
||||||
missing_events = yield self.handler.on_get_missing_events(
|
time_now = self._clock.time_msec()
|
||||||
origin, room_id, earliest_events, latest_events, limit, min_depth
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(missing_events) < 5:
|
|
||||||
logger.info(
|
|
||||||
"Returning %d events: %r", len(missing_events), missing_events
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.info("Returning %d events", len(missing_events))
|
|
||||||
|
|
||||||
time_now = self._clock.time_msec()
|
|
||||||
|
|
||||||
defer.returnValue({
|
defer.returnValue({
|
||||||
"events": [ev.get_pdu_json(time_now) for ev in missing_events],
|
"events": [ev.get_pdu_json(time_now) for ev in missing_events],
|
||||||
})
|
})
|
||||||
|
|
||||||
@log_function
|
|
||||||
def on_openid_userinfo(self, token):
|
|
||||||
ts_now_ms = self._clock.time_msec()
|
|
||||||
return self.store.get_user_id_for_open_id_token(token, ts_now_ms)
|
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def _get_persisted_pdu(self, origin, event_id, do_auth=True):
|
def _get_persisted_pdu(self, origin, event_id, do_auth=True):
|
||||||
""" Get a PDU from the database with given origin and id.
|
""" Get a PDU from the database with given origin and id.
|
||||||
@@ -499,7 +414,6 @@ class FederationServer(FederationBase):
|
|||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def _handle_new_pdu(self, origin, pdu, get_missing=True):
|
def _handle_new_pdu(self, origin, pdu, get_missing=True):
|
||||||
|
|
||||||
# We reprocess pdus when we have seen them only as outliers
|
# We reprocess pdus when we have seen them only as outliers
|
||||||
existing = yield self._get_persisted_pdu(
|
existing = yield self._get_persisted_pdu(
|
||||||
origin, pdu.event_id, do_auth=False
|
origin, pdu.event_id, do_auth=False
|
||||||
@@ -562,88 +476,42 @@ class FederationServer(FederationBase):
|
|||||||
pdu.internal_metadata.outlier = True
|
pdu.internal_metadata.outlier = True
|
||||||
elif min_depth and pdu.depth > min_depth:
|
elif min_depth and pdu.depth > min_depth:
|
||||||
if get_missing and prevs - seen:
|
if get_missing and prevs - seen:
|
||||||
# If we're missing stuff, ensure we only fetch stuff one
|
latest = yield self.store.get_latest_event_ids_in_room(
|
||||||
# at a time.
|
pdu.room_id
|
||||||
logger.info(
|
|
||||||
"Acquiring lock for room %r to fetch %d missing events: %r...",
|
|
||||||
pdu.room_id, len(prevs - seen), list(prevs - seen)[:5],
|
|
||||||
)
|
)
|
||||||
with (yield self._room_pdu_linearizer.queue(pdu.room_id)):
|
|
||||||
logger.info(
|
# We add the prev events that we have seen to the latest
|
||||||
"Acquired lock for room %r to fetch %d missing events",
|
# list to ensure the remote server doesn't give them to us
|
||||||
pdu.room_id, len(prevs - seen),
|
latest = set(latest)
|
||||||
|
latest |= seen
|
||||||
|
|
||||||
|
missing_events = yield self.get_missing_events(
|
||||||
|
origin,
|
||||||
|
pdu.room_id,
|
||||||
|
earliest_events_ids=list(latest),
|
||||||
|
latest_events=[pdu],
|
||||||
|
limit=10,
|
||||||
|
min_depth=min_depth,
|
||||||
|
)
|
||||||
|
|
||||||
|
# We want to sort these by depth so we process them and
|
||||||
|
# tell clients about them in order.
|
||||||
|
missing_events.sort(key=lambda x: x.depth)
|
||||||
|
|
||||||
|
for e in missing_events:
|
||||||
|
yield self._handle_new_pdu(
|
||||||
|
origin,
|
||||||
|
e,
|
||||||
|
get_missing=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# We recalculate seen, since it may have changed.
|
have_seen = yield self.store.have_events(
|
||||||
have_seen = yield self.store.have_events(prevs)
|
[ev for ev, _ in pdu.prev_events]
|
||||||
seen = set(have_seen.keys())
|
)
|
||||||
|
|
||||||
if prevs - seen:
|
|
||||||
latest = yield self.store.get_latest_event_ids_in_room(
|
|
||||||
pdu.room_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# We add the prev events that we have seen to the latest
|
|
||||||
# list to ensure the remote server doesn't give them to us
|
|
||||||
latest = set(latest)
|
|
||||||
latest |= seen
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
"Missing %d events for room %r: %r...",
|
|
||||||
len(prevs - seen), pdu.room_id, list(prevs - seen)[:5]
|
|
||||||
)
|
|
||||||
|
|
||||||
# XXX: we set timeout to 10s to help workaround
|
|
||||||
# https://github.com/matrix-org/synapse/issues/1733.
|
|
||||||
# The reason is to avoid holding the linearizer lock
|
|
||||||
# whilst processing inbound /send transactions, causing
|
|
||||||
# FDs to stack up and block other inbound transactions
|
|
||||||
# which empirically can currently take up to 30 minutes.
|
|
||||||
#
|
|
||||||
# N.B. this explicitly disables retry attempts.
|
|
||||||
#
|
|
||||||
# N.B. this also increases our chances of falling back to
|
|
||||||
# fetching fresh state for the room if the missing event
|
|
||||||
# can't be found, which slightly reduces our security.
|
|
||||||
# it may also increase our DAG extremity count for the room,
|
|
||||||
# causing additional state resolution? See #1760.
|
|
||||||
# However, fetching state doesn't hold the linearizer lock
|
|
||||||
# apparently.
|
|
||||||
#
|
|
||||||
# see https://github.com/matrix-org/synapse/pull/1744
|
|
||||||
|
|
||||||
missing_events = yield self.get_missing_events(
|
|
||||||
origin,
|
|
||||||
pdu.room_id,
|
|
||||||
earliest_events_ids=list(latest),
|
|
||||||
latest_events=[pdu],
|
|
||||||
limit=10,
|
|
||||||
min_depth=min_depth,
|
|
||||||
timeout=10000,
|
|
||||||
)
|
|
||||||
|
|
||||||
# We want to sort these by depth so we process them and
|
|
||||||
# tell clients about them in order.
|
|
||||||
missing_events.sort(key=lambda x: x.depth)
|
|
||||||
|
|
||||||
for e in missing_events:
|
|
||||||
yield self._handle_new_pdu(
|
|
||||||
origin,
|
|
||||||
e,
|
|
||||||
get_missing=False
|
|
||||||
)
|
|
||||||
|
|
||||||
have_seen = yield self.store.have_events(
|
|
||||||
[ev for ev, _ in pdu.prev_events]
|
|
||||||
)
|
|
||||||
|
|
||||||
prevs = {e_id for e_id, _ in pdu.prev_events}
|
prevs = {e_id for e_id, _ in pdu.prev_events}
|
||||||
seen = set(have_seen.keys())
|
seen = set(have_seen.keys())
|
||||||
if prevs - seen:
|
if prevs - seen:
|
||||||
logger.info(
|
|
||||||
"Still missing %d events for room %r: %r...",
|
|
||||||
len(prevs - seen), pdu.room_id, list(prevs - seen)[:5]
|
|
||||||
)
|
|
||||||
fetch_state = True
|
fetch_state = True
|
||||||
|
|
||||||
if fetch_state:
|
if fetch_state:
|
||||||
@@ -658,7 +526,7 @@ class FederationServer(FederationBase):
|
|||||||
origin, pdu.room_id, pdu.event_id,
|
origin, pdu.room_id, pdu.event_id,
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
logger.exception("Failed to get state for event: %s", pdu.event_id)
|
logger.warn("Failed to get state for event: %s", pdu.event_id)
|
||||||
|
|
||||||
yield self.handler.on_receive_pdu(
|
yield self.handler.on_receive_pdu(
|
||||||
origin,
|
origin,
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ a given transport.
|
|||||||
from .federation_client import FederationClient
|
from .federation_client import FederationClient
|
||||||
from .federation_server import FederationServer
|
from .federation_server import FederationServer
|
||||||
|
|
||||||
|
from .transaction_queue import TransactionQueue
|
||||||
|
|
||||||
from .persistence import TransactionActions
|
from .persistence import TransactionActions
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -64,10 +66,11 @@ class ReplicationLayer(FederationClient, FederationServer):
|
|||||||
self._clock = hs.get_clock()
|
self._clock = hs.get_clock()
|
||||||
|
|
||||||
self.transaction_actions = TransactionActions(self.store)
|
self.transaction_actions = TransactionActions(self.store)
|
||||||
|
self._transaction_queue = TransactionQueue(hs, transport_layer)
|
||||||
|
|
||||||
|
self._order = 0
|
||||||
|
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
|
|
||||||
super(ReplicationLayer, self).__init__(hs)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "<ReplicationLayer(%s)>" % self.server_name
|
return "<ReplicationLayer(%s)>" % self.server_name
|
||||||
|
|||||||
@@ -1,298 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014-2016 OpenMarket Ltd
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
"""A federation sender that forwards things to be sent across replication to
|
|
||||||
a worker process.
|
|
||||||
|
|
||||||
It assumes there is a single worker process feeding off of it.
|
|
||||||
|
|
||||||
Each row in the replication stream consists of a type and some json, where the
|
|
||||||
types indicate whether they are presence, or edus, etc.
|
|
||||||
|
|
||||||
Ephemeral or non-event data are queued up in-memory. When the worker requests
|
|
||||||
updates since a particular point, all in-memory data since before that point is
|
|
||||||
dropped. We also expire things in the queue after 5 minutes, to ensure that a
|
|
||||||
dead worker doesn't cause the queues to grow limitlessly.
|
|
||||||
|
|
||||||
Events are replicated via a separate events stream.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .units import Edu
|
|
||||||
|
|
||||||
from synapse.util.metrics import Measure
|
|
||||||
import synapse.metrics
|
|
||||||
|
|
||||||
from blist import sorteddict
|
|
||||||
import ujson
|
|
||||||
|
|
||||||
|
|
||||||
metrics = synapse.metrics.get_metrics_for(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
PRESENCE_TYPE = "p"
|
|
||||||
KEYED_EDU_TYPE = "k"
|
|
||||||
EDU_TYPE = "e"
|
|
||||||
FAILURE_TYPE = "f"
|
|
||||||
DEVICE_MESSAGE_TYPE = "d"
|
|
||||||
|
|
||||||
|
|
||||||
class FederationRemoteSendQueue(object):
|
|
||||||
"""A drop in replacement for TransactionQueue"""
|
|
||||||
|
|
||||||
def __init__(self, hs):
|
|
||||||
self.server_name = hs.hostname
|
|
||||||
self.clock = hs.get_clock()
|
|
||||||
|
|
||||||
self.presence_map = {}
|
|
||||||
self.presence_changed = sorteddict()
|
|
||||||
|
|
||||||
self.keyed_edu = {}
|
|
||||||
self.keyed_edu_changed = sorteddict()
|
|
||||||
|
|
||||||
self.edus = sorteddict()
|
|
||||||
|
|
||||||
self.failures = sorteddict()
|
|
||||||
|
|
||||||
self.device_messages = sorteddict()
|
|
||||||
|
|
||||||
self.pos = 1
|
|
||||||
self.pos_time = sorteddict()
|
|
||||||
|
|
||||||
# EVERYTHING IS SAD. In particular, python only makes new scopes when
|
|
||||||
# we make a new function, so we need to make a new function so the inner
|
|
||||||
# lambda binds to the queue rather than to the name of the queue which
|
|
||||||
# changes. ARGH.
|
|
||||||
def register(name, queue):
|
|
||||||
metrics.register_callback(
|
|
||||||
queue_name + "_size",
|
|
||||||
lambda: len(queue),
|
|
||||||
)
|
|
||||||
|
|
||||||
for queue_name in [
|
|
||||||
"presence_map", "presence_changed", "keyed_edu", "keyed_edu_changed",
|
|
||||||
"edus", "failures", "device_messages", "pos_time",
|
|
||||||
]:
|
|
||||||
register(queue_name, getattr(self, queue_name))
|
|
||||||
|
|
||||||
self.clock.looping_call(self._clear_queue, 30 * 1000)
|
|
||||||
|
|
||||||
def _next_pos(self):
|
|
||||||
pos = self.pos
|
|
||||||
self.pos += 1
|
|
||||||
self.pos_time[self.clock.time_msec()] = pos
|
|
||||||
return pos
|
|
||||||
|
|
||||||
def _clear_queue(self):
|
|
||||||
"""Clear the queues for anything older than N minutes"""
|
|
||||||
|
|
||||||
FIVE_MINUTES_AGO = 5 * 60 * 1000
|
|
||||||
now = self.clock.time_msec()
|
|
||||||
|
|
||||||
keys = self.pos_time.keys()
|
|
||||||
time = keys.bisect_left(now - FIVE_MINUTES_AGO)
|
|
||||||
if not keys[:time]:
|
|
||||||
return
|
|
||||||
|
|
||||||
position_to_delete = max(keys[:time])
|
|
||||||
for key in keys[:time]:
|
|
||||||
del self.pos_time[key]
|
|
||||||
|
|
||||||
self._clear_queue_before_pos(position_to_delete)
|
|
||||||
|
|
||||||
def _clear_queue_before_pos(self, position_to_delete):
|
|
||||||
"""Clear all the queues from before a given position"""
|
|
||||||
with Measure(self.clock, "send_queue._clear"):
|
|
||||||
# Delete things out of presence maps
|
|
||||||
keys = self.presence_changed.keys()
|
|
||||||
i = keys.bisect_left(position_to_delete)
|
|
||||||
for key in keys[:i]:
|
|
||||||
del self.presence_changed[key]
|
|
||||||
|
|
||||||
user_ids = set(
|
|
||||||
user_id for uids in self.presence_changed.values() for _, user_id in uids
|
|
||||||
)
|
|
||||||
|
|
||||||
to_del = [
|
|
||||||
user_id for user_id in self.presence_map if user_id not in user_ids
|
|
||||||
]
|
|
||||||
for user_id in to_del:
|
|
||||||
del self.presence_map[user_id]
|
|
||||||
|
|
||||||
# Delete things out of keyed edus
|
|
||||||
keys = self.keyed_edu_changed.keys()
|
|
||||||
i = keys.bisect_left(position_to_delete)
|
|
||||||
for key in keys[:i]:
|
|
||||||
del self.keyed_edu_changed[key]
|
|
||||||
|
|
||||||
live_keys = set()
|
|
||||||
for edu_key in self.keyed_edu_changed.values():
|
|
||||||
live_keys.add(edu_key)
|
|
||||||
|
|
||||||
to_del = [edu_key for edu_key in self.keyed_edu if edu_key not in live_keys]
|
|
||||||
for edu_key in to_del:
|
|
||||||
del self.keyed_edu[edu_key]
|
|
||||||
|
|
||||||
# Delete things out of edu map
|
|
||||||
keys = self.edus.keys()
|
|
||||||
i = keys.bisect_left(position_to_delete)
|
|
||||||
for key in keys[:i]:
|
|
||||||
del self.edus[key]
|
|
||||||
|
|
||||||
# Delete things out of failure map
|
|
||||||
keys = self.failures.keys()
|
|
||||||
i = keys.bisect_left(position_to_delete)
|
|
||||||
for key in keys[:i]:
|
|
||||||
del self.failures[key]
|
|
||||||
|
|
||||||
# Delete things out of device map
|
|
||||||
keys = self.device_messages.keys()
|
|
||||||
i = keys.bisect_left(position_to_delete)
|
|
||||||
for key in keys[:i]:
|
|
||||||
del self.device_messages[key]
|
|
||||||
|
|
||||||
def notify_new_events(self, current_id):
|
|
||||||
"""As per TransactionQueue"""
|
|
||||||
# We don't need to replicate this as it gets sent down a different
|
|
||||||
# stream.
|
|
||||||
pass
|
|
||||||
|
|
||||||
def send_edu(self, destination, edu_type, content, key=None):
|
|
||||||
"""As per TransactionQueue"""
|
|
||||||
pos = self._next_pos()
|
|
||||||
|
|
||||||
edu = Edu(
|
|
||||||
origin=self.server_name,
|
|
||||||
destination=destination,
|
|
||||||
edu_type=edu_type,
|
|
||||||
content=content,
|
|
||||||
)
|
|
||||||
|
|
||||||
if key:
|
|
||||||
assert isinstance(key, tuple)
|
|
||||||
self.keyed_edu[(destination, key)] = edu
|
|
||||||
self.keyed_edu_changed[pos] = (destination, key)
|
|
||||||
else:
|
|
||||||
self.edus[pos] = edu
|
|
||||||
|
|
||||||
def send_presence(self, destination, states):
|
|
||||||
"""As per TransactionQueue"""
|
|
||||||
pos = self._next_pos()
|
|
||||||
|
|
||||||
self.presence_map.update({
|
|
||||||
state.user_id: state
|
|
||||||
for state in states
|
|
||||||
})
|
|
||||||
|
|
||||||
self.presence_changed[pos] = [
|
|
||||||
(destination, state.user_id) for state in states
|
|
||||||
]
|
|
||||||
|
|
||||||
def send_failure(self, failure, destination):
|
|
||||||
"""As per TransactionQueue"""
|
|
||||||
pos = self._next_pos()
|
|
||||||
|
|
||||||
self.failures[pos] = (destination, str(failure))
|
|
||||||
|
|
||||||
def send_device_messages(self, destination):
|
|
||||||
"""As per TransactionQueue"""
|
|
||||||
pos = self._next_pos()
|
|
||||||
self.device_messages[pos] = destination
|
|
||||||
|
|
||||||
def get_current_token(self):
|
|
||||||
return self.pos - 1
|
|
||||||
|
|
||||||
def get_replication_rows(self, token, limit, federation_ack=None):
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
token (int)
|
|
||||||
limit (int)
|
|
||||||
federation_ack (int): Optional. The position where the worker is
|
|
||||||
explicitly acknowledged it has handled. Allows us to drop
|
|
||||||
data from before that point
|
|
||||||
"""
|
|
||||||
# TODO: Handle limit.
|
|
||||||
|
|
||||||
# To handle restarts where we wrap around
|
|
||||||
if token > self.pos:
|
|
||||||
token = -1
|
|
||||||
|
|
||||||
rows = []
|
|
||||||
|
|
||||||
# There should be only one reader, so lets delete everything its
|
|
||||||
# acknowledged its seen.
|
|
||||||
if federation_ack:
|
|
||||||
self._clear_queue_before_pos(federation_ack)
|
|
||||||
|
|
||||||
# Fetch changed presence
|
|
||||||
keys = self.presence_changed.keys()
|
|
||||||
i = keys.bisect_right(token)
|
|
||||||
dest_user_ids = set(
|
|
||||||
(pos, dest_user_id)
|
|
||||||
for pos in keys[i:]
|
|
||||||
for dest_user_id in self.presence_changed[pos]
|
|
||||||
)
|
|
||||||
|
|
||||||
for (key, (dest, user_id)) in dest_user_ids:
|
|
||||||
rows.append((key, PRESENCE_TYPE, ujson.dumps({
|
|
||||||
"destination": dest,
|
|
||||||
"state": self.presence_map[user_id].as_dict(),
|
|
||||||
})))
|
|
||||||
|
|
||||||
# Fetch changes keyed edus
|
|
||||||
keys = self.keyed_edu_changed.keys()
|
|
||||||
i = keys.bisect_right(token)
|
|
||||||
keyed_edus = set((k, self.keyed_edu_changed[k]) for k in keys[i:])
|
|
||||||
|
|
||||||
for (pos, (destination, edu_key)) in keyed_edus:
|
|
||||||
rows.append(
|
|
||||||
(pos, KEYED_EDU_TYPE, ujson.dumps({
|
|
||||||
"key": edu_key,
|
|
||||||
"edu": self.keyed_edu[(destination, edu_key)].get_internal_dict(),
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
# Fetch changed edus
|
|
||||||
keys = self.edus.keys()
|
|
||||||
i = keys.bisect_right(token)
|
|
||||||
edus = set((k, self.edus[k]) for k in keys[i:])
|
|
||||||
|
|
||||||
for (pos, edu) in edus:
|
|
||||||
rows.append((pos, EDU_TYPE, ujson.dumps(edu.get_internal_dict())))
|
|
||||||
|
|
||||||
# Fetch changed failures
|
|
||||||
keys = self.failures.keys()
|
|
||||||
i = keys.bisect_right(token)
|
|
||||||
failures = set((k, self.failures[k]) for k in keys[i:])
|
|
||||||
|
|
||||||
for (pos, (destination, failure)) in failures:
|
|
||||||
rows.append((pos, FAILURE_TYPE, ujson.dumps({
|
|
||||||
"destination": destination,
|
|
||||||
"failure": failure,
|
|
||||||
})))
|
|
||||||
|
|
||||||
# Fetch changed device messages
|
|
||||||
keys = self.device_messages.keys()
|
|
||||||
i = keys.bisect_right(token)
|
|
||||||
device_messages = set((k, self.device_messages[k]) for k in keys[i:])
|
|
||||||
|
|
||||||
for (pos, destination) in device_messages:
|
|
||||||
rows.append((pos, DEVICE_MESSAGE_TYPE, ujson.dumps({
|
|
||||||
"destination": destination,
|
|
||||||
})))
|
|
||||||
|
|
||||||
# Sort rows based on pos
|
|
||||||
rows.sort()
|
|
||||||
|
|
||||||
return rows
|
|
||||||
@@ -17,17 +17,14 @@
|
|||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from .persistence import TransactionActions
|
from .persistence import TransactionActions
|
||||||
from .units import Transaction, Edu
|
from .units import Transaction
|
||||||
|
|
||||||
from synapse.api.errors import HttpResponseException
|
from synapse.api.errors import HttpResponseException
|
||||||
from synapse.util.async import run_on_reactor
|
from synapse.util.logutils import log_function
|
||||||
from synapse.util.logcontext import preserve_context_over_fn
|
from synapse.util.logcontext import PreserveLoggingContext
|
||||||
from synapse.util.retryutils import (
|
from synapse.util.retryutils import (
|
||||||
get_retry_limiter, NotRetryingDestination,
|
get_retry_limiter, NotRetryingDestination,
|
||||||
)
|
)
|
||||||
from synapse.util.metrics import measure_func
|
|
||||||
from synapse.types import get_domain_from_id
|
|
||||||
from synapse.handlers.presence import format_user_presence_state
|
|
||||||
import synapse.metrics
|
import synapse.metrics
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -37,12 +34,6 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
metrics = synapse.metrics.get_metrics_for(__name__)
|
metrics = synapse.metrics.get_metrics_for(__name__)
|
||||||
|
|
||||||
client_metrics = synapse.metrics.get_metrics_for("synapse.federation.client")
|
|
||||||
sent_pdus_destination_dist = client_metrics.register_distribution(
|
|
||||||
"sent_pdu_destinations"
|
|
||||||
)
|
|
||||||
sent_edus_counter = client_metrics.register_counter("sent_edus")
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionQueue(object):
|
class TransactionQueue(object):
|
||||||
"""This class makes sure we only have one transaction in flight at
|
"""This class makes sure we only have one transaction in flight at
|
||||||
@@ -51,17 +42,15 @@ class TransactionQueue(object):
|
|||||||
It batches pending PDUs into single transactions.
|
It batches pending PDUs into single transactions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs, transport_layer):
|
||||||
self.server_name = hs.hostname
|
self.server_name = hs.hostname
|
||||||
|
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
self.state = hs.get_state_handler()
|
|
||||||
self.transaction_actions = TransactionActions(self.store)
|
self.transaction_actions = TransactionActions(self.store)
|
||||||
|
|
||||||
self.transport_layer = hs.get_federation_transport_client()
|
self.transport_layer = transport_layer
|
||||||
|
|
||||||
self.clock = hs.get_clock()
|
self._clock = hs.get_clock()
|
||||||
self.is_mine_id = hs.is_mine_id
|
|
||||||
|
|
||||||
# Is a mapping from destinations -> deferreds. Used to keep track
|
# Is a mapping from destinations -> deferreds. Used to keep track
|
||||||
# of which destinations have transactions in flight and when they are
|
# of which destinations have transactions in flight and when they are
|
||||||
@@ -79,36 +68,20 @@ class TransactionQueue(object):
|
|||||||
# destination -> list of tuple(edu, deferred)
|
# destination -> list of tuple(edu, deferred)
|
||||||
self.pending_edus_by_dest = edus = {}
|
self.pending_edus_by_dest = edus = {}
|
||||||
|
|
||||||
# Presence needs to be separate as we send single aggragate EDUs
|
|
||||||
self.pending_presence_by_dest = presence = {}
|
|
||||||
self.pending_edus_keyed_by_dest = edus_keyed = {}
|
|
||||||
|
|
||||||
metrics.register_callback(
|
metrics.register_callback(
|
||||||
"pending_pdus",
|
"pending_pdus",
|
||||||
lambda: sum(map(len, pdus.values())),
|
lambda: sum(map(len, pdus.values())),
|
||||||
)
|
)
|
||||||
metrics.register_callback(
|
metrics.register_callback(
|
||||||
"pending_edus",
|
"pending_edus",
|
||||||
lambda: (
|
lambda: sum(map(len, edus.values())),
|
||||||
sum(map(len, edus.values()))
|
|
||||||
+ sum(map(len, presence.values()))
|
|
||||||
+ sum(map(len, edus_keyed.values()))
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# destination -> list of tuple(failure, deferred)
|
# destination -> list of tuple(failure, deferred)
|
||||||
self.pending_failures_by_dest = {}
|
self.pending_failures_by_dest = {}
|
||||||
|
|
||||||
self.last_device_stream_id_by_dest = {}
|
|
||||||
self.last_device_list_stream_id_by_dest = {}
|
|
||||||
|
|
||||||
# HACK to get unique tx id
|
# HACK to get unique tx id
|
||||||
self._next_txn_id = int(self.clock.time_msec())
|
self._next_txn_id = int(self._clock.time_msec())
|
||||||
|
|
||||||
self._order = 1
|
|
||||||
|
|
||||||
self._is_processing = False
|
|
||||||
self._last_poked_id = -1
|
|
||||||
|
|
||||||
def can_send_to(self, destination):
|
def can_send_to(self, destination):
|
||||||
"""Can we send messages to the given server?
|
"""Can we send messages to the given server?
|
||||||
@@ -130,76 +103,11 @@ class TransactionQueue(object):
|
|||||||
else:
|
else:
|
||||||
return not destination.startswith("localhost")
|
return not destination.startswith("localhost")
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
def enqueue_pdu(self, pdu, destinations, order):
|
||||||
def notify_new_events(self, current_id):
|
|
||||||
"""This gets called when we have some new events we might want to
|
|
||||||
send out to other servers.
|
|
||||||
"""
|
|
||||||
self._last_poked_id = max(current_id, self._last_poked_id)
|
|
||||||
|
|
||||||
if self._is_processing:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._is_processing = True
|
|
||||||
while True:
|
|
||||||
last_token = yield self.store.get_federation_out_pos("events")
|
|
||||||
next_token, events = yield self.store.get_all_new_events_stream(
|
|
||||||
last_token, self._last_poked_id, limit=20,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug("Handling %s -> %s", last_token, next_token)
|
|
||||||
|
|
||||||
if not events and next_token >= self._last_poked_id:
|
|
||||||
break
|
|
||||||
|
|
||||||
for event in events:
|
|
||||||
# Only send events for this server.
|
|
||||||
send_on_behalf_of = event.internal_metadata.get_send_on_behalf_of()
|
|
||||||
is_mine = self.is_mine_id(event.event_id)
|
|
||||||
if not is_mine and send_on_behalf_of is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Get the state from before the event.
|
|
||||||
# We need to make sure that this is the state from before
|
|
||||||
# the event and not from after it.
|
|
||||||
# Otherwise if the last member on a server in a room is
|
|
||||||
# banned then it won't receive the event because it won't
|
|
||||||
# be in the room after the ban.
|
|
||||||
users_in_room = yield self.state.get_current_user_in_room(
|
|
||||||
event.room_id, latest_event_ids=[
|
|
||||||
prev_id for prev_id, _ in event.prev_events
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
destinations = set(
|
|
||||||
get_domain_from_id(user_id) for user_id in users_in_room
|
|
||||||
)
|
|
||||||
if send_on_behalf_of is not None:
|
|
||||||
# If we are sending the event on behalf of another server
|
|
||||||
# then it already has the event and there is no reason to
|
|
||||||
# send the event to it.
|
|
||||||
destinations.discard(send_on_behalf_of)
|
|
||||||
|
|
||||||
logger.debug("Sending %s to %r", event, destinations)
|
|
||||||
|
|
||||||
self._send_pdu(event, destinations)
|
|
||||||
|
|
||||||
yield self.store.update_federation_out_pos(
|
|
||||||
"events", next_token
|
|
||||||
)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
self._is_processing = False
|
|
||||||
|
|
||||||
def _send_pdu(self, pdu, destinations):
|
|
||||||
# We loop through all destinations to see whether we already have
|
# We loop through all destinations to see whether we already have
|
||||||
# a transaction in progress. If we do, stick it in the pending_pdus
|
# a transaction in progress. If we do, stick it in the pending_pdus
|
||||||
# table and we'll get back to it later.
|
# table and we'll get back to it later.
|
||||||
|
|
||||||
order = self._order
|
|
||||||
self._order += 1
|
|
||||||
|
|
||||||
destinations = set(destinations)
|
destinations = set(destinations)
|
||||||
destinations = set(
|
destinations = set(
|
||||||
dest for dest in destinations if self.can_send_to(dest)
|
dest for dest in destinations if self.can_send_to(dest)
|
||||||
@@ -210,83 +118,86 @@ class TransactionQueue(object):
|
|||||||
if not destinations:
|
if not destinations:
|
||||||
return
|
return
|
||||||
|
|
||||||
sent_pdus_destination_dist.inc_by(len(destinations))
|
deferreds = []
|
||||||
|
|
||||||
for destination in destinations:
|
for destination in destinations:
|
||||||
|
deferred = defer.Deferred()
|
||||||
self.pending_pdus_by_dest.setdefault(destination, []).append(
|
self.pending_pdus_by_dest.setdefault(destination, []).append(
|
||||||
(pdu, order)
|
(pdu, deferred, order)
|
||||||
)
|
)
|
||||||
|
|
||||||
preserve_context_over_fn(
|
def chain(failure):
|
||||||
self._attempt_new_transaction, destination
|
if not deferred.called:
|
||||||
)
|
deferred.errback(failure)
|
||||||
|
|
||||||
def send_presence(self, destination, states):
|
def log_failure(f):
|
||||||
if not self.can_send_to(destination):
|
logger.warn("Failed to send pdu to %s: %s", destination, f.value)
|
||||||
return
|
|
||||||
|
|
||||||
self.pending_presence_by_dest.setdefault(destination, {}).update({
|
deferred.addErrback(log_failure)
|
||||||
state.user_id: state for state in states
|
|
||||||
})
|
|
||||||
|
|
||||||
preserve_context_over_fn(
|
with PreserveLoggingContext():
|
||||||
self._attempt_new_transaction, destination
|
self._attempt_new_transaction(destination).addErrback(chain)
|
||||||
)
|
|
||||||
|
|
||||||
def send_edu(self, destination, edu_type, content, key=None):
|
deferreds.append(deferred)
|
||||||
edu = Edu(
|
|
||||||
origin=self.server_name,
|
# NO inlineCallbacks
|
||||||
destination=destination,
|
def enqueue_edu(self, edu):
|
||||||
edu_type=edu_type,
|
destination = edu.destination
|
||||||
content=content,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.can_send_to(destination):
|
if not self.can_send_to(destination):
|
||||||
return
|
return
|
||||||
|
|
||||||
sent_edus_counter.inc()
|
deferred = defer.Deferred()
|
||||||
|
self.pending_edus_by_dest.setdefault(destination, []).append(
|
||||||
if key:
|
(edu, deferred)
|
||||||
self.pending_edus_keyed_by_dest.setdefault(
|
|
||||||
destination, {}
|
|
||||||
)[(edu.edu_type, key)] = edu
|
|
||||||
else:
|
|
||||||
self.pending_edus_by_dest.setdefault(destination, []).append(edu)
|
|
||||||
|
|
||||||
preserve_context_over_fn(
|
|
||||||
self._attempt_new_transaction, destination
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def send_failure(self, failure, destination):
|
def chain(failure):
|
||||||
|
if not deferred.called:
|
||||||
|
deferred.errback(failure)
|
||||||
|
|
||||||
|
def log_failure(f):
|
||||||
|
logger.warn("Failed to send edu to %s: %s", destination, f.value)
|
||||||
|
|
||||||
|
deferred.addErrback(log_failure)
|
||||||
|
|
||||||
|
with PreserveLoggingContext():
|
||||||
|
self._attempt_new_transaction(destination).addErrback(chain)
|
||||||
|
|
||||||
|
return deferred
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def enqueue_failure(self, failure, destination):
|
||||||
if destination == self.server_name or destination == "localhost":
|
if destination == self.server_name or destination == "localhost":
|
||||||
return
|
return
|
||||||
|
|
||||||
|
deferred = defer.Deferred()
|
||||||
|
|
||||||
if not self.can_send_to(destination):
|
if not self.can_send_to(destination):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.pending_failures_by_dest.setdefault(
|
self.pending_failures_by_dest.setdefault(
|
||||||
destination, []
|
destination, []
|
||||||
).append(failure)
|
).append(
|
||||||
|
(failure, deferred)
|
||||||
preserve_context_over_fn(
|
|
||||||
self._attempt_new_transaction, destination
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def send_device_messages(self, destination):
|
def chain(f):
|
||||||
if destination == self.server_name or destination == "localhost":
|
if not deferred.called:
|
||||||
return
|
deferred.errback(f)
|
||||||
|
|
||||||
if not self.can_send_to(destination):
|
def log_failure(f):
|
||||||
return
|
logger.warn("Failed to send failure to %s: %s", destination, f.value)
|
||||||
|
|
||||||
preserve_context_over_fn(
|
deferred.addErrback(log_failure)
|
||||||
self._attempt_new_transaction, destination
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_current_token(self):
|
with PreserveLoggingContext():
|
||||||
return 0
|
self._attempt_new_transaction(destination).addErrback(chain)
|
||||||
|
|
||||||
|
yield deferred
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
def _attempt_new_transaction(self, destination):
|
def _attempt_new_transaction(self, destination):
|
||||||
# list of (pending_pdu, deferred, order)
|
# list of (pending_pdu, deferred, order)
|
||||||
if destination in self.pending_transactions:
|
if destination in self.pending_transactions:
|
||||||
@@ -300,154 +211,55 @@ class TransactionQueue(object):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
pending_pdus = self.pending_pdus_by_dest.pop(destination, [])
|
||||||
|
pending_edus = self.pending_edus_by_dest.pop(destination, [])
|
||||||
|
pending_failures = self.pending_failures_by_dest.pop(destination, [])
|
||||||
|
|
||||||
|
if pending_pdus:
|
||||||
|
logger.debug("TX [%s] len(pending_pdus_by_dest[dest]) = %d",
|
||||||
|
destination, len(pending_pdus))
|
||||||
|
|
||||||
|
if not pending_pdus and not pending_edus and not pending_failures:
|
||||||
|
logger.debug("TX [%s] Nothing to send", destination)
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.pending_transactions[destination] = 1
|
self.pending_transactions[destination] = 1
|
||||||
|
|
||||||
yield run_on_reactor()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
pending_pdus = self.pending_pdus_by_dest.pop(destination, [])
|
|
||||||
pending_edus = self.pending_edus_by_dest.pop(destination, [])
|
|
||||||
pending_presence = self.pending_presence_by_dest.pop(destination, {})
|
|
||||||
pending_failures = self.pending_failures_by_dest.pop(destination, [])
|
|
||||||
|
|
||||||
pending_edus.extend(
|
|
||||||
self.pending_edus_keyed_by_dest.pop(destination, {}).values()
|
|
||||||
)
|
|
||||||
|
|
||||||
limiter = yield get_retry_limiter(
|
|
||||||
destination,
|
|
||||||
self.clock,
|
|
||||||
self.store,
|
|
||||||
backoff_on_404=True, # If we get a 404 the other side has gone
|
|
||||||
)
|
|
||||||
|
|
||||||
device_message_edus, device_stream_id, dev_list_id = (
|
|
||||||
yield self._get_new_device_messages(destination)
|
|
||||||
)
|
|
||||||
|
|
||||||
pending_edus.extend(device_message_edus)
|
|
||||||
if pending_presence:
|
|
||||||
pending_edus.append(
|
|
||||||
Edu(
|
|
||||||
origin=self.server_name,
|
|
||||||
destination=destination,
|
|
||||||
edu_type="m.presence",
|
|
||||||
content={
|
|
||||||
"push": [
|
|
||||||
format_user_presence_state(
|
|
||||||
presence, self.clock.time_msec()
|
|
||||||
)
|
|
||||||
for presence in pending_presence.values()
|
|
||||||
]
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if pending_pdus:
|
|
||||||
logger.debug("TX [%s] len(pending_pdus_by_dest[dest]) = %d",
|
|
||||||
destination, len(pending_pdus))
|
|
||||||
|
|
||||||
if not pending_pdus and not pending_edus and not pending_failures:
|
|
||||||
logger.debug("TX [%s] Nothing to send", destination)
|
|
||||||
self.last_device_stream_id_by_dest[destination] = (
|
|
||||||
device_stream_id
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
success = yield self._send_new_transaction(
|
|
||||||
destination, pending_pdus, pending_edus, pending_failures,
|
|
||||||
limiter=limiter,
|
|
||||||
)
|
|
||||||
if success:
|
|
||||||
# Remove the acknowledged device messages from the database
|
|
||||||
# Only bother if we actually sent some device messages
|
|
||||||
if device_message_edus:
|
|
||||||
yield self.store.delete_device_msgs_for_remote(
|
|
||||||
destination, device_stream_id
|
|
||||||
)
|
|
||||||
logger.info("Marking as sent %r %r", destination, dev_list_id)
|
|
||||||
yield self.store.mark_as_sent_devices_by_remote(
|
|
||||||
destination, dev_list_id
|
|
||||||
)
|
|
||||||
|
|
||||||
self.last_device_stream_id_by_dest[destination] = device_stream_id
|
|
||||||
self.last_device_list_stream_id_by_dest[destination] = dev_list_id
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
except NotRetryingDestination:
|
|
||||||
logger.debug(
|
|
||||||
"TX [%s] not ready for retry yet - "
|
|
||||||
"dropping transaction for now",
|
|
||||||
destination,
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
# We want to be *very* sure we delete this after we stop processing
|
|
||||||
self.pending_transactions.pop(destination, None)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _get_new_device_messages(self, destination):
|
|
||||||
last_device_stream_id = self.last_device_stream_id_by_dest.get(destination, 0)
|
|
||||||
to_device_stream_id = self.store.get_to_device_stream_token()
|
|
||||||
contents, stream_id = yield self.store.get_new_device_msgs_for_remote(
|
|
||||||
destination, last_device_stream_id, to_device_stream_id
|
|
||||||
)
|
|
||||||
edus = [
|
|
||||||
Edu(
|
|
||||||
origin=self.server_name,
|
|
||||||
destination=destination,
|
|
||||||
edu_type="m.direct_to_device",
|
|
||||||
content=content,
|
|
||||||
)
|
|
||||||
for content in contents
|
|
||||||
]
|
|
||||||
|
|
||||||
last_device_list = self.last_device_list_stream_id_by_dest.get(destination, 0)
|
|
||||||
now_stream_id, results = yield self.store.get_devices_by_remote(
|
|
||||||
destination, last_device_list
|
|
||||||
)
|
|
||||||
edus.extend(
|
|
||||||
Edu(
|
|
||||||
origin=self.server_name,
|
|
||||||
destination=destination,
|
|
||||||
edu_type="m.device_list_update",
|
|
||||||
content=content,
|
|
||||||
)
|
|
||||||
for content in results
|
|
||||||
)
|
|
||||||
defer.returnValue((edus, stream_id, now_stream_id))
|
|
||||||
|
|
||||||
@measure_func("_send_new_transaction")
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _send_new_transaction(self, destination, pending_pdus, pending_edus,
|
|
||||||
pending_failures, limiter):
|
|
||||||
|
|
||||||
# Sort based on the order field
|
|
||||||
pending_pdus.sort(key=lambda t: t[1])
|
|
||||||
pdus = [x[0] for x in pending_pdus]
|
|
||||||
edus = pending_edus
|
|
||||||
failures = [x.get_dict() for x in pending_failures]
|
|
||||||
|
|
||||||
success = True
|
|
||||||
|
|
||||||
try:
|
|
||||||
logger.debug("TX [%s] _attempt_new_transaction", destination)
|
logger.debug("TX [%s] _attempt_new_transaction", destination)
|
||||||
|
|
||||||
|
# Sort based on the order field
|
||||||
|
pending_pdus.sort(key=lambda t: t[2])
|
||||||
|
|
||||||
|
pdus = [x[0] for x in pending_pdus]
|
||||||
|
edus = [x[0] for x in pending_edus]
|
||||||
|
failures = [x[0].get_dict() for x in pending_failures]
|
||||||
|
deferreds = [
|
||||||
|
x[1]
|
||||||
|
for x in pending_pdus + pending_edus + pending_failures
|
||||||
|
]
|
||||||
|
|
||||||
txn_id = str(self._next_txn_id)
|
txn_id = str(self._next_txn_id)
|
||||||
|
|
||||||
|
limiter = yield get_retry_limiter(
|
||||||
|
destination,
|
||||||
|
self._clock,
|
||||||
|
self.store,
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"TX [%s] {%s} Attempting new transaction"
|
"TX [%s] {%s} Attempting new transaction"
|
||||||
" (pdus: %d, edus: %d, failures: %d)",
|
" (pdus: %d, edus: %d, failures: %d)",
|
||||||
destination, txn_id,
|
destination, txn_id,
|
||||||
len(pdus),
|
len(pending_pdus),
|
||||||
len(edus),
|
len(pending_edus),
|
||||||
len(failures)
|
len(pending_failures)
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug("TX [%s] Persisting transaction...", destination)
|
logger.debug("TX [%s] Persisting transaction...", destination)
|
||||||
|
|
||||||
transaction = Transaction.create_new(
|
transaction = Transaction.create_new(
|
||||||
origin_server_ts=int(self.clock.time_msec()),
|
origin_server_ts=int(self._clock.time_msec()),
|
||||||
transaction_id=txn_id,
|
transaction_id=txn_id,
|
||||||
origin=self.server_name,
|
origin=self.server_name,
|
||||||
destination=destination,
|
destination=destination,
|
||||||
@@ -466,9 +278,9 @@ class TransactionQueue(object):
|
|||||||
" (PDUs: %d, EDUs: %d, failures: %d)",
|
" (PDUs: %d, EDUs: %d, failures: %d)",
|
||||||
destination, txn_id,
|
destination, txn_id,
|
||||||
transaction.transaction_id,
|
transaction.transaction_id,
|
||||||
len(pdus),
|
len(pending_pdus),
|
||||||
len(edus),
|
len(pending_edus),
|
||||||
len(failures),
|
len(pending_failures),
|
||||||
)
|
)
|
||||||
|
|
||||||
with limiter:
|
with limiter:
|
||||||
@@ -478,7 +290,7 @@ class TransactionQueue(object):
|
|||||||
# keys work
|
# keys work
|
||||||
def json_data_cb():
|
def json_data_cb():
|
||||||
data = transaction.get_dict()
|
data = transaction.get_dict()
|
||||||
now = int(self.clock.time_msec())
|
now = int(self._clock.time_msec())
|
||||||
if "pdus" in data:
|
if "pdus" in data:
|
||||||
for p in data["pdus"]:
|
for p in data["pdus"]:
|
||||||
if "age_ts" in p:
|
if "age_ts" in p:
|
||||||
@@ -504,13 +316,6 @@ class TransactionQueue(object):
|
|||||||
code = e.code
|
code = e.code
|
||||||
response = e.response
|
response = e.response
|
||||||
|
|
||||||
if e.code in (401, 404, 429) or 500 <= e.code:
|
|
||||||
logger.info(
|
|
||||||
"TX [%s] {%s} got %d response",
|
|
||||||
destination, txn_id, code
|
|
||||||
)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"TX [%s] {%s} got %d response",
|
"TX [%s] {%s} got %d response",
|
||||||
destination, txn_id, code
|
destination, txn_id, code
|
||||||
@@ -525,12 +330,28 @@ class TransactionQueue(object):
|
|||||||
|
|
||||||
logger.debug("TX [%s] Marked as delivered", destination)
|
logger.debug("TX [%s] Marked as delivered", destination)
|
||||||
|
|
||||||
if code != 200:
|
logger.debug("TX [%s] Yielding to callbacks...", destination)
|
||||||
for p in pdus:
|
|
||||||
logger.info(
|
for deferred in deferreds:
|
||||||
"Failed to send event %s to %s", p.event_id, destination
|
if code == 200:
|
||||||
)
|
deferred.callback(None)
|
||||||
success = False
|
else:
|
||||||
|
deferred.errback(RuntimeError("Got status %d" % code))
|
||||||
|
|
||||||
|
# Ensures we don't continue until all callbacks on that
|
||||||
|
# deferred have fired
|
||||||
|
try:
|
||||||
|
yield deferred
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.debug("TX [%s] Yielded to callbacks", destination)
|
||||||
|
except NotRetryingDestination:
|
||||||
|
logger.info(
|
||||||
|
"TX [%s] not ready for retry yet - "
|
||||||
|
"dropping transaction for now",
|
||||||
|
destination,
|
||||||
|
)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
# We capture this here as there as nothing actually listens
|
# We capture this here as there as nothing actually listens
|
||||||
# for this finishing functions deferred.
|
# for this finishing functions deferred.
|
||||||
@@ -539,11 +360,6 @@ class TransactionQueue(object):
|
|||||||
destination,
|
destination,
|
||||||
e,
|
e,
|
||||||
)
|
)
|
||||||
|
|
||||||
success = False
|
|
||||||
|
|
||||||
for p in pdus:
|
|
||||||
logger.info("Failed to send event %s to %s", p.event_id, destination)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# We capture this here as there as nothing actually listens
|
# We capture this here as there as nothing actually listens
|
||||||
# for this finishing functions deferred.
|
# for this finishing functions deferred.
|
||||||
@@ -553,9 +369,13 @@ class TransactionQueue(object):
|
|||||||
e,
|
e,
|
||||||
)
|
)
|
||||||
|
|
||||||
success = False
|
for deferred in deferreds:
|
||||||
|
if not deferred.called:
|
||||||
|
deferred.errback(e)
|
||||||
|
|
||||||
for p in pdus:
|
finally:
|
||||||
logger.info("Failed to send event %s to %s", p.event_id, destination)
|
# We want to be *very* sure we delete this after we stop processing
|
||||||
|
self.pending_transactions.pop(destination, None)
|
||||||
|
|
||||||
defer.returnValue(success)
|
# Check to see if there is anything else to send.
|
||||||
|
self._attempt_new_transaction(destination)
|
||||||
|
|||||||
@@ -54,28 +54,6 @@ class TransportLayerClient(object):
|
|||||||
destination, path=path, args={"event_id": event_id},
|
destination, path=path, args={"event_id": event_id},
|
||||||
)
|
)
|
||||||
|
|
||||||
@log_function
|
|
||||||
def get_room_state_ids(self, destination, room_id, event_id):
|
|
||||||
""" Requests all state for a given room from the given server at the
|
|
||||||
given event. Returns the state's event_id's
|
|
||||||
|
|
||||||
Args:
|
|
||||||
destination (str): The host name of the remote home server we want
|
|
||||||
to get the state from.
|
|
||||||
context (str): The name of the context we want the state of
|
|
||||||
event_id (str): The event we want the context at.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Deferred: Results in a dict received from the remote homeserver.
|
|
||||||
"""
|
|
||||||
logger.debug("get_room_state_ids dest=%s, room=%s",
|
|
||||||
destination, room_id)
|
|
||||||
|
|
||||||
path = PREFIX + "/state_ids/%s/" % room_id
|
|
||||||
return self.client.get_json(
|
|
||||||
destination, path=path, args={"event_id": event_id},
|
|
||||||
)
|
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def get_event(self, destination, event_id, timeout=None):
|
def get_event(self, destination, event_id, timeout=None):
|
||||||
""" Requests the pdu with give id and origin from the given server.
|
""" Requests the pdu with give id and origin from the given server.
|
||||||
@@ -201,8 +179,7 @@ class TransportLayerClient(object):
|
|||||||
content = yield self.client.get_json(
|
content = yield self.client.get_json(
|
||||||
destination=destination,
|
destination=destination,
|
||||||
path=path,
|
path=path,
|
||||||
retry_on_dns_fail=False,
|
retry_on_dns_fail=True,
|
||||||
timeout=20000,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
defer.returnValue(content)
|
defer.returnValue(content)
|
||||||
@@ -246,33 +223,6 @@ class TransportLayerClient(object):
|
|||||||
|
|
||||||
defer.returnValue(response)
|
defer.returnValue(response)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
@log_function
|
|
||||||
def get_public_rooms(self, remote_server, limit, since_token,
|
|
||||||
search_filter=None, include_all_networks=False,
|
|
||||||
third_party_instance_id=None):
|
|
||||||
path = PREFIX + "/publicRooms"
|
|
||||||
|
|
||||||
args = {
|
|
||||||
"include_all_networks": "true" if include_all_networks else "false",
|
|
||||||
}
|
|
||||||
if third_party_instance_id:
|
|
||||||
args["third_party_instance_id"] = third_party_instance_id,
|
|
||||||
if limit:
|
|
||||||
args["limit"] = [str(limit)]
|
|
||||||
if since_token:
|
|
||||||
args["since"] = [since_token]
|
|
||||||
|
|
||||||
# TODO(erikj): Actually send the search_filter across federation.
|
|
||||||
|
|
||||||
response = yield self.client.get_json(
|
|
||||||
destination=remote_server,
|
|
||||||
path=path,
|
|
||||||
args=args,
|
|
||||||
)
|
|
||||||
|
|
||||||
defer.returnValue(response)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def exchange_third_party_invite(self, destination, room_id, event_dict):
|
def exchange_third_party_invite(self, destination, room_id, event_dict):
|
||||||
@@ -313,7 +263,7 @@ class TransportLayerClient(object):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def query_client_keys(self, destination, query_content, timeout):
|
def query_client_keys(self, destination, query_content):
|
||||||
"""Query the device keys for a list of user ids hosted on a remote
|
"""Query the device keys for a list of user ids hosted on a remote
|
||||||
server.
|
server.
|
||||||
|
|
||||||
@@ -342,39 +292,12 @@ class TransportLayerClient(object):
|
|||||||
destination=destination,
|
destination=destination,
|
||||||
path=path,
|
path=path,
|
||||||
data=query_content,
|
data=query_content,
|
||||||
timeout=timeout,
|
|
||||||
)
|
)
|
||||||
defer.returnValue(content)
|
defer.returnValue(content)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def query_user_devices(self, destination, user_id, timeout):
|
def claim_client_keys(self, destination, query_content):
|
||||||
"""Query the devices for a user id hosted on a remote server.
|
|
||||||
|
|
||||||
Response:
|
|
||||||
{
|
|
||||||
"stream_id": "...",
|
|
||||||
"devices": [ { ... } ]
|
|
||||||
}
|
|
||||||
|
|
||||||
Args:
|
|
||||||
destination(str): The server to query.
|
|
||||||
query_content(dict): The user ids to query.
|
|
||||||
Returns:
|
|
||||||
A dict containg the device keys.
|
|
||||||
"""
|
|
||||||
path = PREFIX + "/user/devices/" + user_id
|
|
||||||
|
|
||||||
content = yield self.client.get_json(
|
|
||||||
destination=destination,
|
|
||||||
path=path,
|
|
||||||
timeout=timeout,
|
|
||||||
)
|
|
||||||
defer.returnValue(content)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
@log_function
|
|
||||||
def claim_client_keys(self, destination, query_content, timeout):
|
|
||||||
"""Claim one-time keys for a list of devices hosted on a remote server.
|
"""Claim one-time keys for a list of devices hosted on a remote server.
|
||||||
|
|
||||||
Request:
|
Request:
|
||||||
@@ -405,14 +328,13 @@ class TransportLayerClient(object):
|
|||||||
destination=destination,
|
destination=destination,
|
||||||
path=path,
|
path=path,
|
||||||
data=query_content,
|
data=query_content,
|
||||||
timeout=timeout,
|
|
||||||
)
|
)
|
||||||
defer.returnValue(content)
|
defer.returnValue(content)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def get_missing_events(self, destination, room_id, earliest_events,
|
def get_missing_events(self, destination, room_id, earliest_events,
|
||||||
latest_events, limit, min_depth, timeout):
|
latest_events, limit, min_depth):
|
||||||
path = PREFIX + "/get_missing_events/%s" % (room_id,)
|
path = PREFIX + "/get_missing_events/%s" % (room_id,)
|
||||||
|
|
||||||
content = yield self.client.post_json(
|
content = yield self.client.post_json(
|
||||||
@@ -423,8 +345,7 @@ class TransportLayerClient(object):
|
|||||||
"min_depth": int(min_depth),
|
"min_depth": int(min_depth),
|
||||||
"earliest_events": earliest_events,
|
"earliest_events": earliest_events,
|
||||||
"latest_events": latest_events,
|
"latest_events": latest_events,
|
||||||
},
|
}
|
||||||
timeout=timeout,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
defer.returnValue(content)
|
defer.returnValue(content)
|
||||||
|
|||||||
@@ -18,18 +18,13 @@ from twisted.internet import defer
|
|||||||
from synapse.api.urls import FEDERATION_PREFIX as PREFIX
|
from synapse.api.urls import FEDERATION_PREFIX as PREFIX
|
||||||
from synapse.api.errors import Codes, SynapseError
|
from synapse.api.errors import Codes, SynapseError
|
||||||
from synapse.http.server import JsonResource
|
from synapse.http.server import JsonResource
|
||||||
from synapse.http.servlet import (
|
from synapse.http.servlet import parse_json_object_from_request
|
||||||
parse_json_object_from_request, parse_integer_from_args, parse_string_from_args,
|
|
||||||
parse_boolean_from_args,
|
|
||||||
)
|
|
||||||
from synapse.util.ratelimitutils import FederationRateLimiter
|
from synapse.util.ratelimitutils import FederationRateLimiter
|
||||||
from synapse.util.versionstring import get_version_string
|
|
||||||
from synapse.types import ThirdPartyInstanceID
|
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
|
import simplejson as json
|
||||||
import re
|
import re
|
||||||
import synapse
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -42,7 +37,7 @@ class TransportLayerServer(JsonResource):
|
|||||||
self.hs = hs
|
self.hs = hs
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
|
|
||||||
super(TransportLayerServer, self).__init__(hs, canonical_json=False)
|
super(TransportLayerServer, self).__init__(hs)
|
||||||
|
|
||||||
self.authenticator = Authenticator(hs)
|
self.authenticator = Authenticator(hs)
|
||||||
self.ratelimiter = FederationRateLimiter(
|
self.ratelimiter = FederationRateLimiter(
|
||||||
@@ -65,16 +60,6 @@ class TransportLayerServer(JsonResource):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationError(SynapseError):
|
|
||||||
"""There was a problem authenticating the request"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NoAuthenticationError(AuthenticationError):
|
|
||||||
"""The request had no authentication information"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Authenticator(object):
|
class Authenticator(object):
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
self.keyring = hs.get_keyring()
|
self.keyring = hs.get_keyring()
|
||||||
@@ -82,7 +67,7 @@ class Authenticator(object):
|
|||||||
|
|
||||||
# A method just so we can pass 'self' as the authenticator to the Servlets
|
# A method just so we can pass 'self' as the authenticator to the Servlets
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def authenticate_request(self, request, content):
|
def authenticate_request(self, request):
|
||||||
json_request = {
|
json_request = {
|
||||||
"method": request.method,
|
"method": request.method,
|
||||||
"uri": request.uri,
|
"uri": request.uri,
|
||||||
@@ -90,11 +75,18 @@ class Authenticator(object):
|
|||||||
"signatures": {},
|
"signatures": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
if content is not None:
|
content = None
|
||||||
json_request["content"] = content
|
|
||||||
|
|
||||||
origin = None
|
origin = None
|
||||||
|
|
||||||
|
if request.method in ["PUT", "POST"]:
|
||||||
|
# TODO: Handle other method types? other content types?
|
||||||
|
try:
|
||||||
|
content_bytes = request.content.read()
|
||||||
|
content = json.loads(content_bytes)
|
||||||
|
json_request["content"] = content
|
||||||
|
except:
|
||||||
|
raise SynapseError(400, "Unable to parse JSON", Codes.BAD_JSON)
|
||||||
|
|
||||||
def parse_auth_header(header_str):
|
def parse_auth_header(header_str):
|
||||||
try:
|
try:
|
||||||
params = auth.split(" ")[1].split(",")
|
params = auth.split(" ")[1].split(",")
|
||||||
@@ -111,14 +103,14 @@ class Authenticator(object):
|
|||||||
sig = strip_quotes(param_dict["sig"])
|
sig = strip_quotes(param_dict["sig"])
|
||||||
return (origin, key, sig)
|
return (origin, key, sig)
|
||||||
except:
|
except:
|
||||||
raise AuthenticationError(
|
raise SynapseError(
|
||||||
400, "Malformed Authorization header", Codes.UNAUTHORIZED
|
400, "Malformed Authorization header", Codes.UNAUTHORIZED
|
||||||
)
|
)
|
||||||
|
|
||||||
auth_headers = request.requestHeaders.getRawHeaders(b"Authorization")
|
auth_headers = request.requestHeaders.getRawHeaders(b"Authorization")
|
||||||
|
|
||||||
if not auth_headers:
|
if not auth_headers:
|
||||||
raise NoAuthenticationError(
|
raise SynapseError(
|
||||||
401, "Missing Authorization headers", Codes.UNAUTHORIZED,
|
401, "Missing Authorization headers", Codes.UNAUTHORIZED,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,7 +121,7 @@ class Authenticator(object):
|
|||||||
json_request["signatures"].setdefault(origin, {})[key] = sig
|
json_request["signatures"].setdefault(origin, {})[key] = sig
|
||||||
|
|
||||||
if not json_request["signatures"]:
|
if not json_request["signatures"]:
|
||||||
raise NoAuthenticationError(
|
raise SynapseError(
|
||||||
401, "Missing Authorization headers", Codes.UNAUTHORIZED,
|
401, "Missing Authorization headers", Codes.UNAUTHORIZED,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -138,59 +130,38 @@ class Authenticator(object):
|
|||||||
logger.info("Request from %s", origin)
|
logger.info("Request from %s", origin)
|
||||||
request.authenticated_entity = origin
|
request.authenticated_entity = origin
|
||||||
|
|
||||||
defer.returnValue(origin)
|
defer.returnValue((origin, content))
|
||||||
|
|
||||||
|
|
||||||
class BaseFederationServlet(object):
|
class BaseFederationServlet(object):
|
||||||
REQUIRE_AUTH = True
|
def __init__(self, handler, authenticator, ratelimiter, server_name):
|
||||||
|
|
||||||
def __init__(self, handler, authenticator, ratelimiter, server_name,
|
|
||||||
room_list_handler):
|
|
||||||
self.handler = handler
|
self.handler = handler
|
||||||
self.authenticator = authenticator
|
self.authenticator = authenticator
|
||||||
self.ratelimiter = ratelimiter
|
self.ratelimiter = ratelimiter
|
||||||
self.room_list_handler = room_list_handler
|
|
||||||
|
|
||||||
def _wrap(self, func):
|
def _wrap(self, code):
|
||||||
authenticator = self.authenticator
|
authenticator = self.authenticator
|
||||||
ratelimiter = self.ratelimiter
|
ratelimiter = self.ratelimiter
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@functools.wraps(func)
|
@functools.wraps(code)
|
||||||
def new_func(request, *args, **kwargs):
|
def new_code(request, *args, **kwargs):
|
||||||
content = None
|
|
||||||
if request.method in ["PUT", "POST"]:
|
|
||||||
# TODO: Handle other method types? other content types?
|
|
||||||
content = parse_json_object_from_request(request)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
origin = yield authenticator.authenticate_request(request, content)
|
(origin, content) = yield authenticator.authenticate_request(request)
|
||||||
except NoAuthenticationError:
|
with ratelimiter.ratelimit(origin) as d:
|
||||||
origin = None
|
yield d
|
||||||
if self.REQUIRE_AUTH:
|
response = yield code(
|
||||||
logger.exception("authenticate_request failed")
|
origin, content, request.args, *args, **kwargs
|
||||||
raise
|
)
|
||||||
except:
|
except:
|
||||||
logger.exception("authenticate_request failed")
|
logger.exception("authenticate_request failed")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if origin:
|
|
||||||
with ratelimiter.ratelimit(origin) as d:
|
|
||||||
yield d
|
|
||||||
response = yield func(
|
|
||||||
origin, content, request.args, *args, **kwargs
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
response = yield func(
|
|
||||||
origin, content, request.args, *args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
defer.returnValue(response)
|
defer.returnValue(response)
|
||||||
|
|
||||||
# Extra logic that functools.wraps() doesn't finish
|
# Extra logic that functools.wraps() doesn't finish
|
||||||
new_func.__self__ = func.__self__
|
new_code.__self__ = code.__self__
|
||||||
|
|
||||||
return new_func
|
return new_code
|
||||||
|
|
||||||
def register(self, server):
|
def register(self, server):
|
||||||
pattern = re.compile("^" + PREFIX + self.PATH + "$")
|
pattern = re.compile("^" + PREFIX + self.PATH + "$")
|
||||||
@@ -298,17 +269,6 @@ class FederationStateServlet(BaseFederationServlet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class FederationStateIdsServlet(BaseFederationServlet):
|
|
||||||
PATH = "/state_ids/(?P<room_id>[^/]*)/"
|
|
||||||
|
|
||||||
def on_GET(self, origin, content, query, room_id):
|
|
||||||
return self.handler.on_state_ids_request(
|
|
||||||
origin,
|
|
||||||
room_id,
|
|
||||||
query.get("event_id", [None])[0],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FederationBackfillServlet(BaseFederationServlet):
|
class FederationBackfillServlet(BaseFederationServlet):
|
||||||
PATH = "/backfill/(?P<context>[^/]*)/"
|
PATH = "/backfill/(?P<context>[^/]*)/"
|
||||||
|
|
||||||
@@ -363,7 +323,7 @@ class FederationSendLeaveServlet(BaseFederationServlet):
|
|||||||
|
|
||||||
|
|
||||||
class FederationEventAuthServlet(BaseFederationServlet):
|
class FederationEventAuthServlet(BaseFederationServlet):
|
||||||
PATH = "/event_auth/(?P<context>[^/]*)/(?P<event_id>[^/]*)"
|
PATH = "/event_auth(?P<context>[^/]*)/(?P<event_id>[^/]*)"
|
||||||
|
|
||||||
def on_GET(self, origin, content, query, context, event_id):
|
def on_GET(self, origin, content, query, context, event_id):
|
||||||
return self.handler.on_event_auth(origin, context, event_id)
|
return self.handler.on_event_auth(origin, context, event_id)
|
||||||
@@ -405,15 +365,10 @@ class FederationThirdPartyInviteExchangeServlet(BaseFederationServlet):
|
|||||||
class FederationClientKeysQueryServlet(BaseFederationServlet):
|
class FederationClientKeysQueryServlet(BaseFederationServlet):
|
||||||
PATH = "/user/keys/query"
|
PATH = "/user/keys/query"
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
def on_POST(self, origin, content, query):
|
def on_POST(self, origin, content, query):
|
||||||
return self.handler.on_query_client_keys(origin, content)
|
response = yield self.handler.on_query_client_keys(origin, content)
|
||||||
|
defer.returnValue((200, response))
|
||||||
|
|
||||||
class FederationUserDevicesQueryServlet(BaseFederationServlet):
|
|
||||||
PATH = "/user/devices/(?P<user_id>[^/]*)"
|
|
||||||
|
|
||||||
def on_GET(self, origin, content, query, user_id):
|
|
||||||
return self.handler.on_query_user_devices(origin, user_id)
|
|
||||||
|
|
||||||
|
|
||||||
class FederationClientKeysClaimServlet(BaseFederationServlet):
|
class FederationClientKeysClaimServlet(BaseFederationServlet):
|
||||||
@@ -431,7 +386,7 @@ class FederationQueryAuthServlet(BaseFederationServlet):
|
|||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_POST(self, origin, content, query, context, event_id):
|
def on_POST(self, origin, content, query, context, event_id):
|
||||||
new_content = yield self.handler.on_query_auth_request(
|
new_content = yield self.handler.on_query_auth_request(
|
||||||
origin, content, context, event_id
|
origin, content, event_id
|
||||||
)
|
)
|
||||||
|
|
||||||
defer.returnValue((200, new_content))
|
defer.returnValue((200, new_content))
|
||||||
@@ -463,10 +418,9 @@ class FederationGetMissingEventsServlet(BaseFederationServlet):
|
|||||||
class On3pidBindServlet(BaseFederationServlet):
|
class On3pidBindServlet(BaseFederationServlet):
|
||||||
PATH = "/3pid/onbind"
|
PATH = "/3pid/onbind"
|
||||||
|
|
||||||
REQUIRE_AUTH = False
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_POST(self, origin, content, query):
|
def on_POST(self, request):
|
||||||
|
content = parse_json_object_from_request(request)
|
||||||
if "invites" in content:
|
if "invites" in content:
|
||||||
last_exception = None
|
last_exception = None
|
||||||
for invite in content["invites"]:
|
for invite in content["invites"]:
|
||||||
@@ -488,118 +442,10 @@ class On3pidBindServlet(BaseFederationServlet):
|
|||||||
raise last_exception
|
raise last_exception
|
||||||
defer.returnValue((200, {}))
|
defer.returnValue((200, {}))
|
||||||
|
|
||||||
|
# Avoid doing remote HS authorization checks which are done by default by
|
||||||
class OpenIdUserInfo(BaseFederationServlet):
|
# BaseFederationServlet.
|
||||||
"""
|
def _wrap(self, code):
|
||||||
Exchange a bearer token for information about a user.
|
return code
|
||||||
|
|
||||||
The response format should be compatible with:
|
|
||||||
http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
|
|
||||||
|
|
||||||
GET /openid/userinfo?access_token=ABDEFGH HTTP/1.1
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"sub": "@userpart:example.org",
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
PATH = "/openid/userinfo"
|
|
||||||
|
|
||||||
REQUIRE_AUTH = False
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def on_GET(self, origin, content, query):
|
|
||||||
token = query.get("access_token", [None])[0]
|
|
||||||
if token is None:
|
|
||||||
defer.returnValue((401, {
|
|
||||||
"errcode": "M_MISSING_TOKEN", "error": "Access Token required"
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
|
|
||||||
user_id = yield self.handler.on_openid_userinfo(token)
|
|
||||||
|
|
||||||
if user_id is None:
|
|
||||||
defer.returnValue((401, {
|
|
||||||
"errcode": "M_UNKNOWN_TOKEN",
|
|
||||||
"error": "Access Token unknown or expired"
|
|
||||||
}))
|
|
||||||
|
|
||||||
defer.returnValue((200, {"sub": user_id}))
|
|
||||||
|
|
||||||
|
|
||||||
class PublicRoomList(BaseFederationServlet):
|
|
||||||
"""
|
|
||||||
Fetch the public room list for this server.
|
|
||||||
|
|
||||||
This API returns information in the same format as /publicRooms on the
|
|
||||||
client API, but will only ever include local public rooms and hence is
|
|
||||||
intended for consumption by other home servers.
|
|
||||||
|
|
||||||
GET /publicRooms HTTP/1.1
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"chunk": [
|
|
||||||
{
|
|
||||||
"aliases": [
|
|
||||||
"#test:localhost"
|
|
||||||
],
|
|
||||||
"guest_can_join": false,
|
|
||||||
"name": "test room",
|
|
||||||
"num_joined_members": 3,
|
|
||||||
"room_id": "!whkydVegtvatLfXmPN:localhost",
|
|
||||||
"world_readable": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": "END",
|
|
||||||
"start": "START"
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
PATH = "/publicRooms"
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def on_GET(self, origin, content, query):
|
|
||||||
limit = parse_integer_from_args(query, "limit", 0)
|
|
||||||
since_token = parse_string_from_args(query, "since", None)
|
|
||||||
include_all_networks = parse_boolean_from_args(
|
|
||||||
query, "include_all_networks", False
|
|
||||||
)
|
|
||||||
third_party_instance_id = parse_string_from_args(
|
|
||||||
query, "third_party_instance_id", None
|
|
||||||
)
|
|
||||||
|
|
||||||
if include_all_networks:
|
|
||||||
network_tuple = None
|
|
||||||
elif third_party_instance_id:
|
|
||||||
network_tuple = ThirdPartyInstanceID.from_string(third_party_instance_id)
|
|
||||||
else:
|
|
||||||
network_tuple = ThirdPartyInstanceID(None, None)
|
|
||||||
|
|
||||||
data = yield self.room_list_handler.get_local_public_room_list(
|
|
||||||
limit, since_token,
|
|
||||||
network_tuple=network_tuple
|
|
||||||
)
|
|
||||||
defer.returnValue((200, data))
|
|
||||||
|
|
||||||
|
|
||||||
class FederationVersionServlet(BaseFederationServlet):
|
|
||||||
PATH = "/version"
|
|
||||||
|
|
||||||
REQUIRE_AUTH = False
|
|
||||||
|
|
||||||
def on_GET(self, origin, content, query):
|
|
||||||
return defer.succeed((200, {
|
|
||||||
"server": {
|
|
||||||
"name": "Synapse",
|
|
||||||
"version": get_version_string(synapse)
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
|
|
||||||
SERVLET_CLASSES = (
|
SERVLET_CLASSES = (
|
||||||
@@ -607,7 +453,6 @@ SERVLET_CLASSES = (
|
|||||||
FederationPullServlet,
|
FederationPullServlet,
|
||||||
FederationEventServlet,
|
FederationEventServlet,
|
||||||
FederationStateServlet,
|
FederationStateServlet,
|
||||||
FederationStateIdsServlet,
|
|
||||||
FederationBackfillServlet,
|
FederationBackfillServlet,
|
||||||
FederationQueryServlet,
|
FederationQueryServlet,
|
||||||
FederationMakeJoinServlet,
|
FederationMakeJoinServlet,
|
||||||
@@ -620,13 +465,9 @@ SERVLET_CLASSES = (
|
|||||||
FederationGetMissingEventsServlet,
|
FederationGetMissingEventsServlet,
|
||||||
FederationEventAuthServlet,
|
FederationEventAuthServlet,
|
||||||
FederationClientKeysQueryServlet,
|
FederationClientKeysQueryServlet,
|
||||||
FederationUserDevicesQueryServlet,
|
|
||||||
FederationClientKeysClaimServlet,
|
FederationClientKeysClaimServlet,
|
||||||
FederationThirdPartyInviteExchangeServlet,
|
FederationThirdPartyInviteExchangeServlet,
|
||||||
On3pidBindServlet,
|
On3pidBindServlet,
|
||||||
OpenIdUserInfo,
|
|
||||||
PublicRoomList,
|
|
||||||
FederationVersionServlet,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -637,5 +478,4 @@ def register_servlets(hs, resource, authenticator, ratelimiter):
|
|||||||
authenticator=authenticator,
|
authenticator=authenticator,
|
||||||
ratelimiter=ratelimiter,
|
ratelimiter=ratelimiter,
|
||||||
server_name=hs.hostname,
|
server_name=hs.hostname,
|
||||||
room_list_handler=hs.get_room_list_handler(),
|
|
||||||
).register(resource)
|
).register(resource)
|
||||||
|
|||||||
@@ -13,37 +13,35 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from synapse.appservice.scheduler import AppServiceScheduler
|
||||||
|
from synapse.appservice.api import ApplicationServiceApi
|
||||||
from .register import RegistrationHandler
|
from .register import RegistrationHandler
|
||||||
from .room import (
|
from .room import (
|
||||||
RoomCreationHandler, RoomContextHandler,
|
RoomCreationHandler, RoomListHandler, RoomContextHandler,
|
||||||
)
|
)
|
||||||
from .room_member import RoomMemberHandler
|
from .room_member import RoomMemberHandler
|
||||||
from .message import MessageHandler
|
from .message import MessageHandler
|
||||||
|
from .events import EventStreamHandler, EventHandler
|
||||||
from .federation import FederationHandler
|
from .federation import FederationHandler
|
||||||
from .profile import ProfileHandler
|
from .profile import ProfileHandler
|
||||||
|
from .presence import PresenceHandler
|
||||||
from .directory import DirectoryHandler
|
from .directory import DirectoryHandler
|
||||||
|
from .typing import TypingNotificationHandler
|
||||||
from .admin import AdminHandler
|
from .admin import AdminHandler
|
||||||
|
from .appservice import ApplicationServicesHandler
|
||||||
|
from .sync import SyncHandler
|
||||||
|
from .auth import AuthHandler
|
||||||
from .identity import IdentityHandler
|
from .identity import IdentityHandler
|
||||||
|
from .receipts import ReceiptsHandler
|
||||||
from .search import SearchHandler
|
from .search import SearchHandler
|
||||||
|
|
||||||
|
|
||||||
class Handlers(object):
|
class Handlers(object):
|
||||||
|
|
||||||
""" Deprecated. A collection of handlers.
|
""" A collection of all the event handlers.
|
||||||
|
|
||||||
At some point most of the classes whose name ended "Handler" were
|
There's no need to lazily create these; we'll just make them all eagerly
|
||||||
accessed through this class.
|
at construction time.
|
||||||
|
|
||||||
However this makes it painful to unit test the handlers and to run cut
|
|
||||||
down versions of synapse that only use specific handlers because using a
|
|
||||||
single handler required creating all of the handlers. So some of the
|
|
||||||
handlers have been lifted out of the Handlers object and are now accessed
|
|
||||||
directly through the homeserver object itself.
|
|
||||||
|
|
||||||
Any new handlers should follow the new pattern of being accessed through
|
|
||||||
the homeserver object and should not be added to the Handlers object.
|
|
||||||
|
|
||||||
The remaining handlers should be moved out of the handlers object.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
@@ -51,10 +49,26 @@ class Handlers(object):
|
|||||||
self.message_handler = MessageHandler(hs)
|
self.message_handler = MessageHandler(hs)
|
||||||
self.room_creation_handler = RoomCreationHandler(hs)
|
self.room_creation_handler = RoomCreationHandler(hs)
|
||||||
self.room_member_handler = RoomMemberHandler(hs)
|
self.room_member_handler = RoomMemberHandler(hs)
|
||||||
|
self.event_stream_handler = EventStreamHandler(hs)
|
||||||
|
self.event_handler = EventHandler(hs)
|
||||||
self.federation_handler = FederationHandler(hs)
|
self.federation_handler = FederationHandler(hs)
|
||||||
self.profile_handler = ProfileHandler(hs)
|
self.profile_handler = ProfileHandler(hs)
|
||||||
|
self.presence_handler = PresenceHandler(hs)
|
||||||
|
self.room_list_handler = RoomListHandler(hs)
|
||||||
self.directory_handler = DirectoryHandler(hs)
|
self.directory_handler = DirectoryHandler(hs)
|
||||||
|
self.typing_notification_handler = TypingNotificationHandler(hs)
|
||||||
self.admin_handler = AdminHandler(hs)
|
self.admin_handler = AdminHandler(hs)
|
||||||
|
self.receipts_handler = ReceiptsHandler(hs)
|
||||||
|
asapi = ApplicationServiceApi(hs)
|
||||||
|
self.appservice_handler = ApplicationServicesHandler(
|
||||||
|
hs, asapi, AppServiceScheduler(
|
||||||
|
clock=hs.get_clock(),
|
||||||
|
store=hs.get_datastore(),
|
||||||
|
as_api=asapi
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.sync_handler = SyncHandler(hs)
|
||||||
|
self.auth_handler = AuthHandler(hs)
|
||||||
self.identity_handler = IdentityHandler(hs)
|
self.identity_handler = IdentityHandler(hs)
|
||||||
self.search_handler = SearchHandler(hs)
|
self.search_handler = SearchHandler(hs)
|
||||||
self.room_context_handler = RoomContextHandler(hs)
|
self.room_context_handler = RoomContextHandler(hs)
|
||||||
|
|||||||
@@ -13,33 +13,49 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
import synapse.types
|
from synapse.api.errors import LimitExceededError, SynapseError, AuthError
|
||||||
|
from synapse.crypto.event_signing import add_hashes_and_signatures
|
||||||
from synapse.api.constants import Membership, EventTypes
|
from synapse.api.constants import Membership, EventTypes
|
||||||
from synapse.api.errors import LimitExceededError
|
from synapse.types import UserID, RoomAlias, Requester
|
||||||
from synapse.types import UserID
|
from synapse.push.action_generator import ActionGenerator
|
||||||
|
|
||||||
|
from synapse.util.logcontext import PreserveLoggingContext
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
VISIBILITY_PRIORITY = (
|
||||||
|
"world_readable",
|
||||||
|
"shared",
|
||||||
|
"invited",
|
||||||
|
"joined",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
MEMBERSHIP_PRIORITY = (
|
||||||
|
Membership.JOIN,
|
||||||
|
Membership.INVITE,
|
||||||
|
Membership.KNOCK,
|
||||||
|
Membership.LEAVE,
|
||||||
|
Membership.BAN,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseHandler(object):
|
class BaseHandler(object):
|
||||||
"""
|
"""
|
||||||
Common base class for the event handlers.
|
Common base class for the event handlers.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
store (synapse.storage.DataStore):
|
store (synapse.storage.events.StateStore):
|
||||||
state_handler (synapse.state.StateHandler):
|
state_handler (synapse.state.StateHandler):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
hs (synapse.server.HomeServer):
|
|
||||||
"""
|
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
self.auth = hs.get_auth()
|
self.auth = hs.get_auth()
|
||||||
self.notifier = hs.get_notifier()
|
self.notifier = hs.get_notifier()
|
||||||
@@ -49,26 +65,165 @@ class BaseHandler(object):
|
|||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
|
|
||||||
|
self.signing_key = hs.config.signing_key[0]
|
||||||
self.server_name = hs.hostname
|
self.server_name = hs.hostname
|
||||||
|
|
||||||
self.event_builder_factory = hs.get_event_builder_factory()
|
self.event_builder_factory = hs.get_event_builder_factory()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def filter_events_for_clients(self, user_tuples, events, event_id_to_state):
|
||||||
|
""" Returns dict of user_id -> list of events that user is allowed to
|
||||||
|
see.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_tuples (str, bool): (user id, is_peeking) for each user to be
|
||||||
|
checked. is_peeking should be true if:
|
||||||
|
* the user is not currently a member of the room, and:
|
||||||
|
* the user has not been a member of the room since the
|
||||||
|
given events
|
||||||
|
events ([synapse.events.EventBase]): list of events to filter
|
||||||
|
"""
|
||||||
|
forgotten = yield defer.gatherResults([
|
||||||
|
self.store.who_forgot_in_room(
|
||||||
|
room_id,
|
||||||
|
)
|
||||||
|
for room_id in frozenset(e.room_id for e in events)
|
||||||
|
], consumeErrors=True)
|
||||||
|
|
||||||
|
# Set of membership event_ids that have been forgotten
|
||||||
|
event_id_forgotten = frozenset(
|
||||||
|
row["event_id"] for rows in forgotten for row in rows
|
||||||
|
)
|
||||||
|
|
||||||
|
def allowed(event, user_id, is_peeking):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
event (synapse.events.EventBase): event to check
|
||||||
|
user_id (str)
|
||||||
|
is_peeking (bool)
|
||||||
|
"""
|
||||||
|
state = event_id_to_state[event.event_id]
|
||||||
|
|
||||||
|
# get the room_visibility at the time of the event.
|
||||||
|
visibility_event = state.get((EventTypes.RoomHistoryVisibility, ""), None)
|
||||||
|
if visibility_event:
|
||||||
|
visibility = visibility_event.content.get("history_visibility", "shared")
|
||||||
|
else:
|
||||||
|
visibility = "shared"
|
||||||
|
|
||||||
|
if visibility not in VISIBILITY_PRIORITY:
|
||||||
|
visibility = "shared"
|
||||||
|
|
||||||
|
# if it was world_readable, it's easy: everyone can read it
|
||||||
|
if visibility == "world_readable":
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Always allow history visibility events on boundaries. This is done
|
||||||
|
# by setting the effective visibility to the least restrictive
|
||||||
|
# of the old vs new.
|
||||||
|
if event.type == EventTypes.RoomHistoryVisibility:
|
||||||
|
prev_content = event.unsigned.get("prev_content", {})
|
||||||
|
prev_visibility = prev_content.get("history_visibility", None)
|
||||||
|
|
||||||
|
if prev_visibility not in VISIBILITY_PRIORITY:
|
||||||
|
prev_visibility = "shared"
|
||||||
|
|
||||||
|
new_priority = VISIBILITY_PRIORITY.index(visibility)
|
||||||
|
old_priority = VISIBILITY_PRIORITY.index(prev_visibility)
|
||||||
|
if old_priority < new_priority:
|
||||||
|
visibility = prev_visibility
|
||||||
|
|
||||||
|
# likewise, if the event is the user's own membership event, use
|
||||||
|
# the 'most joined' membership
|
||||||
|
membership = None
|
||||||
|
if event.type == EventTypes.Member and event.state_key == user_id:
|
||||||
|
membership = event.content.get("membership", None)
|
||||||
|
if membership not in MEMBERSHIP_PRIORITY:
|
||||||
|
membership = "leave"
|
||||||
|
|
||||||
|
prev_content = event.unsigned.get("prev_content", {})
|
||||||
|
prev_membership = prev_content.get("membership", None)
|
||||||
|
if prev_membership not in MEMBERSHIP_PRIORITY:
|
||||||
|
prev_membership = "leave"
|
||||||
|
|
||||||
|
new_priority = MEMBERSHIP_PRIORITY.index(membership)
|
||||||
|
old_priority = MEMBERSHIP_PRIORITY.index(prev_membership)
|
||||||
|
if old_priority < new_priority:
|
||||||
|
membership = prev_membership
|
||||||
|
|
||||||
|
# otherwise, get the user's membership at the time of the event.
|
||||||
|
if membership is None:
|
||||||
|
membership_event = state.get((EventTypes.Member, user_id), None)
|
||||||
|
if membership_event:
|
||||||
|
if membership_event.event_id not in event_id_forgotten:
|
||||||
|
membership = membership_event.membership
|
||||||
|
|
||||||
|
# if the user was a member of the room at the time of the event,
|
||||||
|
# they can see it.
|
||||||
|
if membership == Membership.JOIN:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if visibility == "joined":
|
||||||
|
# we weren't a member at the time of the event, so we can't
|
||||||
|
# see this event.
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif visibility == "invited":
|
||||||
|
# user can also see the event if they were *invited* at the time
|
||||||
|
# of the event.
|
||||||
|
return membership == Membership.INVITE
|
||||||
|
|
||||||
|
else:
|
||||||
|
# visibility is shared: user can also see the event if they have
|
||||||
|
# become a member since the event
|
||||||
|
#
|
||||||
|
# XXX: if the user has subsequently joined and then left again,
|
||||||
|
# ideally we would share history up to the point they left. But
|
||||||
|
# we don't know when they left.
|
||||||
|
return not is_peeking
|
||||||
|
|
||||||
|
defer.returnValue({
|
||||||
|
user_id: [
|
||||||
|
event
|
||||||
|
for event in events
|
||||||
|
if allowed(event, user_id, is_peeking)
|
||||||
|
]
|
||||||
|
for user_id, is_peeking in user_tuples
|
||||||
|
})
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _filter_events_for_client(self, user_id, events, is_peeking=False):
|
||||||
|
"""
|
||||||
|
Check which events a user is allowed to see
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id(str): user id to be checked
|
||||||
|
events([synapse.events.EventBase]): list of events to be checked
|
||||||
|
is_peeking(bool): should be True if:
|
||||||
|
* the user is not currently a member of the room, and:
|
||||||
|
* the user has not been a member of the room since the given
|
||||||
|
events
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
[synapse.events.EventBase]
|
||||||
|
"""
|
||||||
|
types = (
|
||||||
|
(EventTypes.RoomHistoryVisibility, ""),
|
||||||
|
(EventTypes.Member, user_id),
|
||||||
|
)
|
||||||
|
event_id_to_state = yield self.store.get_state_for_events(
|
||||||
|
frozenset(e.event_id for e in events),
|
||||||
|
types=types
|
||||||
|
)
|
||||||
|
res = yield self.filter_events_for_clients(
|
||||||
|
[(user_id, is_peeking)], events, event_id_to_state
|
||||||
|
)
|
||||||
|
defer.returnValue(res.get(user_id, []))
|
||||||
|
|
||||||
def ratelimit(self, requester):
|
def ratelimit(self, requester):
|
||||||
time_now = self.clock.time()
|
time_now = self.clock.time()
|
||||||
user_id = requester.user.to_string()
|
|
||||||
|
|
||||||
# The AS user itself is never rate limited.
|
|
||||||
app_service = self.store.get_app_service_by_user_id(user_id)
|
|
||||||
if app_service is not None:
|
|
||||||
return # do not ratelimit app service senders
|
|
||||||
|
|
||||||
# Disable rate limiting of users belonging to any AS that is configured
|
|
||||||
# not to be rate limited in its registration file (rate_limited: true|false).
|
|
||||||
if requester.app_service and not requester.app_service.is_rate_limited():
|
|
||||||
return
|
|
||||||
|
|
||||||
allowed, time_allowed = self.ratelimiter.send_message(
|
allowed, time_allowed = self.ratelimiter.send_message(
|
||||||
user_id, time_now,
|
requester.user.to_string(), time_now,
|
||||||
msg_rate_hz=self.hs.config.rc_messages_per_second,
|
msg_rate_hz=self.hs.config.rc_messages_per_second,
|
||||||
burst_count=self.hs.config.rc_message_burst_count,
|
burst_count=self.hs.config.rc_message_burst_count,
|
||||||
)
|
)
|
||||||
@@ -78,24 +233,213 @@ class BaseHandler(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def maybe_kick_guest_users(self, event, context=None):
|
def _create_new_client_event(self, builder, prev_event_ids=None):
|
||||||
|
if prev_event_ids:
|
||||||
|
prev_events = yield self.store.add_event_hashes(prev_event_ids)
|
||||||
|
prev_max_depth = yield self.store.get_max_depth_of_events(prev_event_ids)
|
||||||
|
depth = prev_max_depth + 1
|
||||||
|
else:
|
||||||
|
latest_ret = yield self.store.get_latest_event_ids_and_hashes_in_room(
|
||||||
|
builder.room_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if latest_ret:
|
||||||
|
depth = max([d for _, _, d in latest_ret]) + 1
|
||||||
|
else:
|
||||||
|
depth = 1
|
||||||
|
|
||||||
|
prev_events = [
|
||||||
|
(event_id, prev_hashes)
|
||||||
|
for event_id, prev_hashes, _ in latest_ret
|
||||||
|
]
|
||||||
|
|
||||||
|
builder.prev_events = prev_events
|
||||||
|
builder.depth = depth
|
||||||
|
|
||||||
|
state_handler = self.state_handler
|
||||||
|
|
||||||
|
context = yield state_handler.compute_event_context(builder)
|
||||||
|
|
||||||
|
if builder.is_state():
|
||||||
|
builder.prev_state = yield self.store.add_event_hashes(
|
||||||
|
context.prev_state_events
|
||||||
|
)
|
||||||
|
|
||||||
|
yield self.auth.add_auth_events(builder, context)
|
||||||
|
|
||||||
|
add_hashes_and_signatures(
|
||||||
|
builder, self.server_name, self.signing_key
|
||||||
|
)
|
||||||
|
|
||||||
|
event = builder.build()
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"Created event %s with current state: %s",
|
||||||
|
event.event_id, context.current_state,
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(
|
||||||
|
(event, context,)
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_host_in_room(self, current_state):
|
||||||
|
room_members = [
|
||||||
|
(state_key, event.membership)
|
||||||
|
for ((event_type, state_key), event) in current_state.items()
|
||||||
|
if event_type == EventTypes.Member
|
||||||
|
]
|
||||||
|
if len(room_members) == 0:
|
||||||
|
# Have we just created the room, and is this about to be the very
|
||||||
|
# first member event?
|
||||||
|
create_event = current_state.get(("m.room.create", ""))
|
||||||
|
if create_event:
|
||||||
|
return True
|
||||||
|
for (state_key, membership) in room_members:
|
||||||
|
if (
|
||||||
|
UserID.from_string(state_key).domain == self.hs.hostname
|
||||||
|
and membership == Membership.JOIN
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def handle_new_client_event(
|
||||||
|
self,
|
||||||
|
requester,
|
||||||
|
event,
|
||||||
|
context,
|
||||||
|
ratelimit=True,
|
||||||
|
extra_users=[]
|
||||||
|
):
|
||||||
|
# We now need to go and hit out to wherever we need to hit out to.
|
||||||
|
|
||||||
|
if ratelimit:
|
||||||
|
self.ratelimit(requester)
|
||||||
|
|
||||||
|
self.auth.check(event, auth_events=context.current_state)
|
||||||
|
|
||||||
|
yield self.maybe_kick_guest_users(event, context.current_state.values())
|
||||||
|
|
||||||
|
if event.type == EventTypes.CanonicalAlias:
|
||||||
|
# Check the alias is acually valid (at this time at least)
|
||||||
|
room_alias_str = event.content.get("alias", None)
|
||||||
|
if room_alias_str:
|
||||||
|
room_alias = RoomAlias.from_string(room_alias_str)
|
||||||
|
directory_handler = self.hs.get_handlers().directory_handler
|
||||||
|
mapping = yield directory_handler.get_association(room_alias)
|
||||||
|
|
||||||
|
if mapping["room_id"] != event.room_id:
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"Room alias %s does not point to the room" % (
|
||||||
|
room_alias_str,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
federation_handler = self.hs.get_handlers().federation_handler
|
||||||
|
|
||||||
|
if event.type == EventTypes.Member:
|
||||||
|
if event.content["membership"] == Membership.INVITE:
|
||||||
|
def is_inviter_member_event(e):
|
||||||
|
return (
|
||||||
|
e.type == EventTypes.Member and
|
||||||
|
e.sender == event.sender
|
||||||
|
)
|
||||||
|
|
||||||
|
event.unsigned["invite_room_state"] = [
|
||||||
|
{
|
||||||
|
"type": e.type,
|
||||||
|
"state_key": e.state_key,
|
||||||
|
"content": e.content,
|
||||||
|
"sender": e.sender,
|
||||||
|
}
|
||||||
|
for k, e in context.current_state.items()
|
||||||
|
if e.type in self.hs.config.room_invite_state_types
|
||||||
|
or is_inviter_member_event(e)
|
||||||
|
]
|
||||||
|
|
||||||
|
invitee = UserID.from_string(event.state_key)
|
||||||
|
if not self.hs.is_mine(invitee):
|
||||||
|
# TODO: Can we add signature from remote server in a nicer
|
||||||
|
# way? If we have been invited by a remote server, we need
|
||||||
|
# to get them to sign the event.
|
||||||
|
|
||||||
|
returned_invite = yield federation_handler.send_invite(
|
||||||
|
invitee.domain,
|
||||||
|
event,
|
||||||
|
)
|
||||||
|
|
||||||
|
event.unsigned.pop("room_state", None)
|
||||||
|
|
||||||
|
# TODO: Make sure the signatures actually are correct.
|
||||||
|
event.signatures.update(
|
||||||
|
returned_invite.signatures
|
||||||
|
)
|
||||||
|
|
||||||
|
if event.type == EventTypes.Redaction:
|
||||||
|
if self.auth.check_redaction(event, auth_events=context.current_state):
|
||||||
|
original_event = yield self.store.get_event(
|
||||||
|
event.redacts,
|
||||||
|
check_redacted=False,
|
||||||
|
get_prev_content=False,
|
||||||
|
allow_rejected=False,
|
||||||
|
allow_none=False
|
||||||
|
)
|
||||||
|
if event.user_id != original_event.user_id:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to redact events"
|
||||||
|
)
|
||||||
|
|
||||||
|
if event.type == EventTypes.Create and context.current_state:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"Changing the room create event is forbidden",
|
||||||
|
)
|
||||||
|
|
||||||
|
action_generator = ActionGenerator(self.hs)
|
||||||
|
yield action_generator.handle_push_actions_for_event(
|
||||||
|
event, context, self
|
||||||
|
)
|
||||||
|
|
||||||
|
(event_stream_id, max_stream_id) = yield self.store.persist_event(
|
||||||
|
event, context=context
|
||||||
|
)
|
||||||
|
|
||||||
|
destinations = set()
|
||||||
|
for k, s in context.current_state.items():
|
||||||
|
try:
|
||||||
|
if k[0] == EventTypes.Member:
|
||||||
|
if s.content["membership"] == Membership.JOIN:
|
||||||
|
destinations.add(
|
||||||
|
UserID.from_string(s.state_key).domain
|
||||||
|
)
|
||||||
|
except SynapseError:
|
||||||
|
logger.warn(
|
||||||
|
"Failed to get destination from event %s", s.event_id
|
||||||
|
)
|
||||||
|
|
||||||
|
with PreserveLoggingContext():
|
||||||
|
# Don't block waiting on waking up all the listeners.
|
||||||
|
self.notifier.on_new_room_event(
|
||||||
|
event, event_stream_id, max_stream_id,
|
||||||
|
extra_users=extra_users
|
||||||
|
)
|
||||||
|
|
||||||
|
# If invite, remove room_state from unsigned before sending.
|
||||||
|
event.unsigned.pop("invite_room_state", None)
|
||||||
|
|
||||||
|
federation_handler.handle_new_event(
|
||||||
|
event, destinations=destinations,
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def maybe_kick_guest_users(self, event, current_state):
|
||||||
# Technically this function invalidates current_state by changing it.
|
# Technically this function invalidates current_state by changing it.
|
||||||
# Hopefully this isn't that important to the caller.
|
# Hopefully this isn't that important to the caller.
|
||||||
if event.type == EventTypes.GuestAccess:
|
if event.type == EventTypes.GuestAccess:
|
||||||
guest_access = event.content.get("guest_access", "forbidden")
|
guest_access = event.content.get("guest_access", "forbidden")
|
||||||
if guest_access != "can_join":
|
if guest_access != "can_join":
|
||||||
if context:
|
|
||||||
current_state = yield self.store.get_events(
|
|
||||||
context.current_state_ids.values()
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
current_state = yield self.state_handler.get_current_state(
|
|
||||||
event.room_id
|
|
||||||
)
|
|
||||||
|
|
||||||
current_state = current_state.values()
|
|
||||||
|
|
||||||
logger.info("maybe_kick_guest_users %r", current_state)
|
|
||||||
yield self.kick_guest_users(current_state)
|
yield self.kick_guest_users(current_state)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@@ -128,8 +472,7 @@ class BaseHandler(object):
|
|||||||
# and having homeservers have their own users leave keeps more
|
# and having homeservers have their own users leave keeps more
|
||||||
# of that decision-making and control local to the guest-having
|
# of that decision-making and control local to the guest-having
|
||||||
# homeserver.
|
# homeserver.
|
||||||
requester = synapse.types.create_requester(
|
requester = Requester(target_user, "", True)
|
||||||
target_user, is_guest=True)
|
|
||||||
handler = self.hs.get_handlers().room_member_handler
|
handler = self.hs.get_handlers().room_member_handler
|
||||||
yield handler.update_membership(
|
yield handler.update_membership(
|
||||||
requester,
|
requester,
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.constants import EventTypes
|
from synapse.api.constants import EventTypes
|
||||||
from synapse.util.metrics import Measure
|
from synapse.appservice import ApplicationService
|
||||||
from synapse.util.logcontext import preserve_fn, preserve_context_over_deferred
|
from synapse.types import UserID
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -35,81 +35,47 @@ def log_failure(failure):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# NB: Purposefully not inheriting BaseHandler since that contains way too much
|
||||||
|
# setup code which this handler does not need or use. This makes testing a lot
|
||||||
|
# easier.
|
||||||
class ApplicationServicesHandler(object):
|
class ApplicationServicesHandler(object):
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs, appservice_api, appservice_scheduler):
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
self.is_mine_id = hs.is_mine_id
|
self.hs = hs
|
||||||
self.appservice_api = hs.get_application_service_api()
|
self.appservice_api = appservice_api
|
||||||
self.scheduler = hs.get_application_service_scheduler()
|
self.scheduler = appservice_scheduler
|
||||||
self.started_scheduler = False
|
self.started_scheduler = False
|
||||||
self.clock = hs.get_clock()
|
|
||||||
self.notify_appservices = hs.config.notify_appservices
|
|
||||||
|
|
||||||
self.current_max = 0
|
|
||||||
self.is_processing = False
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def notify_interested_services(self, current_id):
|
def notify_interested_services(self, event):
|
||||||
"""Notifies (pushes) all application services interested in this event.
|
"""Notifies (pushes) all application services interested in this event.
|
||||||
|
|
||||||
Pushing is done asynchronously, so this method won't block for any
|
Pushing is done asynchronously, so this method won't block for any
|
||||||
prolonged length of time.
|
prolonged length of time.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
current_id(int): The current maximum ID.
|
event(Event): The event to push out to interested services.
|
||||||
"""
|
"""
|
||||||
services = self.store.get_app_services()
|
# Gather interested services
|
||||||
if not services or not self.notify_appservices:
|
services = yield self._get_services_for_event(event)
|
||||||
return
|
if len(services) == 0:
|
||||||
|
return # no services need notifying
|
||||||
|
|
||||||
self.current_max = max(self.current_max, current_id)
|
# Do we know this user exists? If not, poke the user query API for
|
||||||
if self.is_processing:
|
# all services which match that user regex. This needs to block as these
|
||||||
return
|
# user queries need to be made BEFORE pushing the event.
|
||||||
|
yield self._check_user_exists(event.sender)
|
||||||
|
if event.type == EventTypes.Member:
|
||||||
|
yield self._check_user_exists(event.state_key)
|
||||||
|
|
||||||
with Measure(self.clock, "notify_interested_services"):
|
if not self.started_scheduler:
|
||||||
self.is_processing = True
|
self.scheduler.start().addErrback(log_failure)
|
||||||
try:
|
self.started_scheduler = True
|
||||||
upper_bound = self.current_max
|
|
||||||
limit = 100
|
|
||||||
while True:
|
|
||||||
upper_bound, events = yield self.store.get_new_events_for_appservice(
|
|
||||||
upper_bound, limit
|
|
||||||
)
|
|
||||||
|
|
||||||
if not events:
|
# Fork off pushes to these services
|
||||||
break
|
for service in services:
|
||||||
|
self.scheduler.submit_event_for_as(service, event)
|
||||||
for event in events:
|
|
||||||
# Gather interested services
|
|
||||||
services = yield self._get_services_for_event(event)
|
|
||||||
if len(services) == 0:
|
|
||||||
continue # no services need notifying
|
|
||||||
|
|
||||||
# Do we know this user exists? If not, poke the user
|
|
||||||
# query API for all services which match that user regex.
|
|
||||||
# This needs to block as these user queries need to be
|
|
||||||
# made BEFORE pushing the event.
|
|
||||||
yield self._check_user_exists(event.sender)
|
|
||||||
if event.type == EventTypes.Member:
|
|
||||||
yield self._check_user_exists(event.state_key)
|
|
||||||
|
|
||||||
if not self.started_scheduler:
|
|
||||||
self.scheduler.start().addErrback(log_failure)
|
|
||||||
self.started_scheduler = True
|
|
||||||
|
|
||||||
# Fork off pushes to these services
|
|
||||||
for service in services:
|
|
||||||
preserve_fn(self.scheduler.submit_event_for_as)(
|
|
||||||
service, event
|
|
||||||
)
|
|
||||||
|
|
||||||
yield self.store.set_appservice_last_pos(upper_bound)
|
|
||||||
|
|
||||||
if len(events) < limit:
|
|
||||||
break
|
|
||||||
finally:
|
|
||||||
self.is_processing = False
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def query_user_exists(self, user_id):
|
def query_user_exists(self, user_id):
|
||||||
@@ -142,12 +108,11 @@ class ApplicationServicesHandler(object):
|
|||||||
association can be found.
|
association can be found.
|
||||||
"""
|
"""
|
||||||
room_alias_str = room_alias.to_string()
|
room_alias_str = room_alias.to_string()
|
||||||
services = self.store.get_app_services()
|
alias_query_services = yield self._get_services_for_event(
|
||||||
alias_query_services = [
|
event=None,
|
||||||
s for s in services if (
|
restrict_to=ApplicationService.NS_ALIASES,
|
||||||
s.is_interested_in_alias(room_alias_str)
|
alias_list=[room_alias_str]
|
||||||
)
|
)
|
||||||
]
|
|
||||||
for alias_service in alias_query_services:
|
for alias_service in alias_query_services:
|
||||||
is_known_alias = yield self.appservice_api.query_alias(
|
is_known_alias = yield self.appservice_api.query_alias(
|
||||||
alias_service, room_alias_str
|
alias_service, room_alias_str
|
||||||
@@ -160,97 +125,52 @@ class ApplicationServicesHandler(object):
|
|||||||
defer.returnValue(result)
|
defer.returnValue(result)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def query_3pe(self, kind, protocol, fields):
|
def _get_services_for_event(self, event, restrict_to="", alias_list=None):
|
||||||
services = yield self._get_services_for_3pn(protocol)
|
|
||||||
|
|
||||||
results = yield preserve_context_over_deferred(defer.DeferredList([
|
|
||||||
preserve_fn(self.appservice_api.query_3pe)(service, kind, protocol, fields)
|
|
||||||
for service in services
|
|
||||||
], consumeErrors=True))
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
for (success, result) in results:
|
|
||||||
if success:
|
|
||||||
ret.extend(result)
|
|
||||||
|
|
||||||
defer.returnValue(ret)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def get_3pe_protocols(self, only_protocol=None):
|
|
||||||
services = self.store.get_app_services()
|
|
||||||
protocols = {}
|
|
||||||
|
|
||||||
# Collect up all the individual protocol responses out of the ASes
|
|
||||||
for s in services:
|
|
||||||
for p in s.protocols:
|
|
||||||
if only_protocol is not None and p != only_protocol:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if p not in protocols:
|
|
||||||
protocols[p] = []
|
|
||||||
|
|
||||||
info = yield self.appservice_api.get_3pe_protocol(s, p)
|
|
||||||
|
|
||||||
if info is not None:
|
|
||||||
protocols[p].append(info)
|
|
||||||
|
|
||||||
def _merge_instances(infos):
|
|
||||||
if not infos:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# Merge the 'instances' lists of multiple results, but just take
|
|
||||||
# the other fields from the first as they ought to be identical
|
|
||||||
# copy the result so as not to corrupt the cached one
|
|
||||||
combined = dict(infos[0])
|
|
||||||
combined["instances"] = list(combined["instances"])
|
|
||||||
|
|
||||||
for info in infos[1:]:
|
|
||||||
combined["instances"].extend(info["instances"])
|
|
||||||
|
|
||||||
return combined
|
|
||||||
|
|
||||||
for p in protocols.keys():
|
|
||||||
protocols[p] = _merge_instances(protocols[p])
|
|
||||||
|
|
||||||
defer.returnValue(protocols)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _get_services_for_event(self, event):
|
|
||||||
"""Retrieve a list of application services interested in this event.
|
"""Retrieve a list of application services interested in this event.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
event(Event): The event to check. Can be None if alias_list is not.
|
event(Event): The event to check. Can be None if alias_list is not.
|
||||||
|
restrict_to(str): The namespace to restrict regex tests to.
|
||||||
|
alias_list: A list of aliases to get services for. If None, this
|
||||||
|
list is obtained from the database.
|
||||||
Returns:
|
Returns:
|
||||||
list<ApplicationService>: A list of services interested in this
|
list<ApplicationService>: A list of services interested in this
|
||||||
event based on the service regex.
|
event based on the service regex.
|
||||||
"""
|
"""
|
||||||
services = self.store.get_app_services()
|
member_list = None
|
||||||
|
if hasattr(event, "room_id"):
|
||||||
|
# We need to know the aliases associated with this event.room_id,
|
||||||
|
# if any.
|
||||||
|
if not alias_list:
|
||||||
|
alias_list = yield self.store.get_aliases_for_room(
|
||||||
|
event.room_id
|
||||||
|
)
|
||||||
|
# We need to know the members associated with this event.room_id,
|
||||||
|
# if any.
|
||||||
|
member_list = yield self.store.get_users_in_room(event.room_id)
|
||||||
|
|
||||||
|
services = yield self.store.get_app_services()
|
||||||
interested_list = [
|
interested_list = [
|
||||||
s for s in services if (
|
s for s in services if (
|
||||||
yield s.is_interested(event, self.store)
|
s.is_interested(event, restrict_to, alias_list, member_list)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
defer.returnValue(interested_list)
|
defer.returnValue(interested_list)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
def _get_services_for_user(self, user_id):
|
def _get_services_for_user(self, user_id):
|
||||||
services = self.store.get_app_services()
|
services = yield self.store.get_app_services()
|
||||||
interested_list = [
|
interested_list = [
|
||||||
s for s in services if (
|
s for s in services if (
|
||||||
s.is_interested_in_user(user_id)
|
s.is_interested_in_user(user_id)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
return defer.succeed(interested_list)
|
defer.returnValue(interested_list)
|
||||||
|
|
||||||
def _get_services_for_3pn(self, protocol):
|
|
||||||
services = self.store.get_app_services()
|
|
||||||
interested_list = [
|
|
||||||
s for s in services if s.is_interested_in_protocol(protocol)
|
|
||||||
]
|
|
||||||
return defer.succeed(interested_list)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _is_unknown_user(self, user_id):
|
def _is_unknown_user(self, user_id):
|
||||||
if not self.is_mine_id(user_id):
|
user = UserID.from_string(user_id)
|
||||||
|
if not self.hs.is_mine(user):
|
||||||
# we don't know if they are unknown or not since it isn't one of our
|
# we don't know if they are unknown or not since it isn't one of our
|
||||||
# users. We can't poke ASes.
|
# users. We can't poke ASes.
|
||||||
defer.returnValue(False)
|
defer.returnValue(False)
|
||||||
@@ -262,7 +182,7 @@ class ApplicationServicesHandler(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# user not found; could be the AS though, so check.
|
# user not found; could be the AS though, so check.
|
||||||
services = self.store.get_app_services()
|
services = yield self.store.get_app_services()
|
||||||
service_list = [s for s in services if s.sender == user_id]
|
service_list = [s for s in services if s.sender == user_id]
|
||||||
defer.returnValue(len(service_list) == 0)
|
defer.returnValue(len(service_list) == 0)
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from twisted.internet import defer
|
|||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
from synapse.api.constants import LoginType
|
from synapse.api.constants import LoginType
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID
|
||||||
from synapse.api.errors import AuthError, LoginError, Codes, StoreError, SynapseError
|
from synapse.api.errors import AuthError, LoginError, Codes
|
||||||
from synapse.util.async import run_on_reactor
|
from synapse.util.async import run_on_reactor
|
||||||
|
|
||||||
from twisted.web.client import PartialDownloadError
|
from twisted.web.client import PartialDownloadError
|
||||||
@@ -38,10 +38,6 @@ class AuthHandler(BaseHandler):
|
|||||||
SESSION_EXPIRE_MS = 48 * 60 * 60 * 1000
|
SESSION_EXPIRE_MS = 48 * 60 * 60 * 1000
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
hs (synapse.server.HomeServer):
|
|
||||||
"""
|
|
||||||
super(AuthHandler, self).__init__(hs)
|
super(AuthHandler, self).__init__(hs)
|
||||||
self.checkers = {
|
self.checkers = {
|
||||||
LoginType.PASSWORD: self._check_password_auth,
|
LoginType.PASSWORD: self._check_password_auth,
|
||||||
@@ -51,21 +47,22 @@ class AuthHandler(BaseHandler):
|
|||||||
}
|
}
|
||||||
self.bcrypt_rounds = hs.config.bcrypt_rounds
|
self.bcrypt_rounds = hs.config.bcrypt_rounds
|
||||||
self.sessions = {}
|
self.sessions = {}
|
||||||
|
self.INVALID_TOKEN_HTTP_STATUS = 401
|
||||||
|
|
||||||
account_handler = _AccountHandler(
|
self.ldap_enabled = hs.config.ldap_enabled
|
||||||
hs, check_user_exists=self.check_user_exists
|
self.ldap_server = hs.config.ldap_server
|
||||||
)
|
self.ldap_port = hs.config.ldap_port
|
||||||
|
self.ldap_tls = hs.config.ldap_tls
|
||||||
|
self.ldap_search_base = hs.config.ldap_search_base
|
||||||
|
self.ldap_search_property = hs.config.ldap_search_property
|
||||||
|
self.ldap_email_property = hs.config.ldap_email_property
|
||||||
|
self.ldap_full_name_property = hs.config.ldap_full_name_property
|
||||||
|
|
||||||
self.password_providers = [
|
if self.ldap_enabled is True:
|
||||||
module(config=config, account_handler=account_handler)
|
import ldap
|
||||||
for module, config in hs.config.password_providers
|
logger.info("Import ldap version: %s", ldap.__version__)
|
||||||
]
|
|
||||||
|
|
||||||
logger.info("Extra password_providers: %r", self.password_providers)
|
|
||||||
|
|
||||||
self.hs = hs # FIXME better possibility to access registrationHandler later?
|
self.hs = hs # FIXME better possibility to access registrationHandler later?
|
||||||
self.device_handler = hs.get_device_handler()
|
|
||||||
self.macaroon_gen = hs.get_macaroon_generator()
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def check_auth(self, flows, clientdict, clientip):
|
def check_auth(self, flows, clientdict, clientip):
|
||||||
@@ -136,47 +133,21 @@ class AuthHandler(BaseHandler):
|
|||||||
creds = session['creds']
|
creds = session['creds']
|
||||||
|
|
||||||
# check auth type currently being presented
|
# check auth type currently being presented
|
||||||
errordict = {}
|
|
||||||
if 'type' in authdict:
|
if 'type' in authdict:
|
||||||
login_type = authdict['type']
|
if authdict['type'] not in self.checkers:
|
||||||
if login_type not in self.checkers:
|
|
||||||
raise LoginError(400, "", Codes.UNRECOGNIZED)
|
raise LoginError(400, "", Codes.UNRECOGNIZED)
|
||||||
try:
|
result = yield self.checkers[authdict['type']](authdict, clientip)
|
||||||
result = yield self.checkers[login_type](authdict, clientip)
|
if result:
|
||||||
if result:
|
creds[authdict['type']] = result
|
||||||
creds[login_type] = result
|
self._save_session(session)
|
||||||
self._save_session(session)
|
|
||||||
except LoginError, e:
|
|
||||||
if login_type == LoginType.EMAIL_IDENTITY:
|
|
||||||
# riot used to have a bug where it would request a new
|
|
||||||
# validation token (thus sending a new email) each time it
|
|
||||||
# got a 401 with a 'flows' field.
|
|
||||||
# (https://github.com/vector-im/vector-web/issues/2447).
|
|
||||||
#
|
|
||||||
# Grandfather in the old behaviour for now to avoid
|
|
||||||
# breaking old riot deployments.
|
|
||||||
raise e
|
|
||||||
|
|
||||||
# this step failed. Merge the error dict into the response
|
|
||||||
# so that the client can have another go.
|
|
||||||
errordict = e.error_dict()
|
|
||||||
|
|
||||||
for f in flows:
|
for f in flows:
|
||||||
if len(set(f) - set(creds.keys())) == 0:
|
if len(set(f) - set(creds.keys())) == 0:
|
||||||
# it's very useful to know what args are stored, but this can
|
logger.info("Auth completed with creds: %r", creds)
|
||||||
# include the password in the case of registering, so only log
|
|
||||||
# the keys (confusingly, clientdict may contain a password
|
|
||||||
# param, creds is just what the user authed as for UI auth
|
|
||||||
# and is not sensitive).
|
|
||||||
logger.info(
|
|
||||||
"Auth completed with creds: %r. Client dict has keys: %r",
|
|
||||||
creds, clientdict.keys()
|
|
||||||
)
|
|
||||||
defer.returnValue((True, creds, clientdict, session['id']))
|
defer.returnValue((True, creds, clientdict, session['id']))
|
||||||
|
|
||||||
ret = self._auth_dict_for_flows(flows, session)
|
ret = self._auth_dict_for_flows(flows, session)
|
||||||
ret['completed'] = creds.keys()
|
ret['completed'] = creds.keys()
|
||||||
ret.update(errordict)
|
|
||||||
defer.returnValue((False, ret, clientdict, session['id']))
|
defer.returnValue((False, ret, clientdict, session['id']))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@@ -249,6 +220,7 @@ class AuthHandler(BaseHandler):
|
|||||||
sess = self._get_session_info(session_id)
|
sess = self._get_session_info(session_id)
|
||||||
return sess.setdefault('serverdict', {}).get(key, default)
|
return sess.setdefault('serverdict', {}).get(key, default)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
def _check_password_auth(self, authdict, _):
|
def _check_password_auth(self, authdict, _):
|
||||||
if "user" not in authdict or "password" not in authdict:
|
if "user" not in authdict or "password" not in authdict:
|
||||||
raise LoginError(400, "", Codes.MISSING_PARAM)
|
raise LoginError(400, "", Codes.MISSING_PARAM)
|
||||||
@@ -258,7 +230,11 @@ class AuthHandler(BaseHandler):
|
|||||||
if not user_id.startswith('@'):
|
if not user_id.startswith('@'):
|
||||||
user_id = UserID.create(user_id, self.hs.hostname).to_string()
|
user_id = UserID.create(user_id, self.hs.hostname).to_string()
|
||||||
|
|
||||||
return self._check_password(user_id, password)
|
if not (yield self._check_password(user_id, password)):
|
||||||
|
logger.warn("Failed password login for user %s", user_id)
|
||||||
|
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
|
||||||
|
|
||||||
|
defer.returnValue(user_id)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _check_recaptcha(self, authdict, clientip):
|
def _check_recaptcha(self, authdict, clientip):
|
||||||
@@ -294,17 +270,8 @@ class AuthHandler(BaseHandler):
|
|||||||
data = pde.response
|
data = pde.response
|
||||||
resp_body = simplejson.loads(data)
|
resp_body = simplejson.loads(data)
|
||||||
|
|
||||||
if 'success' in resp_body:
|
if 'success' in resp_body and resp_body['success']:
|
||||||
# Note that we do NOT check the hostname here: we explicitly
|
defer.returnValue(True)
|
||||||
# intend the CAPTCHA to be presented by whatever client the
|
|
||||||
# user is using, we just care that they have completed a CAPTCHA.
|
|
||||||
logger.info(
|
|
||||||
"%s reCAPTCHA from hostname %s",
|
|
||||||
"Successful" if resp_body['success'] else "Failed",
|
|
||||||
resp_body.get('hostname')
|
|
||||||
)
|
|
||||||
if resp_body['success']:
|
|
||||||
defer.returnValue(True)
|
|
||||||
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
|
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@@ -371,229 +338,239 @@ class AuthHandler(BaseHandler):
|
|||||||
|
|
||||||
return self.sessions[session_id]
|
return self.sessions[session_id]
|
||||||
|
|
||||||
def validate_password_login(self, user_id, password):
|
@defer.inlineCallbacks
|
||||||
|
def login_with_password(self, user_id, password):
|
||||||
"""
|
"""
|
||||||
Authenticates the user with their username and password.
|
Authenticates the user with their username and password.
|
||||||
|
|
||||||
Used only by the v1 login API.
|
Used only by the v1 login API.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id (str): complete @user:id
|
user_id (str): User ID
|
||||||
password (str): Password
|
password (str): Password
|
||||||
Returns:
|
Returns:
|
||||||
defer.Deferred: (str) canonical user id
|
A tuple of:
|
||||||
Raises:
|
The user's ID.
|
||||||
StoreError if there was a problem accessing the database
|
|
||||||
LoginError if there was an authentication problem.
|
|
||||||
"""
|
|
||||||
return self._check_password(user_id, password)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def get_access_token_for_user_id(self, user_id, device_id=None,
|
|
||||||
initial_display_name=None):
|
|
||||||
"""
|
|
||||||
Creates a new access token for the user with the given user ID.
|
|
||||||
|
|
||||||
The user is assumed to have been authenticated by some other
|
|
||||||
machanism (e.g. CAS), and the user_id converted to the canonical case.
|
|
||||||
|
|
||||||
The device will be recorded in the table if it is not there already.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_id (str): canonical User ID
|
|
||||||
device_id (str|None): the device ID to associate with the tokens.
|
|
||||||
None to leave the tokens unassociated with a device (deprecated:
|
|
||||||
we should always have a device ID)
|
|
||||||
initial_display_name (str): display name to associate with the
|
|
||||||
device if it needs re-registering
|
|
||||||
Returns:
|
|
||||||
The access token for the user's session.
|
The access token for the user's session.
|
||||||
|
The refresh token for the user's session.
|
||||||
Raises:
|
Raises:
|
||||||
StoreError if there was a problem storing the token.
|
StoreError if there was a problem storing the token.
|
||||||
LoginError if there was an authentication problem.
|
LoginError if there was an authentication problem.
|
||||||
"""
|
"""
|
||||||
logger.info("Logging in user %s on device %s", user_id, device_id)
|
|
||||||
access_token = yield self.issue_access_token(user_id, device_id)
|
|
||||||
|
|
||||||
# the device *should* have been registered before we got here; however,
|
if not (yield self._check_password(user_id, password)):
|
||||||
# it's possible we raced against a DELETE operation. The thing we
|
logger.warn("Failed password login for user %s", user_id)
|
||||||
# really don't want is active access_tokens without a record of the
|
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
|
||||||
# device, so we double-check it here.
|
|
||||||
if device_id is not None:
|
|
||||||
yield self.device_handler.check_device_registered(
|
|
||||||
user_id, device_id, initial_display_name
|
|
||||||
)
|
|
||||||
|
|
||||||
defer.returnValue(access_token)
|
logger.info("Logging in user %s", user_id)
|
||||||
|
access_token = yield self.issue_access_token(user_id)
|
||||||
|
refresh_token = yield self.issue_refresh_token(user_id)
|
||||||
|
defer.returnValue((user_id, access_token, refresh_token))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def check_user_exists(self, user_id):
|
def get_login_tuple_for_user_id(self, user_id):
|
||||||
"""
|
"""
|
||||||
Checks to see if a user with the given id exists. Will check case
|
Gets login tuple for the user with the given user ID.
|
||||||
insensitively, but return None if there are multiple inexact matches.
|
The user is assumed to have been authenticated by some other
|
||||||
|
machanism (e.g. CAS)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
(str) user_id: complete @user:id
|
user_id (str): User ID
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
defer.Deferred: (str) canonical_user_id, or None if zero or
|
A tuple of:
|
||||||
multiple matches
|
The user's ID.
|
||||||
|
The access token for the user's session.
|
||||||
|
The refresh token for the user's session.
|
||||||
|
Raises:
|
||||||
|
StoreError if there was a problem storing the token.
|
||||||
|
LoginError if there was an authentication problem.
|
||||||
"""
|
"""
|
||||||
res = yield self._find_user_id_and_pwd_hash(user_id)
|
user_id, ignored = yield self._find_user_id_and_pwd_hash(user_id)
|
||||||
if res is not None:
|
|
||||||
defer.returnValue(res[0])
|
logger.info("Logging in user %s", user_id)
|
||||||
defer.returnValue(None)
|
access_token = yield self.issue_access_token(user_id)
|
||||||
|
refresh_token = yield self.issue_refresh_token(user_id)
|
||||||
|
defer.returnValue((user_id, access_token, refresh_token))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def does_user_exist(self, user_id):
|
||||||
|
try:
|
||||||
|
yield self._find_user_id_and_pwd_hash(user_id)
|
||||||
|
defer.returnValue(True)
|
||||||
|
except LoginError:
|
||||||
|
defer.returnValue(False)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _find_user_id_and_pwd_hash(self, user_id):
|
def _find_user_id_and_pwd_hash(self, user_id):
|
||||||
"""Checks to see if a user with the given id exists. Will check case
|
"""Checks to see if a user with the given id exists. Will check case
|
||||||
insensitively, but will return None if there are multiple inexact
|
insensitively, but will throw if there are multiple inexact matches.
|
||||||
matches.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple: A 2-tuple of `(canonical_user_id, password_hash)`
|
tuple: A 2-tuple of `(canonical_user_id, password_hash)`
|
||||||
None: if there is not exactly one match
|
|
||||||
"""
|
"""
|
||||||
user_infos = yield self.store.get_users_by_id_case_insensitive(user_id)
|
user_infos = yield self.store.get_users_by_id_case_insensitive(user_id)
|
||||||
|
|
||||||
result = None
|
|
||||||
if not user_infos:
|
if not user_infos:
|
||||||
logger.warn("Attempted to login as %s but they do not exist", user_id)
|
logger.warn("Attempted to login as %s but they do not exist", user_id)
|
||||||
elif len(user_infos) == 1:
|
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
|
||||||
# a single match (possibly not exact)
|
|
||||||
result = user_infos.popitem()
|
if len(user_infos) > 1:
|
||||||
elif user_id in user_infos:
|
if user_id not in user_infos:
|
||||||
# multiple matches, but one is exact
|
logger.warn(
|
||||||
result = (user_id, user_infos[user_id])
|
"Attempted to login as %s but it matches more than one user "
|
||||||
|
"inexactly: %r",
|
||||||
|
user_id, user_infos.keys()
|
||||||
|
)
|
||||||
|
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
|
||||||
|
|
||||||
|
defer.returnValue((user_id, user_infos[user_id]))
|
||||||
else:
|
else:
|
||||||
# multiple matches, none of them exact
|
defer.returnValue(user_infos.popitem())
|
||||||
logger.warn(
|
|
||||||
"Attempted to login as %s but it matches more than one user "
|
|
||||||
"inexactly: %r",
|
|
||||||
user_id, user_infos.keys()
|
|
||||||
)
|
|
||||||
defer.returnValue(result)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _check_password(self, user_id, password):
|
def _check_password(self, user_id, password):
|
||||||
"""Authenticate a user against the LDAP and local databases.
|
defer.returnValue(
|
||||||
|
not (
|
||||||
user_id is checked case insensitively against the local database, but
|
(yield self._check_ldap_password(user_id, password))
|
||||||
will throw if there are multiple inexact matches.
|
or
|
||||||
|
(yield self._check_local_password(user_id, password))
|
||||||
Args:
|
))
|
||||||
user_id (str): complete @user:id
|
|
||||||
Returns:
|
|
||||||
(str) the canonical_user_id
|
|
||||||
Raises:
|
|
||||||
LoginError if login fails
|
|
||||||
"""
|
|
||||||
for provider in self.password_providers:
|
|
||||||
is_valid = yield provider.check_password(user_id, password)
|
|
||||||
if is_valid:
|
|
||||||
defer.returnValue(user_id)
|
|
||||||
|
|
||||||
canonical_user_id = yield self._check_local_password(user_id, password)
|
|
||||||
|
|
||||||
if canonical_user_id:
|
|
||||||
defer.returnValue(canonical_user_id)
|
|
||||||
|
|
||||||
# unknown username or invalid password. We raise a 403 here, but note
|
|
||||||
# that if we're doing user-interactive login, it turns all LoginErrors
|
|
||||||
# into a 401 anyway.
|
|
||||||
raise LoginError(
|
|
||||||
403, "Invalid password",
|
|
||||||
errcode=Codes.FORBIDDEN
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _check_local_password(self, user_id, password):
|
def _check_local_password(self, user_id, password):
|
||||||
"""Authenticate a user against the local password database.
|
try:
|
||||||
|
user_id, password_hash = yield self._find_user_id_and_pwd_hash(user_id)
|
||||||
user_id is checked case insensitively, but will return None if there are
|
defer.returnValue(not self.validate_hash(password, password_hash))
|
||||||
multiple inexact matches.
|
except LoginError:
|
||||||
|
defer.returnValue(False)
|
||||||
Args:
|
|
||||||
user_id (str): complete @user:id
|
|
||||||
Returns:
|
|
||||||
(str) the canonical_user_id, or None if unknown user / bad password
|
|
||||||
"""
|
|
||||||
lookupres = yield self._find_user_id_and_pwd_hash(user_id)
|
|
||||||
if not lookupres:
|
|
||||||
defer.returnValue(None)
|
|
||||||
(user_id, password_hash) = lookupres
|
|
||||||
result = self.validate_hash(password, password_hash)
|
|
||||||
if not result:
|
|
||||||
logger.warn("Failed password login for user %s", user_id)
|
|
||||||
defer.returnValue(None)
|
|
||||||
defer.returnValue(user_id)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def issue_access_token(self, user_id, device_id=None):
|
def _check_ldap_password(self, user_id, password):
|
||||||
access_token = self.macaroon_gen.generate_access_token(user_id)
|
if self.ldap_enabled is not True:
|
||||||
yield self.store.add_access_token_to_user(user_id, access_token,
|
logger.debug("LDAP not configured")
|
||||||
device_id)
|
defer.returnValue(False)
|
||||||
|
|
||||||
|
import ldap
|
||||||
|
|
||||||
|
logger.info("Authenticating %s with LDAP" % user_id)
|
||||||
|
try:
|
||||||
|
ldap_url = "%s:%s" % (self.ldap_server, self.ldap_port)
|
||||||
|
logger.debug("Connecting LDAP server at %s" % ldap_url)
|
||||||
|
l = ldap.initialize(ldap_url)
|
||||||
|
if self.ldap_tls:
|
||||||
|
logger.debug("Initiating TLS")
|
||||||
|
self._connection.start_tls_s()
|
||||||
|
|
||||||
|
local_name = UserID.from_string(user_id).localpart
|
||||||
|
|
||||||
|
dn = "%s=%s, %s" % (
|
||||||
|
self.ldap_search_property,
|
||||||
|
local_name,
|
||||||
|
self.ldap_search_base)
|
||||||
|
logger.debug("DN for LDAP authentication: %s" % dn)
|
||||||
|
|
||||||
|
l.simple_bind_s(dn.encode('utf-8'), password.encode('utf-8'))
|
||||||
|
|
||||||
|
if not (yield self.does_user_exist(user_id)):
|
||||||
|
handler = self.hs.get_handlers().registration_handler
|
||||||
|
user_id, access_token = (
|
||||||
|
yield handler.register(localpart=local_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(True)
|
||||||
|
except ldap.LDAPError, e:
|
||||||
|
logger.warn("LDAP error: %s", e)
|
||||||
|
defer.returnValue(False)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def issue_access_token(self, user_id):
|
||||||
|
access_token = self.generate_access_token(user_id)
|
||||||
|
yield self.store.add_access_token_to_user(user_id, access_token)
|
||||||
defer.returnValue(access_token)
|
defer.returnValue(access_token)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def issue_refresh_token(self, user_id):
|
||||||
|
refresh_token = self.generate_refresh_token(user_id)
|
||||||
|
yield self.store.add_refresh_token_to_user(user_id, refresh_token)
|
||||||
|
defer.returnValue(refresh_token)
|
||||||
|
|
||||||
|
def generate_access_token(self, user_id, extra_caveats=None):
|
||||||
|
extra_caveats = extra_caveats or []
|
||||||
|
macaroon = self._generate_base_macaroon(user_id)
|
||||||
|
macaroon.add_first_party_caveat("type = access")
|
||||||
|
now = self.hs.get_clock().time_msec()
|
||||||
|
expiry = now + (60 * 60 * 1000)
|
||||||
|
macaroon.add_first_party_caveat("time < %d" % (expiry,))
|
||||||
|
for caveat in extra_caveats:
|
||||||
|
macaroon.add_first_party_caveat(caveat)
|
||||||
|
return macaroon.serialize()
|
||||||
|
|
||||||
|
def generate_refresh_token(self, user_id):
|
||||||
|
m = self._generate_base_macaroon(user_id)
|
||||||
|
m.add_first_party_caveat("type = refresh")
|
||||||
|
# Important to add a nonce, because otherwise every refresh token for a
|
||||||
|
# user will be the same.
|
||||||
|
m.add_first_party_caveat("nonce = %s" % (
|
||||||
|
stringutils.random_string_with_symbols(16),
|
||||||
|
))
|
||||||
|
return m.serialize()
|
||||||
|
|
||||||
|
def generate_short_term_login_token(self, user_id):
|
||||||
|
macaroon = self._generate_base_macaroon(user_id)
|
||||||
|
macaroon.add_first_party_caveat("type = login")
|
||||||
|
now = self.hs.get_clock().time_msec()
|
||||||
|
expiry = now + (2 * 60 * 1000)
|
||||||
|
macaroon.add_first_party_caveat("time < %d" % (expiry,))
|
||||||
|
return macaroon.serialize()
|
||||||
|
|
||||||
def validate_short_term_login_token_and_get_user_id(self, login_token):
|
def validate_short_term_login_token_and_get_user_id(self, login_token):
|
||||||
auth_api = self.hs.get_auth()
|
|
||||||
try:
|
try:
|
||||||
macaroon = pymacaroons.Macaroon.deserialize(login_token)
|
macaroon = pymacaroons.Macaroon.deserialize(login_token)
|
||||||
user_id = auth_api.get_user_id_from_macaroon(macaroon)
|
auth_api = self.hs.get_auth()
|
||||||
auth_api.validate_macaroon(macaroon, "login", True, user_id)
|
auth_api.validate_macaroon(macaroon, "login", True)
|
||||||
return user_id
|
return self.get_user_from_macaroon(macaroon)
|
||||||
except Exception:
|
except (pymacaroons.exceptions.MacaroonException, TypeError, ValueError):
|
||||||
raise AuthError(403, "Invalid token", errcode=Codes.FORBIDDEN)
|
raise AuthError(401, "Invalid token", errcode=Codes.UNKNOWN_TOKEN)
|
||||||
|
|
||||||
|
def _generate_base_macaroon(self, user_id):
|
||||||
|
macaroon = pymacaroons.Macaroon(
|
||||||
|
location=self.hs.config.server_name,
|
||||||
|
identifier="key",
|
||||||
|
key=self.hs.config.macaroon_secret_key)
|
||||||
|
macaroon.add_first_party_caveat("gen = 1")
|
||||||
|
macaroon.add_first_party_caveat("user_id = %s" % (user_id,))
|
||||||
|
return macaroon
|
||||||
|
|
||||||
|
def get_user_from_macaroon(self, macaroon):
|
||||||
|
user_prefix = "user_id = "
|
||||||
|
for caveat in macaroon.caveats:
|
||||||
|
if caveat.caveat_id.startswith(user_prefix):
|
||||||
|
return caveat.caveat_id[len(user_prefix):]
|
||||||
|
raise AuthError(
|
||||||
|
self.INVALID_TOKEN_HTTP_STATUS, "No user_id found in token",
|
||||||
|
errcode=Codes.UNKNOWN_TOKEN
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def set_password(self, user_id, newpassword, requester=None):
|
def set_password(self, user_id, newpassword, requester=None):
|
||||||
password_hash = self.hash(newpassword)
|
password_hash = self.hash(newpassword)
|
||||||
|
|
||||||
except_access_token_id = requester.access_token_id if requester else None
|
except_access_token_ids = [requester.access_token_id] if requester else []
|
||||||
|
|
||||||
try:
|
yield self.store.user_set_password_hash(user_id, password_hash)
|
||||||
yield self.store.user_set_password_hash(user_id, password_hash)
|
|
||||||
except StoreError as e:
|
|
||||||
if e.code == 404:
|
|
||||||
raise SynapseError(404, "Unknown user", Codes.NOT_FOUND)
|
|
||||||
raise e
|
|
||||||
yield self.store.user_delete_access_tokens(
|
yield self.store.user_delete_access_tokens(
|
||||||
user_id, except_access_token_id
|
user_id, except_access_token_ids
|
||||||
)
|
)
|
||||||
yield self.hs.get_pusherpool().remove_pushers_by_user(
|
yield self.hs.get_pusherpool().remove_pushers_by_user(
|
||||||
user_id, except_access_token_id
|
user_id, except_access_token_ids
|
||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def add_threepid(self, user_id, medium, address, validated_at):
|
def add_threepid(self, user_id, medium, address, validated_at):
|
||||||
# 'Canonicalise' email addresses down to lower case.
|
|
||||||
# We've now moving towards the Home Server being the entity that
|
|
||||||
# is responsible for validating threepids used for resetting passwords
|
|
||||||
# on accounts, so in future Synapse will gain knowledge of specific
|
|
||||||
# types (mediums) of threepid. For now, we still use the existing
|
|
||||||
# infrastructure, but this is the start of synapse gaining knowledge
|
|
||||||
# of specific types of threepid (and fixes the fact that checking
|
|
||||||
# for the presence of an email address during password reset was
|
|
||||||
# case sensitive).
|
|
||||||
if medium == 'email':
|
|
||||||
address = address.lower()
|
|
||||||
|
|
||||||
yield self.store.user_add_threepid(
|
yield self.store.user_add_threepid(
|
||||||
user_id, medium, address, validated_at,
|
user_id, medium, address, validated_at,
|
||||||
self.hs.get_clock().time_msec()
|
self.hs.get_clock().time_msec()
|
||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def delete_threepid(self, user_id, medium, address):
|
|
||||||
# 'Canonicalise' email addresses as per above
|
|
||||||
if medium == 'email':
|
|
||||||
address = address.lower()
|
|
||||||
|
|
||||||
ret = yield self.store.user_delete_threepid(
|
|
||||||
user_id, medium, address,
|
|
||||||
)
|
|
||||||
defer.returnValue(ret)
|
|
||||||
|
|
||||||
def _save_session(self, session):
|
def _save_session(self, session):
|
||||||
# TODO: Persistent storage
|
# TODO: Persistent storage
|
||||||
logger.debug("Saving session %s", session)
|
logger.debug("Saving session %s", session)
|
||||||
@@ -619,8 +596,7 @@ class AuthHandler(BaseHandler):
|
|||||||
Returns:
|
Returns:
|
||||||
Hashed password (str).
|
Hashed password (str).
|
||||||
"""
|
"""
|
||||||
return bcrypt.hashpw(password.encode('utf8') + self.hs.config.password_pepper,
|
return bcrypt.hashpw(password, bcrypt.gensalt(self.bcrypt_rounds))
|
||||||
bcrypt.gensalt(self.bcrypt_rounds))
|
|
||||||
|
|
||||||
def validate_hash(self, password, stored_hash):
|
def validate_hash(self, password, stored_hash):
|
||||||
"""Validates that self.hash(password) == stored_hash.
|
"""Validates that self.hash(password) == stored_hash.
|
||||||
@@ -632,77 +608,4 @@ class AuthHandler(BaseHandler):
|
|||||||
Returns:
|
Returns:
|
||||||
Whether self.hash(password) == stored_hash (bool).
|
Whether self.hash(password) == stored_hash (bool).
|
||||||
"""
|
"""
|
||||||
if stored_hash:
|
return bcrypt.hashpw(password, stored_hash) == stored_hash
|
||||||
return bcrypt.hashpw(password.encode('utf8') + self.hs.config.password_pepper,
|
|
||||||
stored_hash.encode('utf8')) == stored_hash
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class MacaroonGeneartor(object):
|
|
||||||
def __init__(self, hs):
|
|
||||||
self.clock = hs.get_clock()
|
|
||||||
self.server_name = hs.config.server_name
|
|
||||||
self.macaroon_secret_key = hs.config.macaroon_secret_key
|
|
||||||
|
|
||||||
def generate_access_token(self, user_id, extra_caveats=None):
|
|
||||||
extra_caveats = extra_caveats or []
|
|
||||||
macaroon = self._generate_base_macaroon(user_id)
|
|
||||||
macaroon.add_first_party_caveat("type = access")
|
|
||||||
# Include a nonce, to make sure that each login gets a different
|
|
||||||
# access token.
|
|
||||||
macaroon.add_first_party_caveat("nonce = %s" % (
|
|
||||||
stringutils.random_string_with_symbols(16),
|
|
||||||
))
|
|
||||||
for caveat in extra_caveats:
|
|
||||||
macaroon.add_first_party_caveat(caveat)
|
|
||||||
return macaroon.serialize()
|
|
||||||
|
|
||||||
def generate_short_term_login_token(self, user_id, duration_in_ms=(2 * 60 * 1000)):
|
|
||||||
macaroon = self._generate_base_macaroon(user_id)
|
|
||||||
macaroon.add_first_party_caveat("type = login")
|
|
||||||
now = self.clock.time_msec()
|
|
||||||
expiry = now + duration_in_ms
|
|
||||||
macaroon.add_first_party_caveat("time < %d" % (expiry,))
|
|
||||||
return macaroon.serialize()
|
|
||||||
|
|
||||||
def generate_delete_pusher_token(self, user_id):
|
|
||||||
macaroon = self._generate_base_macaroon(user_id)
|
|
||||||
macaroon.add_first_party_caveat("type = delete_pusher")
|
|
||||||
return macaroon.serialize()
|
|
||||||
|
|
||||||
def _generate_base_macaroon(self, user_id):
|
|
||||||
macaroon = pymacaroons.Macaroon(
|
|
||||||
location=self.server_name,
|
|
||||||
identifier="key",
|
|
||||||
key=self.macaroon_secret_key)
|
|
||||||
macaroon.add_first_party_caveat("gen = 1")
|
|
||||||
macaroon.add_first_party_caveat("user_id = %s" % (user_id,))
|
|
||||||
return macaroon
|
|
||||||
|
|
||||||
|
|
||||||
class _AccountHandler(object):
|
|
||||||
"""A proxy object that gets passed to password auth providers so they
|
|
||||||
can register new users etc if necessary.
|
|
||||||
"""
|
|
||||||
def __init__(self, hs, check_user_exists):
|
|
||||||
self.hs = hs
|
|
||||||
|
|
||||||
self._check_user_exists = check_user_exists
|
|
||||||
|
|
||||||
def check_user_exists(self, user_id):
|
|
||||||
"""Check if user exissts.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Deferred(bool)
|
|
||||||
"""
|
|
||||||
return self._check_user_exists(user_id)
|
|
||||||
|
|
||||||
def register(self, localpart):
|
|
||||||
"""Registers a new user with given localpart
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Deferred: a 2-tuple of (user_id, access_token)
|
|
||||||
"""
|
|
||||||
reg = self.hs.get_handlers().registration_handler
|
|
||||||
return reg.register(localpart=localpart)
|
|
||||||
|
|||||||
@@ -1,358 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2016 OpenMarket Ltd
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from synapse.api import errors
|
|
||||||
from synapse.api.constants import EventTypes
|
|
||||||
from synapse.util import stringutils
|
|
||||||
from synapse.util.async import Linearizer
|
|
||||||
from synapse.util.metrics import measure_func
|
|
||||||
from synapse.types import get_domain_from_id, RoomStreamToken
|
|
||||||
from twisted.internet import defer
|
|
||||||
from ._base import BaseHandler
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceHandler(BaseHandler):
|
|
||||||
def __init__(self, hs):
|
|
||||||
super(DeviceHandler, self).__init__(hs)
|
|
||||||
|
|
||||||
self.hs = hs
|
|
||||||
self.state = hs.get_state_handler()
|
|
||||||
self.federation_sender = hs.get_federation_sender()
|
|
||||||
self.federation = hs.get_replication_layer()
|
|
||||||
self._remote_edue_linearizer = Linearizer(name="remote_device_list")
|
|
||||||
|
|
||||||
self.federation.register_edu_handler(
|
|
||||||
"m.device_list_update", self._incoming_device_list_update,
|
|
||||||
)
|
|
||||||
self.federation.register_query_handler(
|
|
||||||
"user_devices", self.on_federation_query_user_devices,
|
|
||||||
)
|
|
||||||
|
|
||||||
hs.get_distributor().observe("user_left_room", self.user_left_room)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def check_device_registered(self, user_id, device_id,
|
|
||||||
initial_device_display_name=None):
|
|
||||||
"""
|
|
||||||
If the given device has not been registered, register it with the
|
|
||||||
supplied display name.
|
|
||||||
|
|
||||||
If no device_id is supplied, we make one up.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_id (str): @user:id
|
|
||||||
device_id (str | None): device id supplied by client
|
|
||||||
initial_device_display_name (str | None): device display name from
|
|
||||||
client
|
|
||||||
Returns:
|
|
||||||
str: device id (generated if none was supplied)
|
|
||||||
"""
|
|
||||||
if device_id is not None:
|
|
||||||
new_device = yield self.store.store_device(
|
|
||||||
user_id=user_id,
|
|
||||||
device_id=device_id,
|
|
||||||
initial_device_display_name=initial_device_display_name,
|
|
||||||
)
|
|
||||||
if new_device:
|
|
||||||
yield self.notify_device_update(user_id, [device_id])
|
|
||||||
defer.returnValue(device_id)
|
|
||||||
|
|
||||||
# if the device id is not specified, we'll autogen one, but loop a few
|
|
||||||
# times in case of a clash.
|
|
||||||
attempts = 0
|
|
||||||
while attempts < 5:
|
|
||||||
device_id = stringutils.random_string(10).upper()
|
|
||||||
new_device = yield self.store.store_device(
|
|
||||||
user_id=user_id,
|
|
||||||
device_id=device_id,
|
|
||||||
initial_device_display_name=initial_device_display_name,
|
|
||||||
)
|
|
||||||
if new_device:
|
|
||||||
yield self.notify_device_update(user_id, [device_id])
|
|
||||||
defer.returnValue(device_id)
|
|
||||||
attempts += 1
|
|
||||||
|
|
||||||
raise errors.StoreError(500, "Couldn't generate a device ID.")
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def get_devices_by_user(self, user_id):
|
|
||||||
"""
|
|
||||||
Retrieve the given user's devices
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_id (str):
|
|
||||||
Returns:
|
|
||||||
defer.Deferred: list[dict[str, X]]: info on each device
|
|
||||||
"""
|
|
||||||
|
|
||||||
device_map = yield self.store.get_devices_by_user(user_id)
|
|
||||||
|
|
||||||
ips = yield self.store.get_last_client_ip_by_device(
|
|
||||||
devices=((user_id, device_id) for device_id in device_map.keys())
|
|
||||||
)
|
|
||||||
|
|
||||||
devices = device_map.values()
|
|
||||||
for device in devices:
|
|
||||||
_update_device_from_client_ips(device, ips)
|
|
||||||
|
|
||||||
defer.returnValue(devices)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def get_device(self, user_id, device_id):
|
|
||||||
""" Retrieve the given device
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_id (str):
|
|
||||||
device_id (str):
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
defer.Deferred: dict[str, X]: info on the device
|
|
||||||
Raises:
|
|
||||||
errors.NotFoundError: if the device was not found
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
device = yield self.store.get_device(user_id, device_id)
|
|
||||||
except errors.StoreError:
|
|
||||||
raise errors.NotFoundError
|
|
||||||
ips = yield self.store.get_last_client_ip_by_device(
|
|
||||||
devices=((user_id, device_id),)
|
|
||||||
)
|
|
||||||
_update_device_from_client_ips(device, ips)
|
|
||||||
defer.returnValue(device)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def delete_device(self, user_id, device_id):
|
|
||||||
""" Delete the given device
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_id (str):
|
|
||||||
device_id (str):
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
defer.Deferred:
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
yield self.store.delete_device(user_id, device_id)
|
|
||||||
except errors.StoreError, e:
|
|
||||||
if e.code == 404:
|
|
||||||
# no match
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
yield self.store.user_delete_access_tokens(
|
|
||||||
user_id, device_id=device_id,
|
|
||||||
delete_refresh_tokens=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
yield self.store.delete_e2e_keys_by_device(
|
|
||||||
user_id=user_id, device_id=device_id
|
|
||||||
)
|
|
||||||
|
|
||||||
yield self.notify_device_update(user_id, [device_id])
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def update_device(self, user_id, device_id, content):
|
|
||||||
""" Update the given device
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_id (str):
|
|
||||||
device_id (str):
|
|
||||||
content (dict): body of update request
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
defer.Deferred:
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
yield self.store.update_device(
|
|
||||||
user_id,
|
|
||||||
device_id,
|
|
||||||
new_display_name=content.get("display_name")
|
|
||||||
)
|
|
||||||
yield self.notify_device_update(user_id, [device_id])
|
|
||||||
except errors.StoreError, e:
|
|
||||||
if e.code == 404:
|
|
||||||
raise errors.NotFoundError()
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
@measure_func("notify_device_update")
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def notify_device_update(self, user_id, device_ids):
|
|
||||||
"""Notify that a user's device(s) has changed. Pokes the notifier, and
|
|
||||||
remote servers if the user is local.
|
|
||||||
"""
|
|
||||||
users_who_share_room = yield self.store.get_users_who_share_room_with_user(
|
|
||||||
user_id
|
|
||||||
)
|
|
||||||
|
|
||||||
hosts = set()
|
|
||||||
if self.hs.is_mine_id(user_id):
|
|
||||||
hosts.update(get_domain_from_id(u) for u in users_who_share_room)
|
|
||||||
hosts.discard(self.server_name)
|
|
||||||
|
|
||||||
position = yield self.store.add_device_change_to_streams(
|
|
||||||
user_id, device_ids, list(hosts)
|
|
||||||
)
|
|
||||||
|
|
||||||
rooms = yield self.store.get_rooms_for_user(user_id)
|
|
||||||
room_ids = [r.room_id for r in rooms]
|
|
||||||
|
|
||||||
yield self.notifier.on_new_event(
|
|
||||||
"device_list_key", position, rooms=room_ids,
|
|
||||||
)
|
|
||||||
|
|
||||||
if hosts:
|
|
||||||
logger.info("Sending device list update notif to: %r", hosts)
|
|
||||||
for host in hosts:
|
|
||||||
self.federation_sender.send_device_messages(host)
|
|
||||||
|
|
||||||
@measure_func("device.get_user_ids_changed")
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def get_user_ids_changed(self, user_id, from_token):
|
|
||||||
"""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)
|
|
||||||
"""
|
|
||||||
rooms = yield self.store.get_rooms_for_user(user_id)
|
|
||||||
room_ids = set(r.room_id for r in rooms)
|
|
||||||
|
|
||||||
# First we check if any devices have changed
|
|
||||||
changed = yield self.store.get_user_whose_devices_changed(
|
|
||||||
from_token.device_list_key
|
|
||||||
)
|
|
||||||
|
|
||||||
# Then work out if any users have since joined
|
|
||||||
rooms_changed = self.store.get_rooms_that_changed(room_ids, from_token.room_key)
|
|
||||||
|
|
||||||
possibly_changed = set(changed)
|
|
||||||
for room_id in rooms_changed:
|
|
||||||
# Fetch the current state at the time.
|
|
||||||
stream_ordering = RoomStreamToken.parse_stream_token(from_token.room_key)
|
|
||||||
|
|
||||||
try:
|
|
||||||
event_ids = yield self.store.get_forward_extremeties_for_room(
|
|
||||||
room_id, stream_ordering=stream_ordering
|
|
||||||
)
|
|
||||||
prev_state_ids = yield self.store.get_state_ids_for_events(event_ids)
|
|
||||||
except:
|
|
||||||
prev_state_ids = {}
|
|
||||||
|
|
||||||
current_state_ids = yield self.state.get_current_state_ids(room_id)
|
|
||||||
|
|
||||||
# If there has been any change in membership, include them in the
|
|
||||||
# possibly changed list. We'll check if they are joined below,
|
|
||||||
# and we're not toooo worried about spuriously adding users.
|
|
||||||
for key, event_id in current_state_ids.iteritems():
|
|
||||||
etype, state_key = key
|
|
||||||
if etype == EventTypes.Member:
|
|
||||||
prev_event_id = prev_state_ids.get(key, None)
|
|
||||||
if not prev_event_id or prev_event_id != event_id:
|
|
||||||
possibly_changed.add(state_key)
|
|
||||||
|
|
||||||
users_who_share_room = yield self.store.get_users_who_share_room_with_user(
|
|
||||||
user_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# Take the intersection of the users whose devices may have changed
|
|
||||||
# and those that actually still share a room with the user
|
|
||||||
defer.returnValue(users_who_share_room & possibly_changed)
|
|
||||||
|
|
||||||
@measure_func("_incoming_device_list_update")
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _incoming_device_list_update(self, origin, edu_content):
|
|
||||||
user_id = edu_content["user_id"]
|
|
||||||
device_id = edu_content["device_id"]
|
|
||||||
stream_id = edu_content["stream_id"]
|
|
||||||
prev_ids = edu_content.get("prev_id", [])
|
|
||||||
|
|
||||||
if get_domain_from_id(user_id) != origin:
|
|
||||||
# TODO: Raise?
|
|
||||||
logger.warning("Got device list update edu for %r from %r", user_id, origin)
|
|
||||||
return
|
|
||||||
|
|
||||||
rooms = yield self.store.get_rooms_for_user(user_id)
|
|
||||||
if not rooms:
|
|
||||||
# We don't share any rooms with this user. Ignore update, as we
|
|
||||||
# probably won't get any further updates.
|
|
||||||
return
|
|
||||||
|
|
||||||
with (yield self._remote_edue_linearizer.queue(user_id)):
|
|
||||||
# If the prev id matches whats in our cache table, then we don't need
|
|
||||||
# to resync the users device list, otherwise we do.
|
|
||||||
resync = True
|
|
||||||
if len(prev_ids) == 1:
|
|
||||||
extremity = yield self.store.get_device_list_last_stream_id_for_remote(
|
|
||||||
user_id
|
|
||||||
)
|
|
||||||
logger.info("Extrem: %r, prev_ids: %r", extremity, prev_ids)
|
|
||||||
if str(extremity) == str(prev_ids[0]):
|
|
||||||
resync = False
|
|
||||||
|
|
||||||
if resync:
|
|
||||||
# Fetch all devices for the user.
|
|
||||||
result = yield self.federation.query_user_devices(origin, user_id)
|
|
||||||
stream_id = result["stream_id"]
|
|
||||||
devices = result["devices"]
|
|
||||||
yield self.store.update_remote_device_list_cache(
|
|
||||||
user_id, devices, stream_id,
|
|
||||||
)
|
|
||||||
device_ids = [device["device_id"] for device in devices]
|
|
||||||
yield self.notify_device_update(user_id, device_ids)
|
|
||||||
else:
|
|
||||||
# Simply update the single device, since we know that is the only
|
|
||||||
# change (becuase of the single prev_id matching the current cache)
|
|
||||||
content = dict(edu_content)
|
|
||||||
for key in ("user_id", "device_id", "stream_id", "prev_ids"):
|
|
||||||
content.pop(key, None)
|
|
||||||
yield self.store.update_remote_device_list_cache_entry(
|
|
||||||
user_id, device_id, content, stream_id,
|
|
||||||
)
|
|
||||||
yield self.notify_device_update(user_id, [device_id])
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def on_federation_query_user_devices(self, user_id):
|
|
||||||
stream_id, devices = yield self.store.get_devices_with_keys_by_user(user_id)
|
|
||||||
defer.returnValue({
|
|
||||||
"user_id": user_id,
|
|
||||||
"stream_id": stream_id,
|
|
||||||
"devices": devices,
|
|
||||||
})
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def user_left_room(self, user, room_id):
|
|
||||||
user_id = user.to_string()
|
|
||||||
rooms = yield self.store.get_rooms_for_user(user_id)
|
|
||||||
if not rooms:
|
|
||||||
# We no longer share rooms with this user, so we'll no longer
|
|
||||||
# receive device updates. Mark this in DB.
|
|
||||||
yield self.store.mark_remote_user_device_list_as_unsubscribed(user_id)
|
|
||||||
|
|
||||||
|
|
||||||
def _update_device_from_client_ips(device, client_ips):
|
|
||||||
ip = client_ips.get((device["user_id"], device["device_id"]), {})
|
|
||||||
device.update({
|
|
||||||
"last_seen_ts": ip.get("last_seen"),
|
|
||||||
"last_seen_ip": ip.get("ip"),
|
|
||||||
})
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2016 OpenMarket Ltd
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from twisted.internet import defer
|
|
||||||
|
|
||||||
from synapse.types import get_domain_from_id
|
|
||||||
from synapse.util.stringutils import random_string
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceMessageHandler(object):
|
|
||||||
|
|
||||||
def __init__(self, hs):
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
hs (synapse.server.HomeServer): server
|
|
||||||
"""
|
|
||||||
self.store = hs.get_datastore()
|
|
||||||
self.notifier = hs.get_notifier()
|
|
||||||
self.is_mine_id = hs.is_mine_id
|
|
||||||
self.federation = hs.get_federation_sender()
|
|
||||||
|
|
||||||
hs.get_replication_layer().register_edu_handler(
|
|
||||||
"m.direct_to_device", self.on_direct_to_device_edu
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def on_direct_to_device_edu(self, origin, content):
|
|
||||||
local_messages = {}
|
|
||||||
sender_user_id = content["sender"]
|
|
||||||
if origin != get_domain_from_id(sender_user_id):
|
|
||||||
logger.warn(
|
|
||||||
"Dropping device message from %r with spoofed sender %r",
|
|
||||||
origin, sender_user_id
|
|
||||||
)
|
|
||||||
message_type = content["type"]
|
|
||||||
message_id = content["message_id"]
|
|
||||||
for user_id, by_device in content["messages"].items():
|
|
||||||
messages_by_device = {
|
|
||||||
device_id: {
|
|
||||||
"content": message_content,
|
|
||||||
"type": message_type,
|
|
||||||
"sender": sender_user_id,
|
|
||||||
}
|
|
||||||
for device_id, message_content in by_device.items()
|
|
||||||
}
|
|
||||||
if messages_by_device:
|
|
||||||
local_messages[user_id] = messages_by_device
|
|
||||||
|
|
||||||
stream_id = yield self.store.add_messages_from_remote_to_device_inbox(
|
|
||||||
origin, message_id, local_messages
|
|
||||||
)
|
|
||||||
|
|
||||||
self.notifier.on_new_event(
|
|
||||||
"to_device_key", stream_id, users=local_messages.keys()
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def send_device_message(self, sender_user_id, message_type, messages):
|
|
||||||
|
|
||||||
local_messages = {}
|
|
||||||
remote_messages = {}
|
|
||||||
for user_id, by_device in messages.items():
|
|
||||||
if self.is_mine_id(user_id):
|
|
||||||
messages_by_device = {
|
|
||||||
device_id: {
|
|
||||||
"content": message_content,
|
|
||||||
"type": message_type,
|
|
||||||
"sender": sender_user_id,
|
|
||||||
}
|
|
||||||
for device_id, message_content in by_device.items()
|
|
||||||
}
|
|
||||||
if messages_by_device:
|
|
||||||
local_messages[user_id] = messages_by_device
|
|
||||||
else:
|
|
||||||
destination = get_domain_from_id(user_id)
|
|
||||||
remote_messages.setdefault(destination, {})[user_id] = by_device
|
|
||||||
|
|
||||||
message_id = random_string(16)
|
|
||||||
|
|
||||||
remote_edu_contents = {}
|
|
||||||
for destination, messages in remote_messages.items():
|
|
||||||
remote_edu_contents[destination] = {
|
|
||||||
"messages": messages,
|
|
||||||
"sender": sender_user_id,
|
|
||||||
"type": message_type,
|
|
||||||
"message_id": message_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
stream_id = yield self.store.add_messages_to_device_inbox(
|
|
||||||
local_messages, remote_edu_contents
|
|
||||||
)
|
|
||||||
|
|
||||||
self.notifier.on_new_event(
|
|
||||||
"to_device_key", stream_id, users=local_messages.keys()
|
|
||||||
)
|
|
||||||
|
|
||||||
for destination in remote_messages.keys():
|
|
||||||
# Enqueue a new federation transaction to send the new
|
|
||||||
# device messages to each remote destination.
|
|
||||||
self.federation.send_device_messages(destination)
|
|
||||||
@@ -19,7 +19,7 @@ from ._base import BaseHandler
|
|||||||
|
|
||||||
from synapse.api.errors import SynapseError, Codes, CodeMessageException, AuthError
|
from synapse.api.errors import SynapseError, Codes, CodeMessageException, AuthError
|
||||||
from synapse.api.constants import EventTypes
|
from synapse.api.constants import EventTypes
|
||||||
from synapse.types import RoomAlias, UserID, get_domain_from_id
|
from synapse.types import RoomAlias, UserID
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import string
|
import string
|
||||||
@@ -33,7 +33,6 @@ class DirectoryHandler(BaseHandler):
|
|||||||
super(DirectoryHandler, self).__init__(hs)
|
super(DirectoryHandler, self).__init__(hs)
|
||||||
|
|
||||||
self.state = hs.get_state_handler()
|
self.state = hs.get_state_handler()
|
||||||
self.appservice_handler = hs.get_application_service_handler()
|
|
||||||
|
|
||||||
self.federation = hs.get_replication_layer()
|
self.federation = hs.get_replication_layer()
|
||||||
self.federation.register_query_handler(
|
self.federation.register_query_handler(
|
||||||
@@ -55,8 +54,7 @@ class DirectoryHandler(BaseHandler):
|
|||||||
# TODO(erikj): Add transactions.
|
# TODO(erikj): Add transactions.
|
||||||
# TODO(erikj): Check if there is a current association.
|
# TODO(erikj): Check if there is a current association.
|
||||||
if not servers:
|
if not servers:
|
||||||
users = yield self.state.get_current_user_in_room(room_id)
|
servers = yield self.store.get_joined_hosts_for_room(room_id)
|
||||||
servers = set(get_domain_from_id(u) for u in users)
|
|
||||||
|
|
||||||
if not servers:
|
if not servers:
|
||||||
raise SynapseError(400, "Failed to get server list")
|
raise SynapseError(400, "Failed to get server list")
|
||||||
@@ -194,8 +192,7 @@ class DirectoryHandler(BaseHandler):
|
|||||||
Codes.NOT_FOUND
|
Codes.NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
users = yield self.state.get_current_user_in_room(room_id)
|
extra_servers = yield self.store.get_joined_hosts_for_room(room_id)
|
||||||
extra_servers = set(get_domain_from_id(u) for u in users)
|
|
||||||
servers = set(extra_servers) | set(servers)
|
servers = set(extra_servers) | set(servers)
|
||||||
|
|
||||||
# If this server is in the list of servers, return it first.
|
# If this server is in the list of servers, return it first.
|
||||||
@@ -284,16 +281,17 @@ class DirectoryHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
if not result:
|
if not result:
|
||||||
# Query AS to see if it exists
|
# Query AS to see if it exists
|
||||||
as_handler = self.appservice_handler
|
as_handler = self.hs.get_handlers().appservice_handler
|
||||||
result = yield as_handler.query_room_alias_exists(room_alias)
|
result = yield as_handler.query_room_alias_exists(room_alias)
|
||||||
defer.returnValue(result)
|
defer.returnValue(result)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
def can_modify_alias(self, alias, user_id=None):
|
def can_modify_alias(self, alias, user_id=None):
|
||||||
# Any application service "interested" in an alias they are regexing on
|
# Any application service "interested" in an alias they are regexing on
|
||||||
# can modify the alias.
|
# can modify the alias.
|
||||||
# Users can only modify the alias if ALL the interested services have
|
# Users can only modify the alias if ALL the interested services have
|
||||||
# non-exclusive locks on the alias (or there are no interested services)
|
# non-exclusive locks on the alias (or there are no interested services)
|
||||||
services = self.store.get_app_services()
|
services = yield self.store.get_app_services()
|
||||||
interested_services = [
|
interested_services = [
|
||||||
s for s in services if s.is_interested_in_alias(alias.to_string())
|
s for s in services if s.is_interested_in_alias(alias.to_string())
|
||||||
]
|
]
|
||||||
@@ -301,12 +299,14 @@ class DirectoryHandler(BaseHandler):
|
|||||||
for service in interested_services:
|
for service in interested_services:
|
||||||
if user_id == service.sender:
|
if user_id == service.sender:
|
||||||
# this user IS the app service so they can do whatever they like
|
# this user IS the app service so they can do whatever they like
|
||||||
return defer.succeed(True)
|
defer.returnValue(True)
|
||||||
|
return
|
||||||
elif service.is_exclusive_alias(alias.to_string()):
|
elif service.is_exclusive_alias(alias.to_string()):
|
||||||
# another service has an exclusive lock on this alias.
|
# another service has an exclusive lock on this alias.
|
||||||
return defer.succeed(False)
|
defer.returnValue(False)
|
||||||
|
return
|
||||||
# either no interested services, or no service with an exclusive lock
|
# either no interested services, or no service with an exclusive lock
|
||||||
return defer.succeed(True)
|
defer.returnValue(True)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _user_can_delete_alias(self, alias, user_id):
|
def _user_can_delete_alias(self, alias, user_id):
|
||||||
@@ -339,22 +339,3 @@ class DirectoryHandler(BaseHandler):
|
|||||||
yield self.auth.check_can_change_room_list(room_id, requester.user)
|
yield self.auth.check_can_change_room_list(room_id, requester.user)
|
||||||
|
|
||||||
yield self.store.set_room_is_public(room_id, visibility == "public")
|
yield self.store.set_room_is_public(room_id, visibility == "public")
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def edit_published_appservice_room_list(self, appservice_id, network_id,
|
|
||||||
room_id, visibility):
|
|
||||||
"""Add or remove a room from the appservice/network specific public
|
|
||||||
room list.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
appservice_id (str): ID of the appservice that owns the list
|
|
||||||
network_id (str): The ID of the network the list is associated with
|
|
||||||
room_id (str)
|
|
||||||
visibility (str): either "public" or "private"
|
|
||||||
"""
|
|
||||||
if visibility not in ["public", "private"]:
|
|
||||||
raise SynapseError(400, "Invalid visibility setting")
|
|
||||||
|
|
||||||
yield self.store.set_room_is_public_appservice(
|
|
||||||
room_id, appservice_id, network_id, visibility == "public"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,323 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2016 OpenMarket Ltd
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import ujson as json
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from canonicaljson import encode_canonical_json
|
|
||||||
from twisted.internet import defer
|
|
||||||
|
|
||||||
from synapse.api.errors import SynapseError, CodeMessageException
|
|
||||||
from synapse.types import get_domain_from_id
|
|
||||||
from synapse.util.logcontext import preserve_fn, preserve_context_over_deferred
|
|
||||||
from synapse.util.retryutils import get_retry_limiter, NotRetryingDestination
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class E2eKeysHandler(object):
|
|
||||||
def __init__(self, hs):
|
|
||||||
self.store = hs.get_datastore()
|
|
||||||
self.federation = hs.get_replication_layer()
|
|
||||||
self.device_handler = hs.get_device_handler()
|
|
||||||
self.is_mine_id = hs.is_mine_id
|
|
||||||
self.clock = hs.get_clock()
|
|
||||||
|
|
||||||
# doesn't really work as part of the generic query API, because the
|
|
||||||
# query request requires an object POST, but we abuse the
|
|
||||||
# "query handler" interface.
|
|
||||||
self.federation.register_query_handler(
|
|
||||||
"client_keys", self.on_federation_query_client_keys
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def query_devices(self, query_body, timeout):
|
|
||||||
""" Handle a device key query from a client
|
|
||||||
|
|
||||||
{
|
|
||||||
"device_keys": {
|
|
||||||
"<user_id>": ["<device_id>"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
->
|
|
||||||
{
|
|
||||||
"device_keys": {
|
|
||||||
"<user_id>": {
|
|
||||||
"<device_id>": {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
device_keys_query = query_body.get("device_keys", {})
|
|
||||||
|
|
||||||
# separate users by domain.
|
|
||||||
# make a map from domain to user_id to device_ids
|
|
||||||
local_query = {}
|
|
||||||
remote_queries = {}
|
|
||||||
|
|
||||||
for user_id, device_ids in device_keys_query.items():
|
|
||||||
if self.is_mine_id(user_id):
|
|
||||||
local_query[user_id] = device_ids
|
|
||||||
else:
|
|
||||||
remote_queries[user_id] = device_ids
|
|
||||||
|
|
||||||
# Firt get local devices.
|
|
||||||
failures = {}
|
|
||||||
results = {}
|
|
||||||
if local_query:
|
|
||||||
local_result = yield self.query_local_devices(local_query)
|
|
||||||
for user_id, keys in local_result.items():
|
|
||||||
if user_id in local_query:
|
|
||||||
results[user_id] = keys
|
|
||||||
|
|
||||||
# Now attempt to get any remote devices from our local cache.
|
|
||||||
remote_queries_not_in_cache = {}
|
|
||||||
if remote_queries:
|
|
||||||
query_list = []
|
|
||||||
for user_id, device_ids in remote_queries.iteritems():
|
|
||||||
if device_ids:
|
|
||||||
query_list.extend((user_id, device_id) for device_id in device_ids)
|
|
||||||
else:
|
|
||||||
query_list.append((user_id, None))
|
|
||||||
|
|
||||||
user_ids_not_in_cache, remote_results = (
|
|
||||||
yield self.store.get_user_devices_from_cache(
|
|
||||||
query_list
|
|
||||||
)
|
|
||||||
)
|
|
||||||
for user_id, devices in remote_results.iteritems():
|
|
||||||
user_devices = results.setdefault(user_id, {})
|
|
||||||
for device_id, device in devices.iteritems():
|
|
||||||
keys = device.get("keys", None)
|
|
||||||
device_display_name = device.get("device_display_name", None)
|
|
||||||
if keys:
|
|
||||||
result = dict(keys)
|
|
||||||
unsigned = result.setdefault("unsigned", {})
|
|
||||||
if device_display_name:
|
|
||||||
unsigned["device_display_name"] = device_display_name
|
|
||||||
user_devices[device_id] = result
|
|
||||||
|
|
||||||
for user_id in user_ids_not_in_cache:
|
|
||||||
domain = get_domain_from_id(user_id)
|
|
||||||
r = remote_queries_not_in_cache.setdefault(domain, {})
|
|
||||||
r[user_id] = remote_queries[user_id]
|
|
||||||
|
|
||||||
# Now fetch any devices that we don't have in our cache
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def do_remote_query(destination):
|
|
||||||
destination_query = remote_queries_not_in_cache[destination]
|
|
||||||
try:
|
|
||||||
limiter = yield get_retry_limiter(
|
|
||||||
destination, self.clock, self.store
|
|
||||||
)
|
|
||||||
with limiter:
|
|
||||||
remote_result = yield self.federation.query_client_keys(
|
|
||||||
destination,
|
|
||||||
{"device_keys": destination_query},
|
|
||||||
timeout=timeout
|
|
||||||
)
|
|
||||||
|
|
||||||
for user_id, keys in remote_result["device_keys"].items():
|
|
||||||
if user_id in destination_query:
|
|
||||||
results[user_id] = keys
|
|
||||||
|
|
||||||
except CodeMessageException as e:
|
|
||||||
failures[destination] = {
|
|
||||||
"status": e.code, "message": e.message
|
|
||||||
}
|
|
||||||
except NotRetryingDestination as e:
|
|
||||||
failures[destination] = {
|
|
||||||
"status": 503, "message": "Not ready for retry",
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
# include ConnectionRefused and other errors
|
|
||||||
failures[destination] = {
|
|
||||||
"status": 503, "message": e.message
|
|
||||||
}
|
|
||||||
|
|
||||||
yield preserve_context_over_deferred(defer.gatherResults([
|
|
||||||
preserve_fn(do_remote_query)(destination)
|
|
||||||
for destination in remote_queries_not_in_cache
|
|
||||||
]))
|
|
||||||
|
|
||||||
defer.returnValue({
|
|
||||||
"device_keys": results, "failures": failures,
|
|
||||||
})
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def query_local_devices(self, query):
|
|
||||||
"""Get E2E device keys for local users
|
|
||||||
|
|
||||||
Args:
|
|
||||||
query (dict[string, list[string]|None): map from user_id to a list
|
|
||||||
of devices to query (None for all devices)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
defer.Deferred: (resolves to dict[string, dict[string, dict]]):
|
|
||||||
map from user_id -> device_id -> device details
|
|
||||||
"""
|
|
||||||
local_query = []
|
|
||||||
|
|
||||||
result_dict = {}
|
|
||||||
for user_id, device_ids in query.items():
|
|
||||||
if not self.is_mine_id(user_id):
|
|
||||||
logger.warning("Request for keys for non-local user %s",
|
|
||||||
user_id)
|
|
||||||
raise SynapseError(400, "Not a user here")
|
|
||||||
|
|
||||||
if not device_ids:
|
|
||||||
local_query.append((user_id, None))
|
|
||||||
else:
|
|
||||||
for device_id in device_ids:
|
|
||||||
local_query.append((user_id, device_id))
|
|
||||||
|
|
||||||
# make sure that each queried user appears in the result dict
|
|
||||||
result_dict[user_id] = {}
|
|
||||||
|
|
||||||
results = yield self.store.get_e2e_device_keys(local_query)
|
|
||||||
|
|
||||||
# Build the result structure, un-jsonify the results, and add the
|
|
||||||
# "unsigned" section
|
|
||||||
for user_id, device_keys in results.items():
|
|
||||||
for device_id, device_info in device_keys.items():
|
|
||||||
r = dict(device_info["keys"])
|
|
||||||
r["unsigned"] = {}
|
|
||||||
display_name = device_info["device_display_name"]
|
|
||||||
if display_name is not None:
|
|
||||||
r["unsigned"]["device_display_name"] = display_name
|
|
||||||
result_dict[user_id][device_id] = r
|
|
||||||
|
|
||||||
defer.returnValue(result_dict)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def on_federation_query_client_keys(self, query_body):
|
|
||||||
""" Handle a device key query from a federated server
|
|
||||||
"""
|
|
||||||
device_keys_query = query_body.get("device_keys", {})
|
|
||||||
res = yield self.query_local_devices(device_keys_query)
|
|
||||||
defer.returnValue({"device_keys": res})
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def claim_one_time_keys(self, query, timeout):
|
|
||||||
local_query = []
|
|
||||||
remote_queries = {}
|
|
||||||
|
|
||||||
for user_id, device_keys in query.get("one_time_keys", {}).items():
|
|
||||||
if self.is_mine_id(user_id):
|
|
||||||
for device_id, algorithm in device_keys.items():
|
|
||||||
local_query.append((user_id, device_id, algorithm))
|
|
||||||
else:
|
|
||||||
domain = get_domain_from_id(user_id)
|
|
||||||
remote_queries.setdefault(domain, {})[user_id] = device_keys
|
|
||||||
|
|
||||||
results = yield self.store.claim_e2e_one_time_keys(local_query)
|
|
||||||
|
|
||||||
json_result = {}
|
|
||||||
failures = {}
|
|
||||||
for user_id, device_keys in results.items():
|
|
||||||
for device_id, keys in device_keys.items():
|
|
||||||
for key_id, json_bytes in keys.items():
|
|
||||||
json_result.setdefault(user_id, {})[device_id] = {
|
|
||||||
key_id: json.loads(json_bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def claim_client_keys(destination):
|
|
||||||
device_keys = remote_queries[destination]
|
|
||||||
try:
|
|
||||||
limiter = yield get_retry_limiter(
|
|
||||||
destination, self.clock, self.store
|
|
||||||
)
|
|
||||||
with limiter:
|
|
||||||
remote_result = yield self.federation.claim_client_keys(
|
|
||||||
destination,
|
|
||||||
{"one_time_keys": device_keys},
|
|
||||||
timeout=timeout
|
|
||||||
)
|
|
||||||
for user_id, keys in remote_result["one_time_keys"].items():
|
|
||||||
if user_id in device_keys:
|
|
||||||
json_result[user_id] = keys
|
|
||||||
except CodeMessageException as e:
|
|
||||||
failures[destination] = {
|
|
||||||
"status": e.code, "message": e.message
|
|
||||||
}
|
|
||||||
except NotRetryingDestination as e:
|
|
||||||
failures[destination] = {
|
|
||||||
"status": 503, "message": "Not ready for retry",
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
# include ConnectionRefused and other errors
|
|
||||||
failures[destination] = {
|
|
||||||
"status": 503, "message": e.message
|
|
||||||
}
|
|
||||||
|
|
||||||
yield preserve_context_over_deferred(defer.gatherResults([
|
|
||||||
preserve_fn(claim_client_keys)(destination)
|
|
||||||
for destination in remote_queries
|
|
||||||
]))
|
|
||||||
|
|
||||||
defer.returnValue({
|
|
||||||
"one_time_keys": json_result,
|
|
||||||
"failures": failures
|
|
||||||
})
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def upload_keys_for_user(self, user_id, device_id, keys):
|
|
||||||
time_now = self.clock.time_msec()
|
|
||||||
|
|
||||||
# TODO: Validate the JSON to make sure it has the right keys.
|
|
||||||
device_keys = keys.get("device_keys", None)
|
|
||||||
if device_keys:
|
|
||||||
logger.info(
|
|
||||||
"Updating device_keys for device %r for user %s at %d",
|
|
||||||
device_id, user_id, time_now
|
|
||||||
)
|
|
||||||
# TODO: Sign the JSON with the server key
|
|
||||||
changed = yield self.store.set_e2e_device_keys(
|
|
||||||
user_id, device_id, time_now, device_keys,
|
|
||||||
)
|
|
||||||
if changed:
|
|
||||||
# Only notify about device updates *if* the keys actually changed
|
|
||||||
yield self.device_handler.notify_device_update(user_id, [device_id])
|
|
||||||
|
|
||||||
one_time_keys = keys.get("one_time_keys", None)
|
|
||||||
if one_time_keys:
|
|
||||||
logger.info(
|
|
||||||
"Adding %d one_time_keys for device %r for user %r at %d",
|
|
||||||
len(one_time_keys), device_id, user_id, time_now
|
|
||||||
)
|
|
||||||
key_list = []
|
|
||||||
for key_id, key_json in one_time_keys.items():
|
|
||||||
algorithm, key_id = key_id.split(":")
|
|
||||||
key_list.append((
|
|
||||||
algorithm, key_id, encode_canonical_json(key_json)
|
|
||||||
))
|
|
||||||
|
|
||||||
yield self.store.add_e2e_one_time_keys(
|
|
||||||
user_id, device_id, time_now, key_list
|
|
||||||
)
|
|
||||||
|
|
||||||
# the device should have been registered already, but it may have been
|
|
||||||
# deleted due to a race with a DELETE request. Or we may be using an
|
|
||||||
# old access_token without an associated device_id. Either way, we
|
|
||||||
# need to double-check the device is registered to avoid ending up with
|
|
||||||
# keys without a corresponding device.
|
|
||||||
self.device_handler.check_device_registered(user_id, device_id)
|
|
||||||
|
|
||||||
result = yield self.store.count_e2e_one_time_keys(user_id, device_id)
|
|
||||||
|
|
||||||
defer.returnValue({"one_time_key_counts": result})
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user