Compare commits

...

730 Commits

Author SHA1 Message Date
Mark Haines
f35f8d06ea Merge remote-tracking branch 'origin/release-v0.12.0' 2016-01-04 14:02:50 +00:00
Mark Haines
d2709a5389 Bump changelog and version for v0.12.0 2016-01-04 13:57:39 +00:00
Richard van der Hoff
34c09f33da Update CHANGES 2016-01-04 12:45:58 +00:00
Richard van der Hoff
cf3282d103 Merge pull request #459 from matrix-org/rav/fix_r0_login
Expose /login under r0
2016-01-04 12:41:53 +00:00
Richard van der Hoff
32d9fd0b26 Expose /login under r0
The spec says /login should be available at r0 and 'unstable', so make it so.
2016-01-02 17:24:28 +00:00
Mark Haines
c6e79c84de Bump version and update changelog for v0.12.0-rc3 2015-12-23 16:15:54 +00:00
Mark Haines
8d6dde7825 Merge pull request #457 from matrix-org/markjh/cached_sync
Add a cache for initialSync responses that expires after 5 minutes
2015-12-23 16:04:52 +00:00
Mark Haines
d12c00bdc3 Add some docstring explaining the snapshot cache does 2015-12-23 15:18:11 +00:00
Mark Haines
ba39d3d5d7 Merge pull request #458 from matrix-org/markjh/guest_auth
Missing yield on guest access auth check

Needs matrix-org/sytest#125 to land first
2015-12-23 14:10:09 +00:00
Mark Haines
f3948e001f Missing yield on guest access auth check
Needs matrix-org/sytest#125 to land first
2015-12-23 14:10:06 +00:00
Mark Haines
7fa71e3267 Add a unit test for the snapshot cache 2015-12-23 11:48:03 +00:00
Mark Haines
517fb9a023 Move the doc string to the public facing method 2015-12-22 18:53:47 +00:00
Mark Haines
9ac417fa88 Add a cache for initialSync responses that expires after 5 minutes 2015-12-22 18:27:56 +00:00
Mark Haines
7df276d219 Merge pull request #455 from matrix-org/markjh/guest_access
Allow guest access to /sync
2015-12-22 16:09:23 +00:00
Mark Haines
0ee0138325 Include the list of bad room ids in the error 2015-12-22 15:49:32 +00:00
Mark Haines
251aafccca Use a list comprehension 2015-12-22 14:03:24 +00:00
Mark Haines
c058625959 Merge remote-tracking branch 'origin/develop' into markjh/guest_access
Conflicts:
	synapse/api/filtering.py
2015-12-22 13:58:18 +00:00
Mark Haines
cdd04f7055 Hook up read receipts and typing notifications for guest access 2015-12-22 11:59:55 +00:00
Mark Haines
542ab0f886 Merge branch 'develop' into markjh/guest_access 2015-12-22 11:49:12 +00:00
Mark Haines
e525b46f12 Merge pull request #454 from matrix-org/markjh/room_filtering
Add top level filters for filtering by room id

Documented by matrix-org/matrix-doc#246
2015-12-22 11:40:35 +00:00
Mark Haines
b9b4466d0d Add top level filters for filtering by room id
Documented by matrix-org/matrix-doc#246
2015-12-22 11:40:32 +00:00
Mark Haines
c3fff251a9 Allow guest access to /sync 2015-12-22 11:21:03 +00:00
Mark Haines
45a9e0ae0c Allow guest access if the user provides a list of rooms in the filter 2015-12-22 10:25:46 +00:00
Mark Haines
489a4cd1cf Add top level filtering by room id 2015-12-21 21:10:41 +00:00
Mark Haines
2a2b2ef834 Remove bogus comment about branch coverage 2015-12-21 20:21:52 +00:00
Daniel Wagner-Hall
2e2eeb43a6 Merge pull request #453 from matrix-org/daniel/avatarurls
Return room avatar URLs in /publicRooms

Spec: https://github.com/matrix-org/matrix-doc/pull/244
Tests: https://github.com/matrix-org/sytest/pull/121
2015-12-21 20:38:16 +01:00
Daniel Wagner-Hall
7f3148865c Return room avatar URLs in /publicRooms
Spec: https://github.com/matrix-org/matrix-doc/pull/244
Tests: https://github.com/matrix-org/sytest/pull/121
2015-12-21 19:38:04 +00:00
Mark Haines
bb9c7f2dd9 Delete all the .coverage files, including the combined .coverage 2015-12-21 17:51:57 +00:00
Mark Haines
9036d2d6a8 Use an absolute path when specifying the directory for synapse in jenkins.sh 2015-12-21 17:15:05 +00:00
Mark Haines
64b6606824 Remove accidentally committed debug logging 2015-12-21 15:22:03 +00:00
Mark Haines
42a7a09eea Merge pull request #452 from matrix-org/paul/SYN-558
Actually look up required remote server key IDs
2015-12-21 11:01:56 +00:00
Paul "LeoNerd" Evans
a6ba41e078 Actually look up required remote server key IDs
set.union() is a side-effect-free function that returns the union of two
sets. This clearly wanted .update(), which is the side-effecting mutator
version.
2015-12-18 21:36:42 +00:00
Mark Haines
f85949bde0 Merge pull request #451 from matrix-org/markjh/branch_coverage
Generate code coverage report when running jenkins.sh
2015-12-18 20:44:50 +00:00
Mark Haines
2f871ad143 Generate code coverage report when running jenkins.sh 2015-12-18 20:44:47 +00:00
Matthew Hodgson
c8ea2d5b1f Merge pull request #450 from matrix-org/matthew/no-identicons
Matthew/no identicons
2015-12-18 18:14:06 +00:00
David Baker
0a2d73fd60 Merge branch 'release-v0.12.0' into develop 2015-12-18 10:07:48 +00:00
David Baker
ce4999268a Fix typo that broke registration on the mobile clients 2015-12-18 10:07:28 +00:00
Daniel Wagner-Hall
633ceb9bb1 Merge pull request #449 from matrix-org/daniel/3pid
Add display_name to 3pid invite in m.room.member invites
2015-12-18 09:55:59 +01:00
Matthew Hodgson
64374bda5b fix indentation level 2015-12-17 23:04:53 +00:00
Matthew Hodgson
772ad4f715 stop generating default identicons. reverts most of 582019f870 and solves vector-web/vector-im#346 2015-12-17 23:04:20 +00:00
Daniel Wagner-Hall
bdacee476d Add display_name to 3pid invite in m.room.member invites 2015-12-17 18:55:08 +01:00
Daniel Wagner-Hall
10f82b4bea Merge pull request #448 from matrix-org/daniel/3pid
Strip address and such out of 3pid invites
2015-12-17 18:29:22 +01:00
Daniel Wagner-Hall
8c5f252edb Strip address and such out of 3pid invites
We're not meant to leak that into the graph
2015-12-17 18:09:51 +01:00
Mark Haines
8b9f471d27 Merge pull request #447 from matrix-org/rav/fix_search_pagination
Fix 500 error when back-paginating search results
2015-12-17 16:43:50 +00:00
Richard van der Hoff
a64f9bbfe0 Fix 500 error when back-paginating search results
We were mistakenly adding pagination clauses to the count query, which then
failed because the count query doesn't join to the events table.
2015-12-17 12:50:46 +00:00
Daniel Wagner-Hall
2b0f8a9482 Fix typo 2015-12-16 17:59:44 +01:00
Daniel Wagner-Hall
af4422c42a Merge pull request #446 from matrix-org/daniel/invitemetadata
Give the IS a bunch more 3pid invite context

This allows it to form richer emails
2015-12-16 14:05:46 +01:00
Daniel Wagner-Hall
0311612ce9 Give the IS a bunch more 3pid invite context
This allows it to form richer emails
2015-12-16 13:05:32 +00:00
Mark Haines
5fc03449c8 Merge pull request #440 from matrix-org/daniel/ise
Include errcode on Internal Server Error
2015-12-16 11:26:55 +00:00
Mark Haines
4fab578b43 Merge branch 'release-v0.12.0' into develop 2015-12-16 11:18:45 +00:00
Mark Haines
661b76615b Merge pull request #445 from matrix-org/markjh/rebind_threepid
Allow users to change which account a 3pid is bound to
2015-12-15 17:07:47 +00:00
Mark Haines
dcfc70e8ed Allow users to change which account a 3pid is bound to 2015-12-15 17:02:21 +00:00
Mark Haines
63fdd9fe0b Changelog and version bump for v0.12.0-rc2 2015-12-14 16:26:59 +00:00
Mark Haines
910956b0ec Merge pull request #443 from matrix-org/markjh/commentary
Add commentary for fix in PR #442
2015-12-14 15:48:00 +00:00
Oddvar Lovaas
d3ac8fd87d Added info abou Martin Giess' auto-deployment process with vagrant/ansible 2015-12-14 15:27:27 +00:00
Mark Haines
e98e00558a Merge pull request #444 from matrix-org/markjh/presence_race
Fix a race between started/stopped stream
2015-12-14 15:21:52 +00:00
Mark Haines
3ddf0b9722 Fix spacing 2015-12-14 15:20:59 +00:00
Mark Haines
2acae8300f Fix logging to lie less 2015-12-14 15:19:37 +00:00
Mark Haines
dbe7892e03 Fix a race between started/stopped stream 2015-12-14 15:09:41 +00:00
Mark Haines
28c5181dfe Add commentary for fix in PR#442 2015-12-14 14:50:51 +00:00
Mark Haines
15e9885197 Merge branch 'release-v0.12.0' into develop 2015-12-14 14:46:55 +00:00
Mark Haines
8505a4ddc3 Merge pull request #441 from matrix-org/markjh/fts_skip_invalid
Skip events that where the body, name or topic isn't a string
2015-12-14 14:42:35 +00:00
Mark Haines
6051266924 Merge pull request #442 from matrix-org/markjh/missing_prev_content
Check whether prev_content or prev_sender is set …
2015-12-14 14:39:07 +00:00
Mark Haines
070e28e203 Combine the prev content tests 2015-12-14 14:34:04 +00:00
Mark Haines
834924248f Check whether prev_content or prev_sender is set before trying to rollback state 2015-12-14 14:09:21 +00:00
Mark Haines
98dfa7d24f Skip events that where the body, name or topic isn't a string when back populating the FTS index 2015-12-14 13:55:46 +00:00
Daniel Wagner-Hall
338c0a8a69 Include errcode on Internal Server Error 2015-12-14 13:50:50 +00:00
Mark Haines
a874c0894a Merge pull request #437 from matrix-org/markjh/parallel_sync
Do the /sync in parallel across the rooms like /initialSync does
2015-12-14 13:34:28 +00:00
Daniel Wagner-Hall
f382a3bb7e Merge pull request #439 from matrix-org/daniel/typo
Fix typo
2015-12-14 12:46:34 +00:00
Daniel Wagner-Hall
76e69cc8de Fix typo 2015-12-14 12:38:55 +00:00
Mark Haines
fde412b240 Merge pull request #438 from matrix-org/markjh/fix_search_sql
Fix typo in sql for full text search on sqlite3
2015-12-14 11:43:49 +00:00
Mark Haines
bfc52a2342 Fix typo in sql for full text search on sqlite3 2015-12-14 11:38:11 +00:00
Mark Haines
deeebbfcb7 Merge branch 'release-v0.12.0' into develop 2015-12-12 14:21:49 +00:00
Mark Haines
1ee7280c4c Do the /sync in parallel accross the rooms like /initialSync does 2015-12-11 16:48:20 +00:00
Erik Johnston
cde49d3d2b Merge pull request #435 from matrix-org/erikj/search
Include approximate count of search results
2015-12-11 16:27:38 +00:00
Paul Evans
e738920156 Merge pull request #433 from matrix-org/paul/tiny-fixes
Ensure that the event that gets persisted is the one that was signed
2015-12-11 15:29:27 +00:00
Mark Haines
0065e554e0 Merge pull request #436 from matrix-org/markjh/pip_instructions
SYN-90: We don't need --proccess-dependency-links
2015-12-11 15:11:30 +00:00
Mark Haines
5a3e4e43d8 SYN-90: We don't need --proccess-dependency-links
When installing synapse since all its dependencies are on PyPI
2015-12-11 14:39:46 +00:00
Erik Johnston
d9a5c56930 Include approximate count of search results 2015-12-11 11:40:23 +00:00
Erik Johnston
51fb590c0e Use more efficient query form 2015-12-11 11:12:57 +00:00
Matthew Hodgson
5577a61090 throwaway 1-liner for generating password hashes 2015-12-10 19:03:06 +00:00
Mark Haines
e0c9f30efa Merge pull request #434 from matrix-org/markjh/forget_rooms
Add caches for whether a room has been forgotten by a user
2015-12-10 18:01:46 +00:00
Mark Haines
515548a47a Missing yield 2015-12-10 17:54:23 +00:00
Mark Haines
7d6b313312 Add caches for whether a room has been forgotten by a user 2015-12-10 17:49:34 +00:00
Paul "LeoNerd" Evans
99afb4b750 Ensure that the event that gets persisted is the one that was signed 2015-12-10 17:08:21 +00:00
Paul "LeoNerd" Evans
d7ee7b589f Merge branch 'develop' into paul/tiny-fixes 2015-12-10 16:21:00 +00:00
Mark Haines
a8589d1ff3 Mark the version as a -rc1 release candidate 2015-12-10 11:39:00 +00:00
Mark Haines
dd9430e758 Update release date 2015-12-10 11:26:58 +00:00
Mark Haines
05f6cb42db Bump synapse version to v0.12.0 2015-12-09 17:48:02 +00:00
Mark Haines
5bdb93c2a6 Add to changelog 2015-12-09 17:45:35 +00:00
Mark Haines
613748804a Changelog for v0.12.0 2015-12-09 17:35:55 +00:00
David Baker
86345a511f Merge pull request #432 from matrix-org/pushrules_refactor
Split out the push rule evaluator into a separate file
2015-12-09 16:13:57 +00:00
David Baker
a24eedada7 pep8 2015-12-09 15:57:42 +00:00
David Baker
4a728beba1 Split out the push rule evaluator into a separate file so it can be more readily reused. Should be functionally identical. 2015-12-09 15:51:34 +00:00
Mark Haines
019597555f Merge pull request #431 from matrix-org/markjh/filter_inline
Allow filter JSON object in the filter query parameter in /sync

Documented by matrix-org/matrix-doc#224
2015-12-09 12:56:53 +00:00
Mark Haines
e4bfe50e8f Allow filter JSON object in the filter query parameter in /sync
Documented by matrix-org/matrix-doc#224
2015-12-09 12:56:50 +00:00
Daniel Wagner-Hall
0f826b0b0d Merge pull request #430 from matrix-org/daniel/unstable
Merge pull request #430 from matrix-org/daniel/unstable
2015-12-09 11:34:22 +00:00
Erik Johnston
7c2ff8c889 Merge pull request #405 from matrix-org/erikj/search-ts
Change the result dict to be a list in /search response
2015-12-08 16:15:27 +00:00
Daniel Wagner-Hall
7a8ba4c9a0 Actually host r0 and unstable prefixes 2015-12-08 15:26:52 +00:00
Mark Haines
219027f580 Merge pull request #429 from matrix-org/markjh/db_counters
Track the time spent in the database per request.
2015-12-08 11:52:35 +00:00
Mark Haines
6a5ff5f223 Track the time spent in the database per request.
and track the number of transactions that request started.
2015-12-07 17:56:11 +00:00
Mark Haines
f7a1cdbbc6 Merge pull request #423 from matrix-org/markjh/archived_flag
Only include the archived rooms if a include_leave flag in set in the…
2015-12-07 13:16:03 +00:00
Mark Haines
d547afeae0 Merge remote-tracking branch 'origin/master' into develop 2015-12-07 13:13:43 +00:00
Mark Haines
dd108286df Merge pull request #426 from OlegGirko/fix_mock_import
Fix mock import in tests.
2015-12-07 13:13:09 +00:00
David Baker
266df8a9b8 Merge pull request #428 from matrix-org/pusher_api_log
Add logging to pushers API to log the body of the request
2015-12-07 12:44:15 +00:00
David Baker
9c9b2829ae also do more structured logging 2015-12-07 12:01:00 +00:00
David Baker
50e5886de1 pep8 2015-12-07 11:57:48 +00:00
David Baker
ba1d740239 Add logging to pushers API to log the body of the request 2015-12-07 11:52:20 +00:00
Mark Haines
a190b2e85e Merge pull request #427 from matrix-org/markjh/log_context
Add a setter for the current log context.
2015-12-07 11:00:12 +00:00
Mark Haines
3dd1630848 Add a setter for the current log context.
Move the resource tracking inside that setter so that it is easier
to make sure that the resource tracking isn't double counting the
resource usage.
2015-12-07 10:51:18 +00:00
Daniel Wagner-Hall
07d18dcab1 Merge pull request #424 from matrix-org/daniel/pushdictification
Take object not bool

Allows bool as legacy fallback

See https://github.com/matrix-org/matrix-doc/pull/212
2015-12-07 10:44:35 +00:00
Daniel Wagner-Hall
41905784f7 Take object not bool
Allows bool as legacy fallback

See https://github.com/matrix-org/matrix-doc/pull/212
2015-12-07 10:44:33 +00:00
Oleg Girko
4013216fcc Fix mock import in tests.
For some reason, one test imports Mock class from mock.mock
rather than from mock.
This change fixes this error.

Signed-off-by: Oleg Girko <ol@infoserver.lv>
2015-12-06 20:50:11 +00:00
Matthew Hodgson
84f2ad5dea Merge pull request #425 from MadsRC/develop
Added installation instructions for postgres on CentOS 7
2015-12-05 14:46:09 +00:00
Mads R. Christensen
44b2bf91be Added installation instructions for postgres on CentOS 7 2015-12-05 15:09:20 +01:00
Mark Haines
660dee94af Only include the archived rooms if a include_leave flag in set in the filter 2015-12-04 17:32:09 +00:00
Mark Haines
262a97f02b Merge pull request #422 from matrix-org/markjh/schema
Bump schema version.
2015-12-04 15:45:44 +00:00
Mark Haines
bd0fa9e2d2 Merge pull request #421 from matrix-org/markjh/resource_metrics
Add metrics to track the cpu on the main thread consumed by each type…
2015-12-04 15:29:31 +00:00
Mark Haines
d57c5cda71 Bump schema version.
As we released version 26 in v0.11.1
2015-12-04 15:28:39 +00:00
Mark Haines
99e1d6777f Add metrics to track the cpu on the main thread consumed by each type of request 2015-12-04 14:42:24 +00:00
Mark Haines
3c85a317d6 Merge pull request #420 from matrix-org/markjh/resource_usage
Track the cpu used in the main thread by each logging context
2015-12-04 14:15:41 +00:00
Mark Haines
5231737369 Add comments to explain why we are hardcoding RUSAGE_THREAD 2015-12-04 11:53:38 +00:00
Mark Haines
d6059bdd2a Fix warnings 2015-12-04 11:34:23 +00:00
Mark Haines
48a2526d62 Track the cpu used in the main thread by each logging context 2015-12-03 21:03:01 +00:00
Mark Haines
b29d2fd7f8 Merge pull request #419 from matrix-org/markjh/reuse_captcha_client
Reuse the captcha client rather than creating a new one for each request
2015-12-03 17:00:59 +00:00
Daniel Wagner-Hall
edfcb83473 Flatten devices into a dict, not a list 2015-12-03 16:19:21 +00:00
Mark Haines
478b4e3ed4 Reuse the captcha client rather than creating a new one for each request 2015-12-03 13:48:55 +00:00
Erik Johnston
b8680b82c3 Merge pull request #414 from matrix-org/erikj/if_not_exists
Older versions of SQLite don't like IF NOT EXISTS in virtual tables
2015-12-03 13:21:09 +00:00
Daniel Wagner-Hall
ac213c2e08 Merge pull request #415 from matrix-org/daniel/endpoints
Merge pull request #415 from matrix-org/daniel/endpoints
2015-12-03 12:19:12 +00:00
Daniel Wagner-Hall
e880164c59 Merge pull request #418 from matrix-org/daniel/whois
Merge pull request #418 from matrix-org/daniel/whois
2015-12-03 12:18:01 +00:00
Daniel Wagner-Hall
526bc33e02 Fix implementation of /admin/whois 2015-12-02 17:29:47 +00:00
David Baker
181616deed Merge pull request #417 from matrix-org/fix_db_v15_postgres
Fix schema delta 15 on postgres
2015-12-02 17:26:17 +00:00
David Baker
e515b48929 Just replace the table definition with the one from full_schema 16 2015-12-02 17:23:52 +00:00
David Baker
8810eb8c39 Fix schema delta 15 on postgres in the very unlikley event that anyone upgrades to 15... 2015-12-02 17:19:11 +00:00
Mark Haines
748c0f5efa Merge pull request #416 from matrix-org/markjh/idempotent_state
Make state updates in the C+S API idempotent
2015-12-02 17:14:45 +00:00
Mark Haines
491f3d16dc Make state updates in the C+S API idempotent 2015-12-02 15:50:50 +00:00
Daniel Wagner-Hall
872c134807 Update endpoints to reflect current spec 2015-12-02 15:45:04 +00:00
Erik Johnston
f721fdbf87 Merge pull request #412 from matrix-org/erikj/search
Search: Add prefix matching support
2015-12-02 13:56:41 +00:00
Erik Johnston
976cb5aaa8 Throw if unrecognized DB type 2015-12-02 13:50:43 +00:00
Erik Johnston
b2def42bfd Older versions of SQLite don't like IF NOT EXISTS in virtual tables 2015-12-02 13:29:14 +00:00
Erik Johnston
b9acef5301 Fix so highlight matching works again 2015-12-02 13:28:13 +00:00
Mark Haines
58d0927767 Merge pull request #410 from matrix-org/markjh/edu_frequency
Only fire user_joined_room if the user has actually joined.
2015-12-02 13:17:15 +00:00
Erik Johnston
7dd6e5efca Remove deuplication. Add comment about regex. 2015-12-02 13:09:37 +00:00
Mark Haines
5dc09e82c4 Merge pull request #413 from matrix-org/markjh/reuse_http_client
Reuse a single http client, rather than creating new ones
2015-12-02 12:56:23 +00:00
Mark Haines
c2c70f7daf Use the context returned by _handle_new_event 2015-12-02 12:01:24 +00:00
Erik Johnston
477da77b46 Search: Add prefix matching support 2015-12-02 11:40:52 +00:00
Mark Haines
37b2d69bbc Reuse a single http client, rather than creating new ones 2015-12-02 11:36:02 +00:00
David Baker
addb248e0b Merge pull request #411 from matrix-org/default_dont_notify
Change the m.room.message rule to be disabled by default
2015-12-02 11:32:56 +00:00
David Baker
4b1281f9b7 Change the m.room.message rule to be disabled by default so we only notify for 1:1 rooms / highlights out-of-the-box 2015-12-02 11:26:49 +00:00
Mark Haines
dede14f689 Merge branch 'develop' into markjh/edu_frequency 2015-12-02 10:57:51 +00:00
Mark Haines
5eb4d13aaa Fix typo in collect_presencelike_data 2015-12-02 10:50:58 +00:00
Mark Haines
c30cdb0d68 Add comments 2015-12-02 10:49:35 +00:00
Mark Haines
2a0ec3b89d Merge branch 'develop' into markjh/edu_frequency
Conflicts:
	synapse/handlers/federation.py
	synapse/handlers/room.py
2015-12-02 10:40:22 +00:00
Erik Johnston
03b2a6a8aa Merge pull request #409 from MadsRC/develop
Develop
2015-12-02 10:08:18 +00:00
Mark Haines
9fbd504b4e Merge pull request #408 from matrix-org/markjh/distributor_facade
Wrap calls to distributor.fire in appropriately named functions
2015-12-02 09:44:56 +00:00
Erik Johnston
9670f226e3 Merge pull request #406 from matrix-org/erikj/search
Search: Don't disregard grouping info in pagination tokens
2015-12-02 09:13:20 +00:00
Mads R. Christensen
6863466653 Added a single line to explain what the server_name is used for 2015-12-02 00:37:55 +01:00
Mads R. Christensen
3d5c5e8be5 Added a few lines to better explain how to run Synapse on a FQDN that is not part of the UserID 2015-12-02 00:35:45 +01:00
Matthew Hodgson
65a9bf2dd5 various fixes - thanks to Mark White for pointing out you need to run synapse before you try to register a new user 2015-12-01 23:05:12 +00:00
Mark Haines
ae9f8cda7e Merge branch 'develop' into markjh/distributor_facade 2015-12-01 21:18:13 +00:00
Mark Haines
5d321e4b9a Fix definitions script 2015-12-01 21:17:58 +00:00
Mark Haines
a9526831a4 Wrap calls to distributor.fire in appropriately named functions so that static analysis can work out want is calling what 2015-12-01 20:53:04 +00:00
Mark Haines
ed0f79bdc5 Only fire user_joined_room if the membership has changed 2015-12-01 19:46:15 +00:00
Mads R. Christensen
98ee629d00 Added --report-status=yes|no as Synapse won't generate the config without it 2015-12-01 20:20:53 +01:00
Mark Haines
f73ea0bda2 Merge branch 'develop' into markjh/edu_frequency 2015-12-01 19:15:27 +00:00
Mads R. Christensen
c533f69d38 Added libffi-devel in CentOS 7 installation requirements and fixed indentation of yum groupinstall. Signed-off-by: Mads Robin Christensen <mads@v42.dk> 2015-12-01 20:00:41 +01:00
Mark Haines
a2922bb944 Merge pull request #392 from matrix-org/markjh/client_config
Add API for setting per user account data at the top level or room level.
2015-12-01 18:42:59 +00:00
Mark Haines
95f30ecd1f Add API for setting account_data globaly or on a per room basis 2015-12-01 18:41:32 +00:00
Matthew Hodgson
f487355364 Merge pull request #407 from MadsRC/develop
Develop
2015-12-01 18:41:31 +00:00
Daniel Wagner-Hall
6e70979973 Merge pull request #400 from matrix-org/daniel/versioning
Merge pull request # 400 from matrix-org/daniel/versioning
2015-12-01 17:36:37 +00:00
Daniel Wagner-Hall
14d7acfad4 Host /unstable and /r0 versions of r0 APIs 2015-12-01 17:34:32 +00:00
Erik Johnston
27c5e1b374 Search: Don't disregard grouping info in pagination tokens 2015-12-01 16:47:18 +00:00
Mark Haines
af96c6f4d3 Merge pull request #404 from matrix-org/markjh/trivial_rename
Rename presence_handler.send_invite
2015-12-01 16:11:03 +00:00
Mark Haines
d32db0bc45 Merge pull request #402 from matrix-org/markjh/event_formatting
Copy rather than move the fields to shuffle between a v1 and a v2 event.
2015-12-01 16:10:24 +00:00
Mark Haines
7b593af7e1 rename the method in the tests as well 2015-12-01 16:06:17 +00:00
Mark Haines
3d3da2b460 Only fire user_joined_room on the distributor if the user has actually joined the room 2015-12-01 16:03:08 +00:00
Mark Haines
31069ecf6a Rename presence_handler.send_invite to presence_handler.send_presence_invite to distinguish it from normal invites 2015-12-01 15:59:45 +00:00
Erik Johnston
71578e2bf2 Change the result tict to be a list 2015-12-01 14:48:35 +00:00
Erik Johnston
2430fcd462 Merge pull request #403 from matrix-org/erikj/search-ts
Allow paginating search ordered by recents
2015-12-01 14:46:27 +00:00
Mark Haines
f593a6e5f8 Add options to definitions.py to fetch referrers and to output dot 2015-12-01 14:29:42 +00:00
Mark Haines
c91a05776f Merge pull request #398 from matrix-org/markjh/jenkins_postgres
Run sytest against postgresql
2015-12-01 13:25:44 +00:00
Mark Haines
5f9a2cb337 Write the tap results for each database to different files when running sytest 2015-12-01 13:24:43 +00:00
Mark Haines
8c902431ba Set the port when running sytest under postgresql 2015-12-01 12:01:29 +00:00
Mark Haines
a33c0748e3 Use a PORT_BASE environment variable to configure the ports that sytest uses 2015-12-01 11:48:20 +00:00
Mark Haines
306415391d Only add the user_id if the sender is present 2015-12-01 11:14:48 +00:00
Erik Johnston
d0f28b46cd Merge pull request #399 from matrix-org/erikj/search
Return words to highlight in search results
2015-12-01 11:13:07 +00:00
Erik Johnston
da7dd58641 Tidy up a bit 2015-12-01 11:06:40 +00:00
Mark Haines
bde8d78b8a Copy rather than move the fields to shuffle between a v1 and a v2 event.
This should make all v1 APIs compatible with v2 clients. While still
allowing v1 clients to access the fields.

This makes the documentation easier since we can just document the v2
format and explain that some of the fields, in some of the APIs are
duplicated for backwards compatibility, rather than having to document
two separate event formats.
2015-11-30 17:46:35 +00:00
Erik Johnston
4dcaa42b6d Allow paginating search ordered by recents 2015-11-30 17:45:31 +00:00
Erik Johnston
76936f43ae Return words to highlight in search results 2015-11-27 16:40:42 +00:00
Mark Haines
f280726037 Run sytest against postgresql if appropriate databases exist for it to run against 2015-11-26 16:50:44 +00:00
Erik Johnston
6cd595e438 Merge pull request #397 from matrix-org/erikj/redaction_inequality
Allow user to redact with an equal power
2015-11-26 13:23:09 +00:00
Erik Johnston
17dd5071ef Allow user to redact with an equal power
Users only need their power level to be equal to the redact level for
them to be allowed to redact events.
2015-11-26 11:17:57 +00:00
Daniel Wagner-Hall
df7cf6c0eb Fix SQL for postgres again 2015-11-23 18:54:41 +00:00
Daniel Wagner-Hall
3e573a5c6b Fix SQL for postgres 2015-11-23 18:48:53 +00:00
Daniel Wagner-Hall
7dfa455508 Remove size specifier for database column
Postgres doesn't support them like this.

We don't have a bool type in common between postgres and sqlite.
2015-11-23 18:35:25 +00:00
Daniel Wagner-Hall
924d85a75e Merge pull request #375 from matrix-org/daniel/guestroominitialsync
Clean up room initialSync for guest users
2015-11-23 16:10:49 +00:00
Matthew Hodgson
91695150cc Merge pull request #396 from MadsRC/develop
CentOS 7 dep instructions from MadsRC
2015-11-21 10:51:32 +00:00
Mads R. Christensen
3dd09a8795 Added myself to AUTHORS.rst
Signed-off-by: Mads Robin Christensen <mads@v42.dk>
2015-11-20 22:39:10 +01:00
Mads R. Christensen
d7739c4e37 Added prerequisite instructions for CentOS 7
Signed-off-by: Mads Robin Christensen <mads@v42.dk>
2015-11-20 22:37:23 +01:00
Mark Haines
c6a15f5026 Merge pull request #385 from matrix-org/daniel/forgetrooms
Allow users to forget rooms
2015-11-20 18:07:26 +00:00
Erik Johnston
2ca01ed747 Merge branch 'release-v0.11.1' of github.com:matrix-org/synapse 2015-11-20 17:38:58 +00:00
Erik Johnston
1b64cb019e Merge branch 'erikj/perspective_limiter' into release-v0.11.1 2015-11-20 17:24:23 +00:00
Erik Johnston
8c3af5bc62 Merge pull request #395 from matrix-org/erikj/perspective_limiter
Don't limit connections to perspective servers
2015-11-20 17:23:26 +00:00
Erik Johnston
0eabfa55f6 Fix typo 2015-11-20 17:17:58 +00:00
Erik Johnston
6408541075 Don't limit connections to perspective servers 2015-11-20 17:15:44 +00:00
Erik Johnston
13130c2c9f Mention report_stats in upgrade.rst 2015-11-20 16:48:43 +00:00
Erik Johnston
7680ae16c9 Fix english 2015-11-20 16:40:55 +00:00
Erik Johnston
2c1bc4392f Bump changes and version 2015-11-20 16:35:01 +00:00
Erik Johnston
93f7bb8dd5 Merge pull request #394 from matrix-org/erikj/search
Add options for including state in search results
2015-11-20 16:31:24 +00:00
Richard van der Hoff
1d9c1d4166 Merge pull request #389 from matrix-org/rav/flatten_sync_response
v2 sync: Get rid of the event_map, and rename the keys of the rooms obj
2015-11-20 15:18:55 +00:00
Erik Johnston
3f151da314 Merge pull request #391 from matrix-org/erikj/remove_token_from_flow
Remove m.login.token from advertised flows.
2015-11-20 14:16:59 +00:00
Erik Johnston
6b95a79724 Add option to include the current room state 2015-11-20 14:16:42 +00:00
Erik Johnston
e3dae653e8 Comment 2015-11-20 14:05:22 +00:00
Erik Johnston
9de1f328ad Merge pull request #393 from matrix-org/erikj/destination_retry_max
Use min and not max to set an upper bound on retry interval
2015-11-20 13:41:20 +00:00
Erik Johnston
506874cca9 Optionally include historic profile info 2015-11-20 11:39:44 +00:00
Erik Johnston
2f2bbb4d06 Use min and not max to set an upper bound on retry interval 2015-11-20 09:34:58 +00:00
Daniel Wagner-Hall
95c3306798 Merge branch 'daniel/forgetrooms' of github.com:matrix-org/synapse into daniel/forgetrooms 2015-11-19 15:00:14 -05:00
Daniel Wagner-Hall
df6824a008 Ignore forgotten rooms in v2 sync 2015-11-19 14:54:47 -05:00
Paul "LeoNerd" Evans
dd11bf8a79 Merge branch 'develop' into rav/flatten_sync_response 2015-11-19 17:21:03 +00:00
Paul "LeoNerd" Evans
1cfda3d2d8 Merge branch 'develop' into daniel/forgetrooms 2015-11-19 16:53:13 +00:00
Erik Johnston
8b5349c7bc Merge branch 'master' of github.com:matrix-org/synapse into develop 2015-11-19 16:17:54 +00:00
Erik Johnston
37de8a7f4a Remove m.login.token from advertised flows. 2015-11-19 16:16:49 +00:00
Mark Haines
7a802ec0ff Merge pull request #386 from matrix-org/markjh/rename_pud_to_account_data
s/private_user_data/account_data/
2015-11-19 15:21:35 +00:00
Daniel Wagner-Hall
d2ecde2cbb Merge pull request #382 from matrix-org/daniel/macarooncleanup
Take a boolean not a list of lambdas
2015-11-19 15:16:32 +00:00
Daniel Wagner-Hall
248cfd5eb3 Take a boolean not a list of lambdas 2015-11-19 15:16:25 +00:00
Daniel Wagner-Hall
9da4c5340d Simplify code 2015-11-19 10:07:21 -05:00
Erik Johnston
f9d9bd6aa0 Merge branch 'hotfixes-v0.11.0-r2' of github.com:matrix-org/synapse 2015-11-19 13:11:49 +00:00
Erik Johnston
5fcef78c6a Bump changes and version 2015-11-19 13:09:48 +00:00
Erik Johnston
c104fd3494 Merge branch 'erikj/fix_port_script' into hotfixes-v0.11.0-r2 2015-11-19 13:08:44 +00:00
Erik Johnston
57a76c9aee Merge pull request #388 from matrix-org/erikj/messages
Split out text for missing config options.
2015-11-19 13:08:09 +00:00
Erik Johnston
06f74068f4 Comment 2015-11-19 13:05:51 +00:00
Erik Johnston
e5d91b8e57 Merge pull request #387 from matrix-org/erikj/fix_port_script
Fix database port script to work with new event_search table
2015-11-19 13:04:28 +00:00
Richard van der Hoff
f6e092f6cc Put back the 'state.events' subobject
We're keeping 'events', in case we decide to add more keys later.
2015-11-19 12:23:42 +00:00
Richard van der Hoff
24ae0eee8e v2 /sync: Rename the keys of the 'rooms' object to match member states
joined->join
invited->invite
archived->leave
2015-11-19 10:51:12 +00:00
Richard van der Hoff
3c3fc6b268 Flatten the /sync response to remove the event_map 2015-11-19 10:51:11 +00:00
Erik Johnston
b361440738 Spelling 2015-11-19 09:11:42 +00:00
Daniel Wagner-Hall
f0ee1d515b Merge pull request #381 from matrix-org/daniel/jenkins-sytest-cached
Share sytest clone across runs

Depends on https://github.com/matrix-org/synapse/pull/380
2015-11-18 23:22:34 +00:00
Daniel Wagner-Hall
628ba81a77 Share sytest clone across runs
Depends on https://github.com/matrix-org/synapse/pull/380
2015-11-18 23:22:27 +00:00
Daniel Wagner-Hall
bed7889703 Apply forgetting properly to historical events 2015-11-18 18:11:12 -05:00
Erik Johnston
03204f54ac Merge branch 'master' of github.com:matrix-org/synapse into develop 2015-11-18 18:38:06 +00:00
Erik Johnston
037ce4c68f Split out text for missing config options.
This allows packages to more easily override the default messages to
include package specific options.
2015-11-18 18:37:05 +00:00
Oddvar Lovaas
2fcd9819ac Erik pointed out we should advise on the lack of included client 2015-11-18 16:51:26 +00:00
Erik Johnston
162e2c1ce5 Fix database port script to work with new event_search table 2015-11-18 15:54:50 +00:00
Erik Johnston
1fe973fa5a Merge branch 'master' of github.com:matrix-org/synapse into develop 2015-11-18 15:53:39 +00:00
Mark Haines
d153f482dd Rename the database table 2015-11-18 15:33:02 +00:00
Mark Haines
1c960fbb80 s/private_user_data/account_data/ 2015-11-18 15:31:04 +00:00
Erik Johnston
915e56e1af Merge branch 'hotfixes-v0.11.0-r1' 2015-11-18 14:15:25 +00:00
Erik Johnston
fbb76a4d5d Change date 2015-11-18 14:15:03 +00:00
Oddvar Lovaas
8bae98b314 adding link to apt repo 2015-11-18 11:44:35 +00:00
Erik Johnston
fe51b3628e Better change log 2015-11-18 11:31:05 +00:00
Daniel Wagner-Hall
ba26eb3d5d Allow users to forget rooms 2015-11-17 17:17:30 -05:00
Erik Johnston
cf844e2ad6 Bump changelog and version 2015-11-17 18:48:32 +00:00
Erik Johnston
b697a842a5 Merge pull request #384 from matrix-org/erikj/shorter_retries
Only retry federation requests for a long time for background requests
2015-11-17 18:46:04 +00:00
Erik Johnston
bd3de8f39a Update tests 2015-11-17 18:38:48 +00:00
Erik Johnston
cbf3cd6151 Add comment 2015-11-17 18:29:29 +00:00
Erik Johnston
a9770e5d24 Merge branch 'hotfixes-v0.10.0-r1' of github.com:matrix-org/synapse into develop 2015-11-17 18:27:17 +00:00
Erik Johnston
cf4ef5f3c7 Only retry federation requests for a long time for background requests 2015-11-17 18:26:50 +00:00
Erik Johnston
afdfd12bdf Implement required method 'resumeProducing' 2015-11-17 16:57:06 +00:00
Erik Johnston
d3861b4442 Merge branch 'release-v0.11.0' of github.com:matrix-org/synapse 2015-11-17 15:45:43 +00:00
Erik Johnston
391f2aa56c Merge branch 'release-v0.11.0' of github.com:matrix-org/synapse into develop 2015-11-17 15:10:49 +00:00
Erik Johnston
bceec65913 Slightly more aggressive retry timers at HTTP level 2015-11-17 15:10:05 +00:00
Erik Johnston
9eff52d1a6 Bump changelog and version 2015-11-17 14:38:36 +00:00
Erik Johnston
0186aef814 Merge branch 'develop' of github.com:matrix-org/synapse into release-v0.11.0 2015-11-17 14:37:13 +00:00
Erik Johnston
e503848990 Merge pull request #349 from stevenhammerton/sh-cas-auth-via-homeserver
SH CAS auth via homeserver
2015-11-17 14:36:15 +00:00
Daniel Wagner-Hall
90b3a98df7 Merge pull request #380 from matrix-org/daniel/jenkins-sytest
Merge pull request #380 from matrix-org/daniel/jenkins-sytest

Run sytests on jenkins
2015-11-17 09:17:03 -05:00
Daniel Wagner-Hall
d34990141e Merge pull request #379 from matrix-org/daniel/jenkins
Output results files on jenkins
2015-11-17 09:14:20 -05:00
Steven Hammerton
f20d064e05 Always check guest = true in macaroons 2015-11-17 10:58:05 +00:00
Steven Hammerton
f5e25c5f35 Merge branch 'develop' into sh-cas-auth-via-homeserver 2015-11-17 10:55:41 +00:00
Daniel Wagner-Hall
f4db76692f Run sytests on jenkins 2015-11-16 15:40:55 -05:00
Daniel Wagner-Hall
09bb5cf02f Output results files on jenkins
Outputs:
 * results.xml
 * coverage.xml
 * violations.flake8.log
2015-11-16 14:35:57 -05:00
Daniel Wagner-Hall
3b90df21d5 Merge pull request #376 from matrix-org/daniel/jenkins
Pull out jenkins script into a checked in script
2015-11-16 12:50:26 -05:00
Paul Evans
1654d3b329 Merge pull request #377 from matrix-org/paul/tiny-fixes
Don't complain if /make_join response lacks 'prev_state' list (SYN-517)
2015-11-13 17:43:30 +00:00
Paul "LeoNerd" Evans
aca6e5bf46 Don't complain if /make_join response lacks 'prev_state' list (SYN-517) 2015-11-13 17:27:25 +00:00
Paul "LeoNerd" Evans
4fbe6ca401 Merge branch 'develop' into paul/tiny-fixes 2015-11-13 17:26:59 +00:00
Daniel Wagner-Hall
233af7c74b Pull out jenkins script into a checked in script 2015-11-13 16:22:51 +00:00
Daniel Wagner-Hall
641420c5e0 Clean up room initialSync for guest users 2015-11-13 15:44:57 +00:00
Daniel Wagner-Hall
6fed9fd697 Merge pull request #374 from matrix-org/daniel/guestleave
Allow guests to /room/:room_id/{join,leave}
2015-11-13 11:57:57 +00:00
Daniel Wagner-Hall
9c3f4f8dfd Allow guests to /room/:room_id/{join,leave} 2015-11-13 11:56:58 +00:00
Erik Johnston
0644f0eb7d Bump version and change log 2015-11-13 11:43:29 +00:00
Erik Johnston
da3dd4867d Merge branch 'develop' into release-v0.11.0 2015-11-13 11:40:17 +00:00
Richard van der Hoff
e4d622aaaf Implementation of state rollback in /sync
Implementation of SPEC-254: roll back the state dictionary to how it looked at
the start of the timeline.

Merged PR https://github.com/matrix-org/synapse/pull/373
2015-11-13 10:58:56 +00:00
Richard van der Hoff
fddedd51d9 Fix a few race conditions in the state calculation
Be a bit more careful about how we calculate the state to be returned by
/sync. In a few places, it was possible for /sync to return slightly later
state than that represented by the next_batch token and the timeline. In
particular, the following cases were susceptible:

* On a full state sync, for an active room
* During a per-room incremental sync with a timeline gap
* When the user has just joined a room. (Refactor check_joined_room to make it
  less magical)

Also, use store.get_state_for_events() (and thus the existing stategroups) to
calculate the state corresponding to a particular sync position, rather than
state_handler.compute_event_context(), which recalculates from first principles
(and tends to miss some state).

Merged from PR https://github.com/matrix-org/synapse/pull/372
2015-11-13 10:39:09 +00:00
Richard van der Hoff
5ab4b0afe8 Make handlers.sync return a state dictionary, instead of an event list.
Basically this moves the process of flattening the existing dictionary into a
list up to rest.client.*, instead of doing it in handlers.sync. This simplifies
a bit of the code in handlers.sync, but it is also going to be somewhat
beneficial in the next stage of my hacking on SPEC-254.

Merged from PR #371
2015-11-13 10:35:01 +00:00
Richard van der Hoff
5dea4d37d1 Update some comments
Add a couple of type annotations, docstrings, and other comments, in the
interest of keeping track of what types I have.

Merged from pull request #370.
2015-11-13 10:31:15 +00:00
Daniel Wagner-Hall
fc27ca9006 Merge pull request #369 from matrix-org/daniel/guestnonevents
Return non-room events from guest /events calls
2015-11-12 17:28:48 +00:00
Daniel Wagner-Hall
468a2ed4ec Return non-room events from guest /events calls 2015-11-12 16:45:28 +00:00
Erik Johnston
018b504f5b Merge pull request #368 from matrix-org/erikj/fix_federation_profile
Fix missing profile data in federation joins
2015-11-12 16:30:02 +00:00
Erik Johnston
49f1758d74 Merge pull request #366 from matrix-org/erikj/search_fix_sqlite_faster
Use a (hopefully) more efficient SQL query for doing recency based room search
2015-11-12 16:24:32 +00:00
Erik Johnston
c0b3554401 Fix missing profile data in federation joins
There was a regression where we stopped including profile data in
initial joins for rooms joined over federation.
2015-11-12 16:19:55 +00:00
Erik Johnston
3de46c7755 Trailing whitespace 2015-11-12 15:36:43 +00:00
Erik Johnston
8fd8e72cec Expand comment 2015-11-12 15:33:47 +00:00
Richard van der Hoff
78f6010207 Fix an issue with ignoring power_level changes on divergent graphs
Changes to m.room.power_levels events are supposed to be handled at a high
priority; however a typo meant that the relevant bit of code was never
executed, so they were handled just like any other state change - which meant
that a bad person could cause room state changes by forking the graph from a
point in history when they were allowed to do so.
2015-11-12 15:24:59 +00:00
Daniel Wagner-Hall
06bfd0a3c0 Merge pull request #367 from matrix-org/daniel/readafterleave
Merge pull request #367 from matrix-org/daniel/readafterleave

Tweak guest access permissions
2015-11-12 15:22:02 +00:00
Erik Johnston
764e79d051 Comment 2015-11-12 15:19:56 +00:00
Daniel Wagner-Hall
0d08670f61 Merge pull request #360 from matrix-org/daniel/guestroominitialsync
Merge pull request #360 from matrix-org/daniel/guestroominitialsync

Allow guest access to room initialSync
2015-11-12 15:19:55 +00:00
Erik Johnston
320408ef47 Fix SQL syntax 2015-11-12 15:09:45 +00:00
Daniel Wagner-Hall
fb7e260a20 Tweak guest access permissions
* Allow world_readable rooms to be read by guests who have joined and
   left
 * Allow regular users to access world_readable rooms
2015-11-12 15:02:00 +00:00
Erik Johnston
14a9d805b9 Use a (hopefully) more efficient SQL query for doing recency based room search 2015-11-12 14:48:39 +00:00
Erik Johnston
39de87869c Fix bug where assumed dict was namedtuple 2015-11-12 14:47:48 +00:00
Daniel Wagner-Hall
473a239d83 Merge pull request #364 from matrix-org/daniel/guestdisplaynames
Allow guests to set their display names

Depends: https://github.com/matrix-org/synapse/pull/363
Tests in https://github.com/matrix-org/sytest/pull/66
2015-11-12 13:44:46 +00:00
Daniel Wagner-Hall
0a93df5f9c Allow guests to set their display names
Depends: https://github.com/matrix-org/synapse/pull/363
Tests in https://github.com/matrix-org/sytest/pull/66
2015-11-12 13:44:39 +00:00
Daniel Wagner-Hall
8ea5dccea1 Merge pull request #363 from matrix-org/daniel/guestscanjoin
Consider joined guest users as joined users

Otherwise they're inconveniently allowed to write events to the room
but not to read them from the room.
2015-11-12 13:37:09 +00:00
Daniel Wagner-Hall
50f1afbd5b Consider joined guest users as joined users
Otherwise they're inconveniently allowed to write events to the room
but not to read them from the room.
2015-11-12 13:37:07 +00:00
Daniel Wagner-Hall
2fc81af06a Merge pull request #362 from matrix-org/daniel/raceyraceyfunfun
Fix race creating directories
2015-11-12 12:05:21 +00:00
Daniel Wagner-Hall
6a9c4cfd0b Fix race creating directories 2015-11-12 11:58:48 +00:00
Erik Johnston
884e601683 Merge branch 'release-v0.11.0' of github.com:matrix-org/synapse into develop 2015-11-11 18:35:16 +00:00
Erik Johnston
63b28c7816 Update date 2015-11-11 18:21:26 +00:00
Erik Johnston
e327327174 Update CHANGES 2015-11-11 18:17:55 +00:00
Erik Johnston
aa3ab6c6a0 Merge branch 'develop' of github.com:matrix-org/synapse into release-v0.11.0 2015-11-11 18:17:49 +00:00
Erik Johnston
04034d0b56 Merge pull request #361 from matrix-org/daniel/guestcontext
Allow guests to access room context API
2015-11-11 18:17:35 +00:00
Erik Johnston
6341be45c6 Merge branch 'develop' of github.com:matrix-org/synapse into release-v0.11.0 2015-11-11 17:51:14 +00:00
Daniel Wagner-Hall
e93d550b79 Allow guests to access room context API 2015-11-11 17:49:44 +00:00
Erik Johnston
e21cef9bb5 Merge pull request #359 from matrix-org/markjh/incremental_indexing
Incremental background updates for db indexes
2015-11-11 17:19:51 +00:00
Mark Haines
e1627388d1 Fix param style to work on both sqlite and postgres 2015-11-11 17:14:56 +00:00
Daniel Wagner-Hall
f15ba926cc Allow guest access to room initialSync 2015-11-11 17:13:24 +00:00
Daniel Wagner-Hall
5d098a32c9 Merge pull request #358 from matrix-org/daniel/publicwritable
Return world_readable and guest_can_join in /publicRooms
2015-11-11 14:38:31 +00:00
Steven Hammerton
ffdc8e5e1c Snakes not camels 2015-11-11 14:26:47 +00:00
Mark Haines
940a161192 Fix the background update 2015-11-11 13:59:40 +00:00
Steven Hammerton
2b779af10f Minor review fixes 2015-11-11 11:21:43 +00:00
Steven Hammerton
dd2eb49385 Share more code between macaroon validation 2015-11-11 11:12:35 +00:00
Daniel Wagner-Hall
cf437900e0 Return world_readable and guest_can_join in /publicRooms 2015-11-10 17:10:27 +00:00
Daniel Wagner-Hall
466b4ec01d Merge pull request #355 from matrix-org/daniel/anonymouswriting
Allow guest users to join and message rooms
2015-11-10 17:00:25 +00:00
Daniel Wagner-Hall
38d82edf0e Allow guest users to join and message rooms 2015-11-10 16:57:13 +00:00
Mark Haines
90b503216c Use a background task to update databases to use the full text search 2015-11-10 16:20:13 +00:00
Mark Haines
36c58b18a3 Test for background updates 2015-11-10 15:51:40 +00:00
Mark Haines
a412b9a465 Run the background updates when starting synapse. 2015-11-10 15:50:58 +00:00
Daniel Wagner-Hall
82e8a2d763 Merge pull request #356 from matrix-org/daniel/3pidyetagain
Get display name from identity server, not client
2015-11-10 12:44:17 +00:00
Mark Haines
2ede7aa8a1 Add background update task for reindexing event search 2015-11-09 19:29:32 +00:00
Mark Haines
889388f105 Merge pull request #357 from matrix-org/rav/SYN-516
Don't fiddle with results returned by event sources
2015-11-09 19:26:46 +00:00
Richard van der Hoff
c7db2068c8 Don't fiddle with results returned by event sources
Overwriting hashes returned by other methods is poor form.

Fixes: SYN-516
2015-11-09 18:09:46 +00:00
Daniel Wagner-Hall
0d63dc3ec9 Get display name from identity server, not client 2015-11-09 17:26:43 +00:00
Mark Haines
c6a01f2ed0 Add storage module for tracking background updates.
The progress for each background update is stored as a JSON blob in the
database. Each background update is broken up into separate batches.
The batch size is automatically tuned to try avoid blocking single
threaded databases for too long.
2015-11-09 17:26:27 +00:00
Richard van der Hoff
9107ed23b7 Add a couple of unit tests for room/<x>/messages
... merely because I was trying to figure out how it worked, and couldn't.
2015-11-09 16:16:43 +00:00
Mark Haines
b1953a9627 Merge pull request #354 from matrix-org/markjh/SYN-513
SYN-513: Include updates for rooms that have had all their tags deleted
2015-11-09 15:16:17 +00:00
Mark Haines
bbe10e8be7 Merge branch 'develop' into markjh/SYN-513
Conflicts:
	synapse/storage/tags.py
2015-11-09 15:01:59 +00:00
Mark Haines
c4135d85e1 SYN-513: Include updates for rooms that have had all their tags deleted 2015-11-09 14:53:08 +00:00
Matthew Hodgson
dd40fb68e4 fix comedy important missing comma breaking recent-ordered FTS on sqlite 2015-11-08 16:04:37 +00:00
Matthew Hodgson
767c20a869 add a key existence check to tags_by_room to avoid /events 500'ing when testing against vector 2015-11-06 20:49:57 +01:00
Daniel Wagner-Hall
5335bf9c34 Merge pull request #353 from matrix-org/daniel/oops
Remove accidentally added ID column
2015-11-06 14:30:10 +00:00
Daniel Wagner-Hall
f2c4ee41b9 Remove accidentally added ID column 2015-11-06 14:27:49 +00:00
Matthew Hodgson
0da4b11efb remove references to matrix.org/beta 2015-11-06 11:00:06 +01:00
Steven Hammerton
0b31223c7a Updates to fallback CAS login to do new token login 2015-11-06 09:57:17 +00:00
Steven Hammerton
fece2f5c77 Merge branch 'develop' into sh-cas-auth-via-homeserver 2015-11-05 20:59:45 +00:00
Erik Johnston
545a7b291a Remove anonymous access, since its not ready yet 2015-11-05 18:06:18 +00:00
Erik Johnston
f23af34729 Merge branch 'develop' of github.com:matrix-org/synapse into release-v0.11.0 2015-11-05 18:00:51 +00:00
Erik Johnston
6be1b4b113 Merge pull request #350 from matrix-org/erikj/search
Implement pagination, order by and groups in search
2015-11-05 17:52:32 +00:00
Erik Johnston
3a02a13e38 Add PR 2015-11-05 17:26:56 +00:00
Erik Johnston
66d36b8e41 Be explicit about what we're doing 2015-11-05 17:26:19 +00:00
Erik Johnston
2aa98ff3bc Remove redundant test 2015-11-05 17:25:50 +00:00
Erik Johnston
5ee070d21f Increment by one, not five 2015-11-05 17:25:33 +00:00
Erik Johnston
f1dcaf3296 Bump changelog and version number 2015-11-05 17:20:28 +00:00
Daniel Wagner-Hall
2cebe53545 Exchange 3pid invites for m.room.member invites 2015-11-05 16:43:19 +00:00
Daniel Wagner-Hall
32fc0737d6 Merge pull request #351 from matrix-org/daniel/fixtox
Fix tox config after fa1cf5ef34
2015-11-05 16:43:09 +00:00
Daniel Wagner-Hall
4df491b922 Fix tox config after fa1cf5ef34 2015-11-05 16:41:32 +00:00
Erik Johnston
1ad6222ebf COMMENTS 2015-11-05 16:29:16 +00:00
Erik Johnston
5bc690408d Merge pull request #340 from matrix-org/erikj/server_retries
Retry dead servers a lot less often
2015-11-05 16:15:50 +00:00
Erik Johnston
3640ddfbf6 Error handling 2015-11-05 16:10:54 +00:00
Erik Johnston
729ea933ea Merge branch 'develop' of github.com:matrix-org/synapse into erikj/search 2015-11-05 15:43:52 +00:00
Erik Johnston
347146be29 Merge branch 'develop' of github.com:matrix-org/synapse into develop 2015-11-05 15:35:17 +00:00
Erik Johnston
7a5ea067e2 Merge branch 'release-v0.10.1' of github.com:matrix-org/synapse into develop 2015-11-05 15:35:05 +00:00
Erik Johnston
7301e05122 Implement basic pagination for search results 2015-11-05 15:04:08 +00:00
Daniel Wagner-Hall
ca2f90742d Open up /events to anonymous users for room events only
Squash-merge of PR #345 from daniel/anonymousevents
2015-11-05 14:32:26 +00:00
Steven Hammerton
414a4a71b4 Allow hs to do CAS login completely and issue the client with a login token that can be redeemed for the usual successful login response 2015-11-05 14:06:48 +00:00
Mark Haines
7a369e8a55 Merge pull request #347 from matrix-org/markjh/check_filter
Remove fields that are both unspecified and unused from the filter checks
2015-11-05 11:15:39 +00:00
Steven Hammerton
45f1827fb7 Add service URL to CAS config 2015-11-04 23:32:30 +00:00
Erik Johnston
05c326d445 Implement order and group by 2015-11-04 17:57:44 +00:00
Daniel Wagner-Hall
4e62ffdb21 Merge branch 'develop' of github.com:matrix-org/synapse into develop 2015-11-04 17:31:01 +00:00
Daniel Wagner-Hall
f522f50a08 Allow guests to register and call /events?room_id=
This follows the same flows-based flow as regular registration, but as
the only implemented flow has no requirements, it auto-succeeds. In the
future, other flows (e.g. captcha) may be required, so clients should
treat this like the regular registration flow choices.
2015-11-04 17:29:07 +00:00
Daniel Wagner-Hall
1758187715 Merge pull request #339 from matrix-org/daniel/removesomelies
Remove unused arguments and code
2015-11-04 17:26:41 +00:00
Mark Haines
33b3e04049 Merge branch 'develop' into daniel/removesomelies
Conflicts:
	synapse/notifier.py
2015-11-04 16:01:00 +00:00
Mark Haines
23cfd32e64 Merge pull request #346 from matrix-org/markjh/remove_lock_manager
Remove the LockManager class because it wasn't being used
2015-11-04 15:49:45 +00:00
Mark Haines
285d056629 Remove fields that are both unspecified and unused from the filter checks, check the right top level definitions in the filter 2015-11-04 15:47:19 +00:00
Mark Haines
c452dabc3d Remove the LockManager class because it wasn't being used 2015-11-04 14:08:15 +00:00
Mark Haines
f74f48e9e6 Merge pull request #341 from matrix-org/markjh/v2_sync_receipts
Include read receipts in v2 sync
2015-11-03 18:45:13 +00:00
Erik Johnston
6a3a840b19 Merge pull request #343 from matrix-org/erikj/fix_retries
Fix broken cache for getting retry times.
2015-11-03 17:51:49 +00:00
Mark Haines
a3bfef35fd Merge branch 'develop' into markjh/v2_sync_receipts
Conflicts:
	synapse/handlers/sync.py
2015-11-03 17:31:17 +00:00
Mark Haines
6797fcd9ab Merge pull request #335 from matrix-org/markjh/room_tags
Add APIs for adding and removing tags from rooms
2015-11-03 16:45:53 +00:00
Erik Johnston
97d792b28f Don't rearrange transaction_queue 2015-11-03 16:31:08 +00:00
Erik Johnston
7ce264ce5f Fix broken cache for getting retry times. This meant we retried remote destinations way more frequently than we should 2015-11-03 16:24:03 +00:00
Paul "LeoNerd" Evans
8a0407c7e6 Merge branch 'develop' into paul/tiny-fixes 2015-11-03 16:08:48 +00:00
Mark Haines
06986e46a3 That TODO was done 2015-11-03 14:28:51 +00:00
Mark Haines
5897e773fd Spell "deferred" more correctly 2015-11-03 14:27:35 +00:00
Mark Haines
2657140c58 Include read receipts in v2 sync 2015-11-02 17:54:04 +00:00
Erik Johnston
eacb068ac2 Retry dead servers a lot less often 2015-11-02 16:56:30 +00:00
Mark Haines
57be722c46 Include room tags in v2 /sync 2015-11-02 16:23:15 +00:00
Daniel Wagner-Hall
771ca56c88 Remove more unused parameters 2015-11-02 15:31:57 +00:00
Mark Haines
ddd8566f41 Store room tag content and return the content in the m.tag event 2015-11-02 15:11:31 +00:00
Daniel Wagner-Hall
192241cf2a Remove unused arguments and code 2015-11-02 15:10:59 +00:00
Erik Johnston
3eb62873f6 Merge pull request #338 from matrix-org/daniel/fixdb
Add missing column
2015-11-02 14:54:31 +00:00
Mark Haines
0e36756383 Merge branch 'develop' into markjh/room_tags 2015-11-02 10:57:00 +00:00
Mark Haines
fb46937413 Support clients supplying older tokens, fix short poll test 2015-10-30 16:38:35 +00:00
Mark Haines
79b65f3875 Include tags in v1 room initial sync 2015-10-30 16:28:19 +00:00
Daniel Wagner-Hall
621e84d9a0 Add missing column 2015-10-30 16:25:53 +00:00
Mark Haines
fdf73c6855 Include room tags v1 /initialSync 2015-10-30 16:22:32 +00:00
Mark Haines
0f432ba551 Merge pull request #337 from matrix-org/markjh/v2_sync_joining
Don't mark newly joined room timelines as limited in an incremental sync
2015-10-30 11:18:08 +00:00
Mark Haines
d58edd98e9 Update the other place check_joined_room is called 2015-10-30 11:15:37 +00:00
Mark Haines
5cf22f0596 Don't mark newly joined room timelines as limited in an incremental sync 2015-10-29 19:58:51 +00:00
Erik Johnston
f6e6f3d87a Make search API honour limit set in filter 2015-10-29 16:17:47 +00:00
Mark Haines
f40b0ed5e1 Inform the client of new room tags using v1 /events 2015-10-29 15:21:09 +00:00
Erik Johnston
5d80dad99e Merge pull request #336 from matrix-org/erikj/search
Optionally return event contexts with search results
2015-10-28 18:39:40 +00:00
Erik Johnston
e83c4b8e3e Merge pull request #334 from matrix-org/erikj/context_api
Add room context api
2015-10-28 18:26:01 +00:00
Erik Johnston
a2e5f7f3d8 Optionally return event contexts with search results 2015-10-28 18:25:11 +00:00
Erik Johnston
2f6ad79a80 Merge branch 'erikj/context_api' into erikj/search 2015-10-28 17:53:24 +00:00
Mark Haines
a89b86dc47 Fix pyflakes errors 2015-10-28 16:45:57 +00:00
Mark Haines
892e70ec84 Add APIs for adding and removing tags from rooms 2015-10-28 16:06:57 +00:00
Erik Johnston
56dbcd1524 Docs 2015-10-28 14:05:50 +00:00
Richard van der Hoff
234d6f9f3e Merge pull request #332 from matrix-org/rav/full_state_sync
Implement full_state incremental sync
2015-10-28 14:04:39 +00:00
Erik Johnston
5cb298c934 Add room context api 2015-10-28 13:45:56 +00:00
Richard van der Hoff
d0b1968a4c Merge pull request #331 from matrix-org/rav/500_on_missing_sigil
Fix a 500 error resulting from empty room_ids
2015-10-27 15:20:57 +00:00
Richard van der Hoff
c79c4f9b14 Implement full_state incremental sync
A hopefully-complete implementation of the full_state incremental sync, as
specced at https://github.com/matrix-org/matrix-doc/pull/133.

This actually turns out to be a relatively simple modification to the initial
sync implementation.
2015-10-26 18:47:18 +00:00
Richard van der Hoff
f69a5c9134 Fix a 500 error resulting from empty room_ids
POST /_matrix/client/api/v1/rooms//send/a.b.c gave a 500 error, because we
assumed that rooms always had at least one character.
2015-10-26 18:44:03 +00:00
Erik Johnston
a299fede9d Merge branch 'release-v0.10.1' of github.com:matrix-org/synapse into release-v0.10.1 2015-10-26 18:16:31 +00:00
Erik Johnston
f73de2004e Use correct service url 2015-10-26 18:12:09 +00:00
Erik Johnston
cea2039b56 Merge pull request #330 from matrix-org/erikj/login_fallback
Add login fallback
2015-10-26 17:57:00 +00:00
Erik Johnston
f7e14bb535 Merge pull request #329 from matrix-org/erikj/static
Move static folder into synapse
2015-10-26 17:35:37 +00:00
Erik Johnston
87961d8dcf Add login fallback 2015-10-26 17:35:24 +00:00
Erik Johnston
fa1cf5ef34 Move static folder into synapse
This is because otherwise it won't get picked up by python packaging.

This also fixes the problem where the "static" folder was found if
synapse wasn't started from that directory.
2015-10-26 15:37:44 +00:00
Erik Johnston
3f0a57eb9b Merge pull request #328 from matrix-org/erikj/search
Pull out sender when computing search results
2015-10-26 10:24:19 +00:00
Erik Johnston
4cf633d5e9 Pull out sender when computing search results 2015-10-23 15:41:36 +01:00
Erik Johnston
b8e37ed944 Merge pull request #327 from matrix-org/erikj/search
Implement rank function for SQLite FTS
2015-10-23 15:27:51 +01:00
Erik Johnston
0c36098c1f Implement rank function for SQLite FTS 2015-10-23 13:23:48 +01:00
Daniel Wagner-Hall
216c976399 Merge pull request #323 from matrix-org/daniel/sizelimits
Reject events which are too large
2015-10-23 11:26:03 +01:00
Erik Johnston
259d10f0e4 Merge branch 'release-v0.10.1' of github.com:matrix-org/synapse into develop 2015-10-23 11:11:56 +01:00
Mark Haines
b051781ddb Merge pull request #325 from matrix-org/markjh/filter_dicts
Support filtering events represented as dicts.
2015-10-22 17:14:52 +01:00
Erik Johnston
53c679b59b Merge pull request #324 from matrix-org/erikj/search
Add filters to search.
2015-10-22 17:14:12 +01:00
Mark Haines
4e05aab4f7 Don't assume that the event has a room_id or sender 2015-10-22 17:08:59 +01:00
Erik Johnston
671ac699f1 Actually filter results 2015-10-22 16:54:56 +01:00
Mark Haines
9b6f3bc742 Support filtering events represented as dicts.
This is useful because the emphemeral events such as presence and
typing are represented as dicts inside synapse.
2015-10-22 16:38:03 +01:00
Erik Johnston
2980136d75 Rename 2015-10-22 16:19:53 +01:00
Erik Johnston
fb0fecd0b9 LESS THAN 2015-10-22 16:18:35 +01:00
Erik Johnston
61547106f5 Fix receipts for room initial sync 2015-10-22 16:17:23 +01:00
Erik Johnston
232beb3a3c Use namedtuple as return value 2015-10-22 15:02:35 +01:00
Erik Johnston
ba02bba88c Limit max number of SQL vars 2015-10-22 13:25:27 +01:00
Erik Johnston
1fc2d11a14 Merge branch 'develop' of github.com:matrix-org/synapse into erikj/search 2015-10-22 13:17:14 +01:00
Erik Johnston
b0ac0a9438 Merge pull request #319 from matrix-org/erikj/filter_refactor
Refactor api.filtering to have a Filter API
2015-10-22 13:17:10 +01:00
Erik Johnston
8a98f0dc5b Merge branch 'develop' of github.com:matrix-org/synapse into erikj/search 2015-10-22 13:16:35 +01:00
Erik Johnston
c9c82e8f4d Merge pull request #320 from matrix-org/appservice-retry-cap
Cap the time to retry txns to appservices to 8.5 minutes
2015-10-22 13:15:54 +01:00
Daniel Wagner-Hall
e60dad86ba Reject events which are too large
SPEC-222
2015-10-22 11:44:31 +01:00
Erik Johnston
f142898f52 PEP8 2015-10-22 11:18:01 +01:00
Erik Johnston
3993d6ecc2 Merge pull request #322 from matrix-org/erikj/password_config
Add config option to disable password login
2015-10-22 11:16:49 +01:00
Erik Johnston
4d25bc6c92 Move FTS to delta 25 2015-10-22 11:12:28 +01:00
Mark Haines
87da71bace Merge pull request #314 from matrix-org/paul/event-redaction
Add some unit tests of prune_events()
2015-10-22 11:07:20 +01:00
Erik Johnston
3ce1b8c705 Don't keep appending report_stats to demo config 2015-10-22 10:43:46 +01:00
Erik Johnston
5025ba959f Add config option to disable password login 2015-10-22 10:37:04 +01:00
Mark Haines
13a6e9beaf Merge pull request #321 from matrix-org/markjh/v2_sync_typing
Include typing events in initial v2 sync
2015-10-21 16:47:19 +01:00
Mark Haines
5201c66108 Merge branch 'develop' into markjh/v2_sync_typing
Conflicts:
	synapse/handlers/sync.py
2015-10-21 15:48:34 +01:00
Mark Haines
e94ffd89d6 Merge pull request #316 from matrix-org/markjh/v2_sync_archived
Add rooms that the user has left under archived in v2 sync.
2015-10-21 15:46:41 +01:00
Mark Haines
d63a0ca34b Doc string for the SyncHandler.typing_by_room method 2015-10-21 15:45:37 +01:00
Mark Haines
e3d75f564a Include banned rooms in the archived section of v2 sync 2015-10-21 11:15:48 +01:00
Kegsay
8627048787 Merge pull request #318 from matrix-org/syn-502-login-bad-emails
Don't 500 on /login when the email doesn't map to a valid user ID.
2015-10-21 10:16:33 +01:00
Kegan Dougal
4dec901c76 Cap the time to retry txns to appservices to 8.5 minutes
There's been numerous issues with people playing around with their
application service and then not receiving events from their HS for
ages due to backoff timers reaching crazy heights (albeit capped at
< 1 day).

Reduce the max time between pokes to be 8.5 minutes (2^9 secs) which
is quick enough for people to wait it out (avg wait time being 4.25 min)
but long enough to actually give the AS breathing room if it needs it.
2015-10-21 10:10:55 +01:00
Erik Johnston
5c41224a89 Filter room ids before hitting the database 2015-10-21 10:09:26 +01:00
Erik Johnston
c8baada94a Filter search results 2015-10-21 10:08:53 +01:00
Kegan Dougal
ede07434e0 Use 403 and message to match handlers/auth 2015-10-21 09:42:07 +01:00
Erik Johnston
44e2933bf8 Merge branch 'erikj/filter_refactor' into erikj/search 2015-10-20 16:57:51 +01:00
Mark Haines
7be06680ed Include typing events in initial v2 sync 2015-10-20 16:36:20 +01:00
Erik Johnston
87deec824a Docstring 2015-10-20 15:47:42 +01:00
Erik Johnston
45cd2b0233 Refactor api.filtering to have a Filter API 2015-10-20 15:33:25 +01:00
Daniel Wagner-Hall
f510586372 Merge branch 'develop' of github.com:matrix-org/synapse into develop 2015-10-20 12:00:22 +01:00
Daniel Wagner-Hall
137fafce4e Allow rejecting invites
This is done by using the same /leave flow as you would use if you had
already accepted the invite and wanted to leave.
2015-10-20 11:58:58 +01:00
Kegan Dougal
b02a342750 Don't 500 when the email doesn't map to a valid user ID. 2015-10-20 11:07:50 +01:00
Mark Haines
51d03e65b2 Fix pep8 2015-10-19 17:48:58 +01:00
Paul Evans
3c7d6202ea Merge pull request #315 from matrix-org/paul/test-vectors
Repeatable unit test of event hashing/signing algorithm
2015-10-19 17:47:57 +01:00
Paul "LeoNerd" Evans
9ed784098a Invoke EventBuilder directly instead of going via the EventBuilderFactory 2015-10-19 17:42:34 +01:00
Paul "LeoNerd" Evans
531e3aa75e Capture __init__.py 2015-10-19 17:37:35 +01:00
Mark Haines
68b7fc3e2b Add rooms that the user has left under archived in v2 sync. 2015-10-19 17:26:18 +01:00
Daniel Wagner-Hall
9261ef3a15 Merge pull request #312 from matrix-org/daniel/3pidinvites
Stuff signed data in a standalone object
2015-10-19 15:52:34 +01:00
Paul "LeoNerd" Evans
a8795c9644 Use assertIn() instead of assertTrue on the 'in' operator 2015-10-19 15:24:49 +01:00
Paul "LeoNerd" Evans
07b58a431f Another signing test vector using an 'm.room.message' with content, so that the implementation will have to redact it 2015-10-19 15:00:52 +01:00
Paul "LeoNerd" Evans
0aab34004b Initial minimial hack at a test of event hashing and signing 2015-10-19 14:40:15 +01:00
Erik Johnston
e0bf0258ee Merge pull request #307 from matrix-org/erikj/search
Add basic search API
2015-10-19 13:37:15 +01:00
Paul "LeoNerd" Evans
aff4d850bd Add some unit tests of prune_events() 2015-10-16 19:56:46 +01:00
Paul Evans
ae3082dd31 Merge pull request #313 from matrix-org/paul/tiny-fixes
Surely we don't need to preserve 'events_default' twice
2015-10-16 19:14:19 +01:00
Paul "LeoNerd" Evans
243a79d291 Surely we don't need to preserve 'events_default' twice 2015-10-16 18:25:19 +01:00
Mark Haines
9371a35e89 Merge pull request #306 from matrix-org/markjh/unused_methods
Remove some login classes from synapse.
2015-10-16 18:18:41 +01:00
Daniel Wagner-Hall
0e5239ffc3 Stuff signed data in a standalone object
Makes both generating it in sydent, and verifying it here, simpler at
the cost of some repetition
2015-10-16 17:45:48 +01:00
Mark Haines
b19b9535f6 Merge pull request #310 from matrix-org/markjh/bcrypt_rounds
Add config for how many bcrypt rounds to use for password hashes
2015-10-16 17:05:21 +01:00
Erik Johnston
46d39343d9 Explicitly check for Sqlite3Engine 2015-10-16 16:58:00 +01:00
Erik Johnston
f2d698cb52 Typing 2015-10-16 16:46:48 +01:00
Erik Johnston
33646eb000 Merge branch 'develop' of github.com:matrix-org/synapse into erikj/search 2015-10-16 15:35:35 +01:00
Erik Johnston
524b708f98 Merge pull request #311 from matrix-org/markjh/postgres_fixes
Fix FilteringStore.get_user_filter to work with postgres
2015-10-16 15:35:26 +01:00
Erik Johnston
380f148db7 Remove unused import 2015-10-16 15:32:56 +01:00
Mark Haines
fc012aa8dc Fix FilteringStore.get_user_filter to work with postgres 2015-10-16 15:28:43 +01:00
Daniel Wagner-Hall
e5acc8a47b Merge pull request #302 from matrix-org/daniel/3pidinvites
Implement third party identifier invites
2015-10-16 15:23:30 +01:00
Erik Johnston
d4b5621e0a Remove duplicate _filter_events_for_client 2015-10-16 15:19:52 +01:00
Erik Johnston
23ed7dc0e7 Merge branch 'develop' of github.com:matrix-org/synapse into erikj/search 2015-10-16 15:18:42 +01:00
Erik Johnston
315b03b58d Merge pull request #309 from matrix-org/erikj/_filter_events_for_client
Amalgamate _filter_events_for_client
2015-10-16 15:18:32 +01:00
Daniel Wagner-Hall
c225d63e9e Add signing host and keyname to signatures 2015-10-16 15:07:56 +01:00
Daniel Wagner-Hall
b8dd5b1a2d Verify third party ID server certificates 2015-10-16 14:54:54 +01:00
Erik Johnston
366af6b73a Amalgamate _filter_events_for_client 2015-10-16 14:52:48 +01:00
Mark Haines
f2f031fd57 Add config for how many bcrypt rounds to use for password hashes
By default we leave it at the default value of 12. But now we can reduce
it for preparing users for loadtests or running integration tests.
2015-10-16 14:52:08 +01:00
Erik Johnston
12122bfc36 Merge branch 'develop' of github.com:matrix-org/synapse into erikj/search 2015-10-16 14:46:32 +01:00
Erik Johnston
edb998ba23 Explicitly check for Sqlite3Engine 2015-10-16 14:37:14 +01:00
Mark Haines
5df54de801 Merge pull request #308 from matrix-org/markjh/v2_filter_encoding
Encode the filter JSON as UTF-8 before storing in the database.
2015-10-16 13:41:31 +01:00
Erik Johnston
b62da463e1 docstring 2015-10-16 11:52:16 +01:00
Erik Johnston
3cf9948b8d Add docstring 2015-10-16 11:28:12 +01:00
Erik Johnston
73260ad01f Comment on the LIMIT 500 2015-10-16 11:24:02 +01:00
Erik Johnston
22a8c91448 Split up run_upgrade 2015-10-16 11:19:44 +01:00
Erik Johnston
a8945d24d1 Reorder changelog 2015-10-16 11:07:37 +01:00
Mark Haines
6296590bf7 Encode the filter JSON as UTF-8 before storing in the database.
Because we are using a binary column type to store the filter JSON.
2015-10-16 10:50:32 +01:00
Erik Johnston
bcfb653816 Merge branch 'develop' of github.com:matrix-org/synapse into erikj/search 2015-10-15 16:37:32 +01:00
Erik Johnston
e46cdc08cc Update change log 2015-10-15 15:29:00 +01:00
Erik Johnston
8189c4e3fd Bump version 2015-10-15 15:29:00 +01:00
Daniel Wagner-Hall
6ffbcf45c6 Use non-placeholder name for endpoint 2015-10-15 13:12:52 +01:00
Daniel Wagner-Hall
643b5fcdc8 Look for keys on the right objects 2015-10-15 13:10:30 +01:00
Daniel Wagner-Hall
f38df51e8d Merge branch 'develop' into daniel/3pidinvites 2015-10-15 11:51:55 +01:00
Mark Haines
1a934e8bfd synapse.client.v1.login.LoginFallbackRestServlet and synapse.client.v1.login.PasswordResetRestServlet are unused 2015-10-15 11:09:57 +01:00
Mark Haines
5338220d3a synapse.util.emailutils was unused 2015-10-15 10:39:33 +01:00
Mark Haines
a059760954 Merge pull request #305 from matrix-org/markjh/v2_sync_api
Update the v2 sync API to work as specified in the current spec.
2015-10-14 13:56:23 +01:00
Erik Johnston
d7c70d09f0 Merge pull request #304 from matrix-org/erikj/remove_unused_arg
Remove unused room_id arg
2015-10-14 13:39:33 +01:00
Mark Haines
c185c1c413 Fix v2 sync polling 2015-10-14 13:16:53 +01:00
Mark Haines
f50c43464c Merge branch 'develop' into markjh/v2_sync_api 2015-10-14 11:40:45 +01:00
Erik Johnston
f45aaf0e35 Remove unused constatns 2015-10-14 10:36:55 +01:00
Erik Johnston
8c9df8774e Make 'keys' optional 2015-10-14 10:35:50 +01:00
Erik Johnston
99c7fbfef7 Fix to work with SQLite 2015-10-14 09:52:40 +01:00
Erik Johnston
1d9e109820 More TODO markers 2015-10-14 09:49:00 +01:00
Erik Johnston
d25b0f65ea Add TODO markers 2015-10-14 09:46:31 +01:00
Erik Johnston
858634e1d0 Remove unused room_id arg 2015-10-14 09:31:20 +01:00
Mark Haines
474274583f Merge pull request #303 from matrix-org/markjh/twisted_debugging
Bounce all deferreds through the reactor to make debugging easier.
2015-10-13 18:40:57 +01:00
Daniel Wagner-Hall
d82c5f7b5c Use more descriptive error code 2015-10-13 18:02:00 +01:00
Daniel Wagner-Hall
0c38e8637f Remove unnecessary class-wrapping 2015-10-13 18:00:38 +01:00
Mark Haines
1941eb315d Enable stack traces for the demo scripts 2015-10-13 18:00:02 +01:00
Mark Haines
9020860479 Only turn on the twisted deferred debugging if full_twisted_stacktraces is set in the config 2015-10-13 17:50:44 +01:00
Daniel Wagner-Hall
14edea1aff Move logic into handler 2015-10-13 17:47:58 +01:00
Daniel Wagner-Hall
b68db61222 Add logging 2015-10-13 17:22:50 +01:00
Daniel Wagner-Hall
bb407cd624 Re-add accidentally removed code 2015-10-13 17:19:26 +01:00
Mark Haines
32d66738b0 Fix pep8 warnings. 2015-10-13 17:18:29 +01:00
Daniel Wagner-Hall
95e53ac535 Add some docstring 2015-10-13 17:18:24 +01:00
Mark Haines
7639c3d9e5 Bounce all deferreds through the reactor to make debugging easier.
If all deferreds wait a reactor tick before resolving then there is
always a chance to add an errback to the deferred so that stacktraces
get reported, rather than being discarded.
2015-10-13 17:13:04 +01:00
Erik Johnston
7ecd11accb Add paranoia limit 2015-10-13 15:50:56 +01:00
Daniel Wagner-Hall
17dffef5ec Move event contents into third_party_layout field 2015-10-13 15:48:12 +01:00
Erik Johnston
3e2a1297b5 Remove constraints in preperation of using filters 2015-10-13 15:22:14 +01:00
Erik Johnston
323d3e506d Merge branch 'develop' of github.com:matrix-org/synapse into erikj/search 2015-10-13 14:34:01 +01:00
Erik Johnston
ff2b66f42e Merge pull request #301 from matrix-org/markjh/v2_filtering
Update the v2 filters to support filtering presence.
2015-10-13 14:33:48 +01:00
Mark Haines
8897781558 update filtering tests 2015-10-13 14:13:51 +01:00
Mark Haines
2fa9e23e04 Update the v2 filters to support filtering presence and remove support for public/private user data 2015-10-13 14:12:43 +01:00
Mark Haines
cacf0688c6 Add a get_invites_for_user method to the storage to find out the rooms a user is invited to 2015-10-13 14:08:38 +01:00
Erik Johnston
88971fd034 Merge branch 'erikj/store_engine' into erikj/search 2015-10-13 14:03:30 +01:00
Erik Johnston
7ec9be9c53 Merge pull request #300 from matrix-org/erikj/store_engine
Split out the schema preparation and update logic into its own module
2015-10-13 14:01:15 +01:00
Erik Johnston
17c80c8a3d rename schema_prepare to prepare_database 2015-10-13 13:56:22 +01:00
Erik Johnston
cfd39d6b55 Add SQLite support 2015-10-13 13:47:50 +01:00
Daniel Wagner-Hall
32a453d7ba Merge branch 'develop' into daniel/3pidinvites 2015-10-13 13:32:43 +01:00
Erik Johnston
f9340ea0d5 Merge branch 'erikj/store_engine' into erikj/search 2015-10-13 13:29:02 +01:00
Erik Johnston
ec398af41c Expose error more nicely 2015-10-13 11:43:43 +01:00
Mark Haines
54414221e4 Include invites in incremental sync 2015-10-13 11:43:12 +01:00
Erik Johnston
40b6a5aad1 Split out the schema preparation and update logic into its own module 2015-10-13 11:38:48 +01:00
Mark Haines
ab9cf73258 Include invited rooms in the initial sync 2015-10-13 11:03:48 +01:00
Erik Johnston
30c2783d2f Search left rooms too 2015-10-13 10:36:36 +01:00
Erik Johnston
1a40afa756 Add sqlite schema 2015-10-13 10:36:25 +01:00
Mark Haines
f96b480670 Merge branch 'develop' into markjh/v2_sync_api 2015-10-13 10:33:00 +01:00
Mark Haines
956509dfec Start spliting out the rooms into joined and invited in v2 sync 2015-10-13 10:24:51 +01:00
Mark Haines
586beb8318 Update the filters to match the latest spec.
Apply the filter the 'timeline' and 'ephemeral' keys of rooms.
Apply the filter to the 'presence' key of a sync response.
2015-10-12 16:54:58 +01:00
Erik Johnston
427943907f Merge pull request #299 from stevenhammerton/sh-cas-required-attribute
SH CAS Required Attribute
2015-10-12 16:08:34 +01:00
Steven Hammerton
739464fbc5 Add a comment to clarify why we split on closing curly brace when reading CAS attribute tags 2015-10-12 16:02:17 +01:00
Erik Johnston
ca53ad7425 Filter events to only thsoe that the user is allowed to see 2015-10-12 15:52:55 +01:00
Erik Johnston
f6fde343a1 Merge remote-tracking branch 'origin/develop' into erikj/search 2015-10-12 15:06:18 +01:00
Erik Johnston
927004e349 Remove unused room_id parameter 2015-10-12 15:06:14 +01:00
Steven Hammerton
83b464e4f7 Unpack dictionary in for loop for nicer syntax 2015-10-12 15:05:34 +01:00
Steven Hammerton
ab7f9bb861 Default cas_required_attributes to empty dictionary 2015-10-12 14:58:59 +01:00
Mark Haines
54cb509d64 Merge pull request #296 from matrix-org/markjh/eventstream_presence
Split the sections of EventStreamHandler.get_stream that handle presence
2015-10-12 14:48:09 +01:00
Mark Haines
885301486c Merge pull request #297 from matrix-org/markjh/presence_races
Fix some races in the synapse presence handler caused by not yielding…
2015-10-12 14:47:53 +01:00
Steven Hammerton
7f8fdc9814 Remove not required parenthesis 2015-10-12 14:45:24 +01:00
Steven Hammerton
01a5f1991c Support multiple required attributes in CAS response, and in a nicer config format too 2015-10-12 14:43:17 +01:00
Steven Hammerton
76421c496d Allow optional config params for a required attribute and it's value, if specified any CAS user must have the given attribute and the value must equal 2015-10-12 11:11:49 +01:00
Steven Hammerton
7845f62c22 Parse both user and attributes from CAS response 2015-10-12 10:55:13 +01:00
Erik Johnston
ae72e247fa PEP8 2015-10-12 10:50:46 +01:00
Erik Johnston
61561b9df7 Keep FTS indexes up to date. Only search through rooms currently joined 2015-10-12 10:49:53 +01:00
Matthew Hodgson
782f7fb489 add steve to authors 2015-10-10 18:24:44 +01:00
Erik Johnston
a80ef851f7 Fix previous merge to s/version_string/user_agent/ 2015-10-10 12:35:39 +01:00
Erik Johnston
347aa3c225 Merge pull request #295 from stevenhammerton/sh-cas-auth
Provide ability to login using CAS
2015-10-10 12:18:33 +01:00
Steven Hammerton
95f7661170 Raise LoginError if CasResponse doensn't contain user 2015-10-10 10:54:19 +01:00
Steven Hammerton
a9c299c0be Fix my broken line splitting 2015-10-10 10:54:19 +01:00
Steven Hammerton
e52f4dc599 Use UserId to create FQ user id 2015-10-10 10:54:19 +01:00
Steven Hammerton
625e13bfde Add get_raw method to SimpleHttpClient, use this in CAS auth rather than requests 2015-10-10 10:54:19 +01:00
Steven Hammerton
22112f8d14 Formatting changes 2015-10-10 10:49:42 +01:00
Steven Hammerton
c33f5c1a24 Provide ability to login using CAS 2015-10-10 10:49:42 +01:00
Mark Haines
1a46daf621 Merge branch 'markjh/presence_races' into markjh/v2_sync_api 2015-10-09 20:02:30 +01:00
Mark Haines
987803781e Fix some races in the synapse presence handler caused by not yielding on deferreds 2015-10-09 19:59:50 +01:00
Mark Haines
0a96a9a023 Set the user as online if they start polling the v2 sync 2015-10-09 19:57:50 +01:00
Mark Haines
af7b214476 Merge branch 'markjh/eventstream_presence' into markjh/v2_sync_api 2015-10-09 19:18:09 +01:00
Mark Haines
1b9802a0d9 Split the sections of EventStreamHandler.get_stream that handle presence
into separate functions.

This makes the code a bit easier to read, and means that we can reuse
the logic when implementing the v2 sync API.
2015-10-09 19:13:08 +01:00
Mark Haines
c15cf6ac06 Format the presence events correctly for v2 2015-10-09 18:50:15 +01:00
Erik Johnston
c85c912562 Add basic full text search impl. 2015-10-09 15:48:31 +01:00
Mark Haines
ce19fc0f11 Merge pull request #294 from matrix-org/markjh/initial_sync_archived_flag
Add a flag to initial sync to include we want rooms that the user has left
2015-10-09 10:32:27 +01:00
Mark Haines
51ef725647 Use 'true' rather than '1' for archived flag 2015-10-08 18:13:02 +01:00
Mark Haines
dc72021748 Add a flag to initial sync to indicate we want rooms that the user has left 2015-10-08 17:26:23 +01:00
Mark Haines
dfef2b41aa Update the v2 room sync format to match the current v2 spec 2015-10-08 15:17:43 +01:00
David Baker
91482cd6a0 Use raw string for regex here, otherwise \b is the backspace character. Fixes displayname matching. 2015-10-08 11:22:15 +01:00
Mark Haines
e3d3205cd9 Update the sync response to match the latest spec 2015-10-07 15:55:20 +01:00
Daniel Wagner-Hall
7c809abe86 Merge branch 'develop' into daniel/3pidinvites 2015-10-06 10:24:32 -05:00
Daniel Wagner-Hall
db6e1e1fe3 Merge pull request #292 from matrix-org/daniel/useragent
Allow synapse's useragent to be customized
2015-10-06 10:23:21 -05:00
Daniel Wagner-Hall
61ee72517c Remove merge thinko 2015-10-06 10:16:15 -05:00
Daniel Wagner-Hall
1cacc71050 Add third party invites to auth_events for joins 2015-10-06 10:13:28 -05:00
Mark Haines
fac990a656 Merge pull request #293 from matrix-org/markjh/remove_spamy_error_logging
Remove log line that was generated whenever an error was created.
2015-10-06 16:13:07 +01:00
Daniel Wagner-Hall
fcd9ba8802 Fix lint errors 2015-10-06 10:13:05 -05:00
Mark Haines
93cc60e805 Remove log line that was generated whenever an error was created. We are now creating error objects that aren't raised so it's probably a bit too confusing to keep 2015-10-06 16:10:19 +01:00
Daniel Wagner-Hall
d4bb28c59b Revert "Revert "Merge pull request #283 from matrix-org/erikj/atomic_join_federation""
This reverts commit 34d26d3687.
2015-10-06 09:58:21 -05:00
Daniel Wagner-Hall
ca6496c27c Merge branch 'daniel/useragent' into daniel/3pidinvites 2015-10-06 09:55:21 -05:00
Daniel Wagner-Hall
492beb62a8 Use space not dash as delimiter 2015-10-06 09:53:33 -05:00
Daniel Wagner-Hall
e0b466bcfd Use space not dash as delimiter 2015-10-06 09:32:26 -05:00
Daniel Wagner-Hall
287c81abf3 Merge branch 'develop' into daniel/useragent 2015-10-06 09:30:17 -05:00
Daniel Wagner-Hall
c05b5ef7b0 Merge branch 'develop' into daniel/3pidinvites 2015-10-06 08:10:34 -05:00
Daniel Wagner-Hall
ddd079c8f8 Merge branch 'daniel/useragent' into daniel/3pidinvites 2015-10-05 20:52:15 -05:00
Daniel Wagner-Hall
b28c7da0a4 Preserve version string in user agent 2015-10-05 20:49:39 -05:00
Daniel Wagner-Hall
34d26d3687 Revert "Merge pull request #283 from matrix-org/erikj/atomic_join_federation"
This reverts commit 5879edbb09, reversing
changes made to b43930d4c9.
2015-10-05 19:10:47 -05:00
Mark Haines
471555b3a8 Move the rooms out into a room_map mapping from room_id to room. 2015-10-05 16:39:36 +01:00
Daniel Wagner-Hall
58e6a58eb7 Merge branch 'develop' into daniel/3pidinvites 2015-10-05 10:33:41 -05:00
Daniel Wagner-Hall
8fc52bc56a Allow synapse's useragent to be customized
This will allow me to write tests which verify which server made HTTP
requests in a federation context.
2015-10-02 17:13:51 -05:00
Erik Johnston
49ebd472fa Explicitly add Create event as auth event 2015-10-02 13:22:36 +01:00
Erik Johnston
40017a9a11 Add 'trusted_private_chat' to room creation presets 2015-10-02 11:22:56 +01:00
Erik Johnston
a086b7aa00 Merge pull request #275 from matrix-org/erikj/invite_state
Bundle in some room state in invites.
2015-10-02 11:15:43 +01:00
Erik Johnston
9c311dfce5 Also bundle in sender 2015-10-02 11:04:23 +01:00
Erik Johnston
a38d36ccd0 Merge pull request #279 from matrix-org/erikj/unfederatable
Add flag which disables federation of the room
2015-10-02 10:41:14 +01:00
Erik Johnston
d5e081c7ae Merge branch 'develop' of github.com:matrix-org/synapse into erikj/unfederatable 2015-10-02 10:33:49 +01:00
Erik Johnston
5879edbb09 Merge pull request #283 from matrix-org/erikj/atomic_join_federation
Atomically persist events when joining a room over federation/
2015-10-02 09:18:44 +01:00
Mark Haines
f31014b18f Start updating the sync API to match the specification 2015-10-01 17:53:07 +01:00
Daniel Wagner-Hall
5b3e9713dd Implement third party identifier invites 2015-10-01 17:49:52 +01:00
Kegsay
b43930d4c9 Merge pull request #291 from matrix-org/receipt-validation
Validate the receipt type before passing it on to the receipt handler
2015-10-01 14:44:39 +01:00
Kegan Dougal
bad780a197 Validate the receipt type before passing it on to the receipt handler 2015-10-01 14:01:52 +01:00
Erik Johnston
0a4b7226fc Don't change cwd in synctl 2015-10-01 09:21:36 +01:00
Erik Johnston
0ec78b360c Merge pull request #287 from matrix-org/erikj/canonical_alias
Set m.room.canonical_alias on room creation.
2015-09-30 17:14:55 +01:00
Erik Johnston
ecd0c0dfc5 Remove double indentation 2015-09-30 16:46:24 +01:00
Erik Johnston
83892d0d30 Comment 2015-09-30 16:41:48 +01:00
Erik Johnston
9d39615b7d Rename var 2015-09-30 16:37:59 +01:00
Mark Haines
301141515a Merge pull request #288 from matrix-org/markjh/unused_definitions
Remove some of the unused definitions from synapse
2015-09-28 14:22:44 +01:00
Daniel Wagner-Hall
741777235c Merge pull request #290 from matrix-org/daniel/synctl
Allow config file path to be configurable in in synctl
2015-09-28 10:30:45 +01:00
Erik Johnston
a14665bde7 Merge branch 'develop' of github.com:matrix-org/synapse into erikj/invite_state 2015-09-25 11:38:28 +01:00
Daniel Wagner-Hall
f87a11e0fd Fix restart 2015-09-24 21:59:38 +00:00
Daniel Wagner-Hall
76328b85f6 Allow config file path to be configurable in in synctl
Also, allow it to be run from directories other than the synapse directory
2015-09-24 21:50:20 +00:00
Erik Johnston
17795161c3 Merge pull request #289 from matrix-org/markjh/fix_sql
Fix order of ON constraints in _get_rooms_for_user_where_membership
2015-09-24 17:39:47 +01:00
Mark Haines
cf1100887b Fix order of ON constraints in _get_rooms_for_user_where_membership_is_txn 2015-09-24 17:35:10 +01:00
Mark Haines
314aabba82 Fix scripts-dev/definitions.py argparse options 2015-09-23 10:45:33 +01:00
Mark Haines
7d55314277 Remove unused _execute_and_decode from scripts/synapse_port_db 2015-09-23 10:42:02 +01:00
Mark Haines
1cd65a8d1e synapse/storage/state.py: _make_group_id was unused 2015-09-23 10:37:58 +01:00
Mark Haines
973ebb66ba Remove unused functions from synapse/storage/signatures.py 2015-09-23 10:36:33 +01:00
Mark Haines
e51aa4be96 synapse/storage/roommember.py:_get_members_query was unused 2015-09-23 10:35:10 +01:00
Mark Haines
92d8d724c5 Remove unused functions from synapse/storage/events.py 2015-09-23 10:33:06 +01:00
Mark Haines
c292dba70c Remove unused functions from synapse/storage/event_federation.py 2015-09-23 10:31:25 +01:00
Mark Haines
396834f1c0 synapse/storage/_base.py:_simple_max_id was unused 2015-09-23 10:30:38 +01:00
Mark Haines
1d9036aff2 synapse/storage/_base.py:_simple_delete was unused 2015-09-23 10:30:25 +01:00
Mark Haines
1ee3d26432 synapse/storage/_base.py:_simple_selectupdate_one was unused 2015-09-23 10:30:03 +01:00
Mark Haines
82b8d4b86a synapse/state.py:_get_state_key_from_event was unused 2015-09-23 10:27:47 +01:00
Mark Haines
57338a9768 synapse/handlers/room.py:_should_invite_join was unused 2015-09-23 10:26:45 +01:00
Mark Haines
60728c8c9e synapse/handlers/federation.py:_handle_auth_events was unused 2015-09-23 10:25:26 +01:00
Mark Haines
04abf53a56 Use argparse for definition finder 2015-09-23 10:17:50 +01:00
Erik Johnston
257fa1c53e Set m.room.canonical_alias on room creation. 2015-09-23 10:07:31 +01:00
Erik Johnston
8a519ac76d Fix demo/start.sh to work with --report-stats 2015-09-23 09:55:24 +01:00
Erik Johnston
d2fc591619 Merge pull request #282 from matrix-org/erikj/missing_keys
Fix bug where we sometimes didn't fetch all the keys requested for a server.
2015-09-23 09:22:01 +01:00
Erik Johnston
dc6094b908 Merge pull request #271 from matrix-org/erikj/default_history
Change default history visibility for private rooms
2015-09-23 09:21:00 +01:00
Mark Haines
3559a835a2 synapse/storage/event_federation.py:_get_auth_events is unused 2015-09-22 18:39:46 +01:00
Mark Haines
7dd4f79c49 synapse/storage/_base.py:_execute_and_decode was unused 2015-09-22 18:37:07 +01:00
Mark Haines
bb4dddd6c4 Move NullSource out of synapse and into tests since it is only used by the tests 2015-09-22 18:33:34 +01:00
Mark Haines
7a5818ed81 Note that GzipFile was removed in comment that referenced it 2015-09-22 18:27:22 +01:00
Mark Haines
184ba0968a synapse/app/homeserver.py:GzipFile was unused 2015-09-22 18:25:30 +01:00
Mark Haines
a247729806 synapse/streams/events.py:StreamSource was unused 2015-09-22 18:19:49 +01:00
Mark Haines
f2fcc0a8cf synapse/api/errors.py:RoomError was unused 2015-09-22 18:18:45 +01:00
Mark Haines
372ac60375 synapse/util/__init__.py:unwrap_deferred was unused 2015-09-22 18:16:07 +01:00
Mark Haines
527d95dea0 synapse/storage/_base.py:Table was unused 2015-09-22 18:14:15 +01:00
Mark Haines
cc3ab0c214 Add dev script for finding where functions are called from, and finding functions that aren't called at all 2015-09-22 18:13:06 +01:00
Mark Haines
ca2abf9a6e Merge pull request #286 from matrix-org/markjh/stream_config_repr
Define __repr__ methods for StreamConfig and PaginationConfig
2015-09-22 15:19:53 +01:00
Mark Haines
b35baf6f3c Define __repr__ methods for StreamConfig and PaginationConfig
So that they can be used with "%r" log formats.
2015-09-22 15:13:10 +01:00
Daniel Wagner-Hall
f17aadd1b5 Merge pull request #285 from matrix-org/daniel/metrics-2
Implement configurable stats reporting
2015-09-22 13:59:37 +01:00
Daniel Wagner-Hall
6d59ffe1ce Add some docstrings 2015-09-22 13:47:40 +01:00
Daniel Wagner-Hall
b6e0303c83 Catch stats-reporting errors 2015-09-22 13:34:29 +01:00
Daniel Wagner-Hall
eb011cd99b Add docstring 2015-09-22 13:29:36 +01:00
Daniel Wagner-Hall
6d7f291b93 Front-load spaces 2015-09-22 13:13:07 +01:00
Daniel Wagner-Hall
7213588083 Implement configurable stats reporting
SYN-287

This requires that HS owners either opt in or out of stats reporting.

When --generate-config is passed, --report-stats must be specified
If an already-generated config is used, and doesn't have the
report_stats key, it is requested to be set.
2015-09-22 12:57:40 +01:00
Mark Haines
ee2d722f0f Merge pull request #276 from matrix-org/markjh/history_for_rooms_that_have_been_left
SPEC-216: Allow users to view the history of rooms that they have left.
2015-09-21 14:38:13 +01:00
Mark Haines
49c0a0b5c4 Clarify that room_initial_sync returns a python dict 2015-09-21 14:21:03 +01:00
Mark Haines
95c304e3f9 Fix doc string to point at the right class 2015-09-21 14:18:47 +01:00
Mark Haines
0c16285989 Add explicit "elif event.membership == Membership.LEAVE" for clarity 2015-09-21 14:17:16 +01:00
Mark Haines
1e101ed4a4 Clamp the "to" token for /rooms/{roomId}/messages to when the user left
the room.

There isn't a way for the client to learn a valid "to" token for a room
that they have left in the C-S API but that doesn't stop a client making
one up.
2015-09-21 14:13:10 +01:00
Mark Haines
8e3bbc9bd0 Clarify which event is returned by check_user_was_in_room 2015-09-21 13:47:44 +01:00
Mark Haines
0b5c9adeb5 Merge pull request #267 from matrix-org/markjh/missing_requirements
Print an example "pip install" line for a missing requirement
2015-09-18 18:52:08 +01:00
Erik Johnston
afe475e9be Merge branch 'develop' of github.com:matrix-org/synapse into erikj/atomic_join_federation 2015-09-17 10:28:55 +01:00
Erik Johnston
b105996fc1 Remove run_on_reactor 2015-09-17 10:28:36 +01:00
Erik Johnston
51b2448e05 Revert change of scripts/check_auth.py 2015-09-17 10:26:03 +01:00
Erik Johnston
c34ffd2736 Fix getting an event for a room the server forgot it was in 2015-09-17 10:26:03 +01:00
Erik Johnston
54e688277a Also persist state 2015-09-17 10:26:03 +01:00
Erik Johnston
3a01901d6c Capture err 2015-09-17 10:26:03 +01:00
Erik Johnston
744e7d2790 Also handle state 2015-09-17 10:26:03 +01:00
Erik Johnston
a3e332af19 Don't bail out of joining if we encounter a rejected event 2015-09-17 10:26:03 +01:00
Erik Johnston
4678055173 Refactor do_invite_join 2015-09-17 10:24:51 +01:00
Erik Johnston
ffe8cf7e59 Fix bug where we sometimes didn't fetch all the keys requested for a
server.
2015-09-17 10:21:32 +01:00
Erik Johnston
eb700cdc38 Merge branch 'master' of github.com:matrix-org/synapse into develop 2015-09-16 11:05:34 +01:00
Erik Johnston
16026e60c5 Merge branch 'hotfixes-v0.10.0-r2' of github.com:matrix-org/synapse 2015-09-16 09:56:15 +01:00
Erik Johnston
0b1a55c60a Update changelog 2015-09-16 09:55:44 +01:00
Erik Johnston
663b96ae96 Merge branch 'erikj/update_extremeties' into hotfixes-v0.10.0-r2 2015-09-16 09:54:42 +01:00
Erik Johnston
2048388cfd Merge pull request #281 from matrix-org/erikj/update_extremeties
When updating a stored event from outlier to non-outlier, remember to update the extremeties
2015-09-15 16:57:25 +01:00
Erik Johnston
8148c48f11 "Comments" 2015-09-15 16:54:48 +01:00
Daniel Wagner-Hall
2c8f16257a Merge pull request #272 from matrix-org/daniel/insecureclient
Allow configuration to ignore invalid SSL certs
2015-09-15 16:52:38 +01:00
Erik Johnston
1107e83b54 Merge branch 'master' of github.com:matrix-org/synapse into develop 2015-09-15 16:35:34 +01:00
Erik Johnston
3b05b67c89 When updating a stored event from outlier to non-outlier, remember to update the extremeties 2015-09-15 16:34:42 +01:00
Daniel Wagner-Hall
d4af08a167 Use shorter config key name 2015-09-15 15:50:13 +01:00
Daniel Wagner-Hall
3bcbabc9fb Rename context factory
Mjark is officially no fun.
2015-09-15 15:46:22 +01:00
Daniel Wagner-Hall
9fc0aad567 Merge branch 'master' into daniel/insecureclient 2015-09-15 15:42:44 +01:00
Paul Evans
929ae19d00 Merge pull request #280 from matrix-org/paul/sighup
Hacky attempt at catching SIGHUP and rotating the logfile around
2015-09-15 10:47:40 +01:00
Paul "LeoNerd" Evans
9cd5b9a802 Hacky attempt at catching SIGHUP and rotating the logfile around 2015-09-14 19:03:53 +01:00
Daniel Wagner-Hall
728d07c8c1 Merge pull request #256 from matrix-org/auth
Attempt to validate macaroons
2015-09-14 18:09:33 +01:00
Erik Johnston
d59acb8c5b Merge branch 'develop' of github.com:matrix-org/synapse into erikj/unfederatable 2015-09-14 18:05:31 +01:00
Mark Haines
e2054ce21a Allow users to GET individual state events for rooms that they have left 2015-09-10 15:06:47 +01:00
Erik Johnston
49ae42bbe1 Bundle in some room state in the unsigned bit of the invite when sending to invited servers 2015-09-10 14:25:54 +01:00
Erik Johnston
4ba8189b74 Bump change log 2015-09-10 10:45:22 +01:00
David Baker
ca32c7a065 Fix adding threepids to an existing account 2015-09-10 10:44:56 +01:00
Erik Johnston
3f60481655 Bump version and change log 2015-09-10 09:58:32 +01:00
Erik Johnston
e1eb1f3fb9 Various bug fixes to crypto.keyring 2015-09-10 09:48:12 +01:00
Mark Haines
09cb5c7d33 Allow users that have left a room to get the messages that happend in the room before they left 2015-09-09 17:31:09 +01:00
Mark Haines
3c166a24c5 Remove undocumented and unimplemented 'feedback' parameter from the Client-Server API 2015-09-09 16:05:09 +01:00
Mark Haines
bc8b25eb56 Allow users that have left the room to view the member list from the point they left 2015-09-09 15:42:16 +01:00
Daniel Wagner-Hall
2c746382e0 Merge branch 'daniel/insecureclient' into develop 2015-09-09 14:27:30 +01:00
Mark Haines
1d579df664 Allow rooms/{roomId}/state for a room that has been left 2015-09-09 14:12:24 +01:00
Daniel Wagner-Hall
ddfe30ba83 Better document the intent of the insecure SSL setting 2015-09-09 13:26:23 +01:00
Mark Haines
89ae0166de Allow room initialSync for users that have left the room, returning a snapshot of how the room was when they left it 2015-09-09 13:25:22 +01:00
Daniel Wagner-Hall
6485f03d91 Fix random formatting 2015-09-09 13:05:00 +01:00
Daniel Wagner-Hall
81a93ddcc8 Allow configuration to ignore invalid SSL certs
This will be useful for sytest, and sytest only, hence the aggressive
config key name.
2015-09-09 12:02:07 +01:00
Erik Johnston
e530208e68 Change default history visibility for private rooms 2015-09-09 09:57:49 +01:00
Mark Haines
dd42bb78d0 Include rooms that a user has left in an initialSync. Include the state and messages at the point they left the room 2015-09-08 18:16:09 +01:00
Mark Haines
417485eefa Include the event_id and stream_ordering of membership events when looking up which rooms a user is in 2015-09-08 18:14:54 +01:00
Erik Johnston
2ff439cff7 Bump version/changelog 2015-09-08 11:01:48 +01:00
Mark Haines
8bab7abddd Add nacl.bindings to the list of modules checked. Re-arrange import order to check packages after the packages they depend on 2015-09-01 16:51:10 +01:00
Mark Haines
3cdfd37d95 Print an example "pip install" line for a missing requirement 2015-09-01 16:47:26 +01:00
Erik Johnston
9b05ef6f39 Also check the domains for membership state_keys 2015-09-01 16:17:25 +01:00
Erik Johnston
187320b019 Merge branch 'erikj/check_room_exists' into erikj/unfederatable 2015-09-01 15:58:10 +01:00
Erik Johnston
b345853918 Check against sender rather than event_id 2015-09-01 15:57:35 +01:00
Erik Johnston
a88e16152f Add flag which disables federation of the room 2015-09-01 15:47:30 +01:00
Daniel Wagner-Hall
e255c2c32f s/user_id/user/g for consistency 2015-09-01 12:41:16 +01:00
Daniel Wagner-Hall
81450fded8 Turn TODO into thing which actually will fail 2015-08-26 13:56:01 +01:00
Daniel Wagner-Hall
37f0ddca5f Merge branch 'mergeeriksmadness' into auth 2015-08-26 13:45:06 +01:00
Daniel Wagner-Hall
6a4b650d8a Attempt to validate macaroons
A couple of weird caveats:
 * If we can't validate your macaroon, we fall back to checking that
   your access token is in the DB, and ignoring the failure
 * Even if we can validate your macaroon, we still have to hit the DB to
   get the access token ID, which we pretend is a device ID all over the
   codebase.

This mostly adds the interesting code, and points out the two pieces we
need to delete (and necessary conditions) in order to fix the above
caveats.
2015-08-26 13:22:23 +01:00
181 changed files with 10238 additions and 3261 deletions

View File

@@ -44,4 +44,10 @@ Eric Myhre <hash at exultant.us>
repository API. repository API.
Muthu Subramanian <muthu.subramanian.karunanidhi at ericsson.com> Muthu Subramanian <muthu.subramanian.karunanidhi at ericsson.com>
* Add SAML2 support for registration and logins. * Add SAML2 support for registration and login.
Steven Hammerton <steven.hammerton at openmarket.com>
* Add CAS support for registration and login.
Mads Robin Christensen <mads at v42 dot dk>
* CentOS 7 installation instructions.

View File

@@ -1,3 +1,163 @@
Changes in synapse v0.12.0 (2016-01-04)
=======================================
* Expose ``/login`` under ``r0`` (PR #459)
Changes in synapse v0.12.0-rc3 (2015-12-23)
===========================================
* Allow guest accounts access to ``/sync`` (PR #455)
* Allow filters to include/exclude rooms at the room level
rather than just from the components of the sync for each
room. (PR #454)
* Include urls for room avatars in the response to ``/publicRooms`` (PR #453)
* Don't set a identicon as the avatar for a user when they register (PR #450)
* Add a ``display_name`` to third-party invites (PR #449)
* Send more information to the identity server for third-party invites so that
it can send richer messages to the invitee (PR #446)
* Cache the responses to ``/intialSync`` for 5 minutes. If a client
retries a request to ``/initialSync`` before the a response was computed
to the first request then the same response is used for both requests
(PR #457)
* Fix a bug where synapse would always request the signing keys of
remote servers even when the key was cached locally (PR #452)
* Fix 500 when pagination search results (PR #447)
* Fix a bug where synapse was leaking raw email address in third-party invites
(PR #448)
Changes in synapse v0.12.0-rc2 (2015-12-14)
===========================================
* Add caches for whether rooms have been forgotten by a user (PR #434)
* Remove instructions to use ``--process-dependency-link`` since all of the
dependencies of synapse are on PyPI (PR #436)
* Parallelise the processing of ``/sync`` requests (PR #437)
* Fix race updating presence in ``/events`` (PR #444)
* Fix bug back-populating search results (PR #441)
* Fix bug calculating state in ``/sync`` requests (PR #442)
Changes in synapse v0.12.0-rc1 (2015-12-10)
===========================================
* Host the client APIs released as r0 by
https://matrix.org/docs/spec/r0.0.0/client_server.html
on paths prefixed by ``/_matrix/client/r0``. (PR #430, PR #415, PR #400)
* Updates the client APIs to match r0 of the matrix specification.
* All APIs return events in the new event format, old APIs also include
the fields needed to parse the event using the old format for
compatibility. (PR #402)
* Search results are now given as a JSON array rather than
a JSON object (PR #405)
* Miscellaneous changes to search (PR #403, PR #406, PR #412)
* Filter JSON objects may now be passed as query parameters to ``/sync``
(PR #431)
* Fix implementation of ``/admin/whois`` (PR #418)
* Only include the rooms that user has left in ``/sync`` if the client
requests them in the filter (PR #423)
* Don't push for ``m.room.message`` by default (PR #411)
* Add API for setting per account user data (PR #392)
* Allow users to forget rooms (PR #385)
* Performance improvements and monitoring:
* Add per-request counters for CPU time spent on the main python thread.
(PR #421, PR #420)
* Add per-request counters for time spent in the database (PR #429)
* Make state updates in the C+S API idempotent (PR #416)
* Only fire ``user_joined_room`` if the user has actually joined. (PR #410)
* Reuse a single http client, rather than creating new ones (PR #413)
* Fixed a bug upgrading from older versions of synapse on postgresql (PR #417)
Changes in synapse v0.11.1 (2015-11-20)
=======================================
* Add extra options to search API (PR #394)
* Fix bug where we did not correctly cap federation retry timers. This meant it
could take several hours for servers to start talking to ressurected servers,
even when they were receiving traffic from them (PR #393)
* Don't advertise login token flow unless CAS is enabled. This caused issues
where some clients would always use the fallback API if they did not
recognize all login flows (PR #391)
* Change /v2 sync API to rename ``private_user_data`` to ``account_data``
(PR #386)
* Change /v2 sync API to remove the ``event_map`` and rename keys in ``rooms``
object (PR #389)
Changes in synapse v0.11.0-r2 (2015-11-19)
==========================================
* Fix bug in database port script (PR #387)
Changes in synapse v0.11.0-r1 (2015-11-18)
==========================================
* Retry and fail federation requests more aggressively for requests that block
client side requests (PR #384)
Changes in synapse v0.11.0 (2015-11-17)
=======================================
* Change CAS login API (PR #349)
Changes in synapse v0.11.0-rc2 (2015-11-13)
===========================================
* Various changes to /sync API response format (PR #373)
* Fix regression when setting display name in newly joined room over
federation (PR #368)
* Fix problem where /search was slow when using SQLite (PR #366)
Changes in synapse v0.11.0-rc1 (2015-11-11)
===========================================
* Add Search API (PR #307, #324, #327, #336, #350, #359)
* Add 'archived' state to v2 /sync API (PR #316)
* Add ability to reject invites (PR #317)
* Add config option to disable password login (PR #322)
* Add the login fallback API (PR #330)
* Add room context API (PR #334)
* Add room tagging support (PR #335)
* Update v2 /sync API to match spec (PR #305, #316, #321, #332, #337, #341)
* Change retry schedule for application services (PR #320)
* Change retry schedule for remote servers (PR #340)
* Fix bug where we hosted static content in the incorrect place (PR #329)
* Fix bug where we didn't increment retry interval for remote servers (PR #343)
Changes in synapse v0.10.1-rc1 (2015-10-15)
===========================================
* Add support for CAS, thanks to Steven Hammerton (PR #295, #296)
* Add support for using macaroons for ``access_token`` (PR #256, #229)
* Add support for ``m.room.canonical_alias`` (PR #287)
* Add support for viewing the history of rooms that they have left. (PR #276,
#294)
* Add support for refresh tokens (PR #240)
* Add flag on creation which disables federation of the room (PR #279)
* Add some room state to invites. (PR #275)
* Atomically persist events when joining a room over federation (PR #283)
* Change default history visibility for private rooms (PR #271)
* Allow users to redact their own sent events (PR #262)
* Use tox for tests (PR #247)
* Split up syutil into separate libraries (PR #243)
Changes in synapse v0.10.0-r2 (2015-09-16)
==========================================
* Fix bug where we always fetched remote server signing keys instead of using
ones in our cache.
* Fix adding threepids to an existing account.
* Fix bug with invinting over federation where remote server was already in
the room. (PR #281, SYN-392)
Changes in synapse v0.10.0-r1 (2015-09-08)
==========================================
* Fix bug with python packaging
Changes in synapse v0.10.0 (2015-09-03) Changes in synapse v0.10.0 (2015-09-03)
======================================= =======================================

View File

@@ -15,8 +15,11 @@ recursive-include scripts *
recursive-include scripts-dev * recursive-include scripts-dev *
recursive-include tests *.py recursive-include tests *.py
recursive-include static *.css recursive-include synapse/static *.css
recursive-include static *.html recursive-include synapse/static *.gif
recursive-include static *.js recursive-include synapse/static *.html
recursive-include synapse/static *.js
exclude jenkins.sh
prune demo/etc prune demo/etc

View File

@@ -20,8 +20,8 @@ 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 the web client at http://matrix.org/beta or via an IRC bridge at accessed by any client from https://matrix.org/blog/try-matrix-now or via IRC
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!
@@ -77,14 +77,14 @@ Meanwhile, iOS and Android SDKs and clients are available from:
- https://github.com/matrix-org/matrix-android-sdk - 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/beta), run a homeserver, take a look at the Matrix spec at https://matrix.org/blog/try-matrix-now), run a homeserver, take a look at the
https://matrix.org/docs/spec and API docs at https://matrix.org/docs/api, Matrix spec at https://matrix.org/docs/spec and API docs at
experiment with the APIs and the demo clients, and report any bugs via https://matrix.org/docs/api, experiment with the APIs and the demo clients, and
https://matrix.org/jira. report any bugs via https://matrix.org/jira.
Thanks for using Matrix! Thanks for using Matrix!
[1] End-to-end encryption is currently in development [1] End-to-end encryption is currently in development - see https://matrix.org/git/olm
Synapse Installation Synapse Installation
==================== ====================
@@ -111,6 +111,14 @@ Installing prerequisites on ArchLinux::
sudo pacman -S base-devel python2 python-pip \ sudo pacman -S base-devel python2 python-pip \
python-setuptools python-virtualenv sqlite3 python-setuptools python-virtualenv sqlite3
Installing prerequisites on CentOS 7::
sudo yum install libtiff-devel libjpeg-devel libzip-devel freetype-devel \
lcms2-devel libwebp-devel tcl-devel tk-devel \
python-virtualenv libffi-devel openssl-devel
sudo yum groupinstall "Development Tools"
Installing prerequisites on Mac OS X:: Installing prerequisites on Mac OS X::
xcode-select --install xcode-select --install
@@ -122,7 +130,7 @@ To install the synapse homeserver run::
virtualenv -p python2.7 ~/.synapse virtualenv -p python2.7 ~/.synapse
source ~/.synapse/bin/activate source ~/.synapse/bin/activate
pip install --upgrade setuptools pip install --upgrade setuptools
pip install --process-dependency-links https://github.com/matrix-org/synapse/tarball/master pip install https://github.com/matrix-org/synapse/tarball/master
This installs synapse, along with the libraries it uses, into a virtual 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
@@ -133,15 +141,25 @@ 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/.
Another alternative is to install via apt from http://matrix.org/packages/debian/.
Note that these packages do not include a client - choose one from
https://matrix.org/blog/try-matrix-now/ (or build your own with
https://github.com/matrix-org/matrix-js-sdk/).
Finally, 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
for details.
To set up your homeserver, run (in your virtualenv, as before):: To set up your homeserver, run (in your virtualenv, as before)::
cd ~/.synapse cd ~/.synapse
python -m synapse.app.homeserver \ python -m synapse.app.homeserver \
--server-name machine.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]
Substituting your host and domain name as appropriate. ...substituting your host and domain name as appropriate.
This 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
@@ -154,10 +172,11 @@ key in the <server name>.signing.key file (the second word, which by default is
By default, registration of new users is disabled. You can either enable By default, registration of new users is disabled. You can either enable
registration in the config by specifying ``enable_registration: true`` registration in the config by specifying ``enable_registration: true``
(it is then recommended to also set up CAPTCHA), or (it is then recommended to also set up CAPTCHA - see docs/CAPTCHA_SETUP), or
you can use the command line to register new users:: you can use the command line to register new users::
$ source ~/.synapse/bin/activate $ source ~/.synapse/bin/activate
$ synctl start # if not already running
$ register_new_matrix_user -c homeserver.yaml https://localhost:8448 $ register_new_matrix_user -c homeserver.yaml https://localhost:8448
New user localpart: erikj New user localpart: erikj
Password: Password:
@@ -167,6 +186,16 @@ you can use the command line to register new users::
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
===============
To actually run your new homeserver, pick a working directory for Synapse to
run (e.g. ``~/.synapse``), and::
cd ~/.synapse
source ./bin/activate
synctl start
Using PostgreSQL Using PostgreSQL
================ ================
@@ -189,16 +218,6 @@ may have a few regressions relative to SQLite.
For information on how to install and use PostgreSQL, please see For information on how to install and use PostgreSQL, please see
`docs/postgres.rst <docs/postgres.rst>`_. `docs/postgres.rst <docs/postgres.rst>`_.
Running Synapse
===============
To actually run your new homeserver, pick a working directory for Synapse to
run (e.g. ``~/.synapse``), and::
cd ~/.synapse
source ./bin/activate
synctl start
Platform Specific Instructions Platform Specific Instructions
============================== ==============================
@@ -220,8 +239,7 @@ pip may be outdated (6.0.7-1 and needs to be upgraded to 6.0.8-1 )::
You also may need to explicitly specify python 2.7 again during the install You also may need to explicitly specify python 2.7 again during the install
request:: request::
pip2.7 install --process-dependency-links \ pip2.7 install https://github.com/matrix-org/synapse/tarball/master
https://github.com/matrix-org/synapse/tarball/master
If you encounter an error with lib bcrypt causing an Wrong ELF Class: If you encounter an error with lib bcrypt causing an Wrong ELF Class:
ELFCLASS32 (x64 Systems), you may need to reinstall py-bcrypt to correctly ELFCLASS32 (x64 Systems), you may need to reinstall py-bcrypt to correctly
@@ -280,8 +298,7 @@ Troubleshooting
Troubleshooting Installation Troubleshooting Installation
---------------------------- ----------------------------
Synapse requires pip 1.7 or later, so if your OS provides too old a version and Synapse requires pip 1.7 or later, so if your OS provides too old a version you
you get errors about ``error: no such option: --process-dependency-links`` you
may need to manually upgrade it:: may need to manually upgrade it::
sudo pip install --upgrade pip sudo pip install --upgrade pip
@@ -425,6 +442,10 @@ SRV record, as that is the name other machines will expect it to have::
python -m synapse.app.homeserver --config-path homeserver.yaml 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 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. increase the verbosity of logging output; at least for initial testing.

View File

@@ -30,6 +30,19 @@ running:
python synapse/python_dependencies.py | xargs -n1 pip install python synapse/python_dependencies.py | xargs -n1 pip install
Upgrading to v0.11.0
====================
This release includes the option to send anonymous usage stats to matrix.org,
and requires that administrators explictly opt in or out by setting the
``report_stats`` option to either ``true`` or ``false``.
We would really appreciate it if you could help our project out by reporting
anonymized usage statistics from your homeserver. Only very basic aggregate
data (e.g. number of users) will be reported, but it helps us to track the
growth of the Matrix community, and helps us to make Matrix a success, as well
as to convince other networks that they should peer with us.
Upgrading to v0.9.0 Upgrading to v0.9.0
=================== ===================

View File

@@ -25,6 +25,7 @@ for port in 8080 8081 8082; do
--generate-config \ --generate-config \
-H "localhost:$https_port" \ -H "localhost:$https_port" \
--config-path "$DIR/etc/$port.config" \ --config-path "$DIR/etc/$port.config" \
--report-stats no
# Check script parameters # Check script parameters
if [ $# -eq 1 ]; then if [ $# -eq 1 ]; then
@@ -37,6 +38,13 @@ for port in 8080 8081 8082; do
perl -p -i -e 's/^enable_registration:.*/enable_registration: true/g' $DIR/etc/$port.config perl -p -i -e 's/^enable_registration:.*/enable_registration: true/g' $DIR/etc/$port.config
if ! grep -F "full_twisted_stacktraces" -q $DIR/etc/$port.config; then
echo "full_twisted_stacktraces: true" >> $DIR/etc/$port.config
fi
if ! grep -F "report_stats" -q $DIR/etc/$port.config ; then
echo "report_stats: false" >> $DIR/etc/$port.config
fi
python -m synapse.app.homeserver \ python -m synapse.app.homeserver \
--config-path "$DIR/etc/$port.config" \ --config-path "$DIR/etc/$port.config" \
-D \ -D \

View File

@@ -18,8 +18,8 @@ encoding use, e.g.::
This would create an appropriate database named ``synapse`` owned by the This would create an appropriate database named ``synapse`` owned by the
``synapse_user`` user (which must already exist). ``synapse_user`` user (which must already exist).
Set up client Set up client in Debian/Ubuntu
============= ===========================
Postgres support depends on the postgres python connector ``psycopg2``. In the Postgres support depends on the postgres python connector ``psycopg2``. In the
virtual env:: virtual env::
@@ -27,6 +27,19 @@ virtual env::
sudo apt-get install libpq-dev sudo apt-get install libpq-dev
pip install psycopg2 pip install psycopg2
Set up client in RHEL/CentOs 7
==============================
Make sure you have the appropriate version of postgres-devel installed. For a
postgres 9.4, use the postgres 9.4 packages from
[here](https://wiki.postgresql.org/wiki/YUM_Installation).
As with Debian/Ubuntu, postgres support depends on the postgres python connector
``psycopg2``. In the virtual env::
sudo yum install postgresql-devel libpqxx-devel.x86_64
export PATH=/usr/pgsql-9.4/bin/:$PATH
pip install psycopg2
Synapse config Synapse config
============== ==============

81
jenkins.sh Executable file
View File

@@ -0,0 +1,81 @@
#!/bin/bash -eu
export PYTHONDONTWRITEBYTECODE=yep
# 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)
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

175
scripts-dev/definitions.py Executable file
View File

@@ -0,0 +1,175 @@
#! /usr/bin/python
import ast
import yaml
class DefinitionVisitor(ast.NodeVisitor):
def __init__(self):
super(DefinitionVisitor, self).__init__()
self.functions = {}
self.classes = {}
self.names = {}
self.attrs = set()
self.definitions = {
'def': self.functions,
'class': self.classes,
'names': self.names,
'attrs': self.attrs,
}
def visit_Name(self, node):
self.names.setdefault(type(node.ctx).__name__, set()).add(node.id)
def visit_Attribute(self, node):
self.attrs.add(node.attr)
for child in ast.iter_child_nodes(node):
self.visit(child)
def visit_ClassDef(self, node):
visitor = DefinitionVisitor()
self.classes[node.name] = visitor.definitions
for child in ast.iter_child_nodes(node):
visitor.visit(child)
def visit_FunctionDef(self, node):
visitor = DefinitionVisitor()
self.functions[node.name] = visitor.definitions
for child in ast.iter_child_nodes(node):
visitor.visit(child)
def non_empty(defs):
functions = {name: non_empty(f) for name, f in defs['def'].items()}
classes = {name: non_empty(f) for name, f in defs['class'].items()}
result = {}
if functions: result['def'] = functions
if classes: result['class'] = classes
names = defs['names']
uses = []
for name in names.get('Load', ()):
if name not in names.get('Param', ()) and name not in names.get('Store', ()):
uses.append(name)
uses.extend(defs['attrs'])
if uses: result['uses'] = uses
result['names'] = names
result['attrs'] = defs['attrs']
return result
def definitions_in_code(input_code):
input_ast = ast.parse(input_code)
visitor = DefinitionVisitor()
visitor.visit(input_ast)
definitions = non_empty(visitor.definitions)
return definitions
def definitions_in_file(filepath):
with open(filepath) as f:
return definitions_in_code(f.read())
def defined_names(prefix, defs, names):
for name, funcs in defs.get('def', {}).items():
names.setdefault(name, {'defined': []})['defined'].append(prefix + name)
defined_names(prefix + name + ".", funcs, names)
for name, funcs in defs.get('class', {}).items():
names.setdefault(name, {'defined': []})['defined'].append(prefix + name)
defined_names(prefix + name + ".", funcs, names)
def used_names(prefix, item, defs, names):
for name, funcs in defs.get('def', {}).items():
used_names(prefix + name + ".", name, funcs, names)
for name, funcs in defs.get('class', {}).items():
used_names(prefix + name + ".", name, funcs, names)
for used in defs.get('uses', ()):
if used in names:
names[used].setdefault('used', {}).setdefault(item, []).append(prefix.rstrip('.'))
if __name__ == '__main__':
import sys, os, argparse, re
parser = argparse.ArgumentParser(description='Find definitions.')
parser.add_argument(
"--unused", action="store_true", help="Only list unused definitions"
)
parser.add_argument(
"--ignore", action="append", metavar="REGEXP", help="Ignore a pattern"
)
parser.add_argument(
"--pattern", action="append", metavar="REGEXP",
help="Search for a pattern"
)
parser.add_argument(
"directories", nargs='+', metavar="DIR",
help="Directories to search for definitions"
)
parser.add_argument(
"--referrers", default=0, type=int,
help="Include referrers up to the given depth"
)
parser.add_argument(
"--format", default="yaml",
help="Output format, one of 'yaml' or 'dot'"
)
args = parser.parse_args()
definitions = {}
for directory in args.directories:
for root, dirs, files in os.walk(directory):
for filename in files:
if filename.endswith(".py"):
filepath = os.path.join(root, filename)
definitions[filepath] = definitions_in_file(filepath)
names = {}
for filepath, defs in definitions.items():
defined_names(filepath + ":", defs, names)
for filepath, defs in definitions.items():
used_names(filepath + ":", None, defs, names)
patterns = [re.compile(pattern) for pattern in args.pattern or ()]
ignore = [re.compile(pattern) for pattern in args.ignore or ()]
result = {}
for name, definition in names.items():
if patterns and not any(pattern.match(name) for pattern in patterns):
continue
if ignore and any(pattern.match(name) for pattern in ignore):
continue
if args.unused and definition.get('used'):
continue
result[name] = definition
referrer_depth = args.referrers
referrers = set()
while referrer_depth:
referrer_depth -= 1
for entry in result.values():
for used_by in entry.get("used", ()):
referrers.add(used_by)
for name, definition in names.items():
if not name in referrers:
continue
if ignore and any(pattern.match(name) for pattern in ignore):
continue
result[name] = definition
if args.format == 'yaml':
yaml.dump(result, sys.stdout, default_flow_style=False)
elif args.format == 'dot':
print "digraph {"
for name, entry in result.items():
print name
for used_by in entry.get("used", ()):
if used_by in result:
print used_by, "->", name
print "}"
else:
raise ValueError("Unknown format %r" % (args.format))

1
scripts/gen_password Normal file
View File

@@ -0,0 +1 @@
perl -MCrypt::Random -MCrypt::Eksblowfish::Bcrypt -e 'print Crypt::Eksblowfish::Bcrypt::bcrypt("secret", "\$2\$12\$" . Crypt::Eksblowfish::Bcrypt::en_base64(Crypt::Random::makerandom_octet(Length=>16)))."\n"'

View File

@@ -68,6 +68,7 @@ APPEND_ONLY_TABLES = [
"state_groups_state", "state_groups_state",
"event_to_state_groups", "event_to_state_groups",
"rejections", "rejections",
"event_search",
] ]
@@ -95,8 +96,6 @@ class Store(object):
_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"]
_execute_and_decode = SQLBaseStore.__dict__["_execute_and_decode"]
def runInteraction(self, desc, func, *args, **kwargs): def runInteraction(self, desc, func, *args, **kwargs):
def r(conn): def r(conn):
try: try:
@@ -231,19 +230,51 @@ class Porter(object):
if rows: if rows:
next_chunk = rows[-1][0] + 1 next_chunk = rows[-1][0] + 1
self._convert_rows(table, headers, rows) if table == "event_search":
# We have to treat event_search differently since it has a
# different structure in the two different databases.
def insert(txn):
sql = (
"INSERT INTO event_search (event_id, room_id, key, sender, vector)"
" VALUES (?,?,?,?,to_tsvector('english', ?))"
)
def insert(txn): rows_dict = [
self.postgres_store.insert_many_txn( dict(zip(headers, row))
txn, table, headers[1:], rows for row in rows
) ]
self.postgres_store._simple_update_one_txn( txn.executemany(sql, [
txn, (
table="port_from_sqlite3", row["event_id"],
keyvalues={"table_name": table}, row["room_id"],
updatevalues={"rowid": next_chunk}, 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)

View File

@@ -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.10.0" __version__ = "0.12.0"

View File

@@ -14,15 +14,20 @@
# limitations under the License. # limitations under the License.
"""This module contains classes for authenticating the user.""" """This module contains classes for authenticating the user."""
from canonicaljson import encode_canonical_json
from signedjson.key import decode_verify_key_bytes
from signedjson.sign import verify_signed_json, SignatureVerifyException
from twisted.internet import defer from twisted.internet import defer
from synapse.api.constants import EventTypes, Membership, JoinRules from synapse.api.constants import EventTypes, Membership, JoinRules
from synapse.api.errors import AuthError, Codes, SynapseError from synapse.api.errors import AuthError, Codes, SynapseError, EventSizeError
from synapse.types import RoomID, UserID, EventID
from synapse.util.logutils import log_function from synapse.util.logutils import log_function
from synapse.types import UserID, EventID from unpaddedbase64 import decode_base64
import logging import logging
import pymacaroons
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -30,6 +35,7 @@ logger = logging.getLogger(__name__)
AuthEventTypes = ( AuthEventTypes = (
EventTypes.Create, EventTypes.Member, EventTypes.PowerLevels, EventTypes.Create, EventTypes.Member, EventTypes.PowerLevels,
EventTypes.JoinRules, EventTypes.RoomHistoryVisibility, EventTypes.JoinRules, EventTypes.RoomHistoryVisibility,
EventTypes.ThirdPartyInvite,
) )
@@ -40,6 +46,13 @@ class Auth(object):
self.store = hs.get_datastore() self.store = hs.get_datastore()
self.state = hs.get_state_handler() self.state = hs.get_state_handler()
self.TOKEN_NOT_FOUND_HTTP_STATUS = 401 self.TOKEN_NOT_FOUND_HTTP_STATUS = 401
self._KNOWN_CAVEAT_PREFIXES = set([
"gen = ",
"guest = ",
"type = ",
"time < ",
"user_id = ",
])
def check(self, event, auth_events): def check(self, event, auth_events):
""" Checks if this event is correctly authed. """ Checks if this event is correctly authed.
@@ -52,6 +65,8 @@ class Auth(object):
Returns: Returns:
True if the auth checks pass. True if the auth checks pass.
""" """
self.check_size_limits(event)
try: try:
if not hasattr(event, "room_id"): if not hasattr(event, "room_id"):
raise AuthError(500, "Event has no room_id: %s" % event) raise AuthError(500, "Event has no room_id: %s" % event)
@@ -73,6 +88,15 @@ class Auth(object):
"Room %r does not exist" % (event.room_id,) "Room %r does not exist" % (event.room_id,)
) )
creating_domain = RoomID.from_string(event.room_id).domain
originating_domain = UserID.from_string(event.sender).domain
if creating_domain != originating_domain:
if not self.can_federate(event, auth_events):
raise AuthError(
403,
"This room has been marked as unfederatable."
)
# FIXME: Temp hack # FIXME: Temp hack
if event.type == EventTypes.Aliases: if event.type == EventTypes.Aliases:
return True return True
@@ -110,8 +134,39 @@ class Auth(object):
logger.info("Denying! %s", event) logger.info("Denying! %s", event)
raise raise
def check_size_limits(self, 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")
@defer.inlineCallbacks @defer.inlineCallbacks
def check_joined_room(self, room_id, user_id, current_state=None): def check_joined_room(self, room_id, user_id, current_state=None):
"""Check if the user is currently joined in the room
Args:
room_id(str): The room to check.
user_id(str): The user to check.
current_state(dict): Optional map of the current state of the room.
If provided then that map is used to check whether they are a
member of the room. Otherwise the current membership is
loaded from the database.
Raises:
AuthError if the user is not in the room.
Returns:
A deferred membership event for the user if the user is in
the room.
"""
if current_state: if current_state:
member = current_state.get( member = current_state.get(
(EventTypes.Member, user_id), (EventTypes.Member, user_id),
@@ -127,6 +182,40 @@ class Auth(object):
self._check_joined_room(member, user_id, room_id) self._check_joined_room(member, user_id, room_id)
defer.returnValue(member) defer.returnValue(member)
@defer.inlineCallbacks
def check_user_was_in_room(self, room_id, user_id):
"""Check if the user was in the room at some point.
Args:
room_id(str): The room to check.
user_id(str): The user to check.
Raises:
AuthError if the user was never in the room.
Returns:
A deferred membership event for the user if the user was in the
room. This will be the join event if they are currently joined to
the room. This will be the leave event if they have left the room.
"""
member = yield self.state.get_current_state(
room_id=room_id,
event_type=EventTypes.Member,
state_key=user_id
)
membership = member.membership if member else None
if membership not in (Membership.JOIN, Membership.LEAVE):
raise AuthError(403, "User %s not in room %s" % (
user_id, room_id
))
if membership == Membership.LEAVE:
forgot = yield self.store.did_forget(user_id, room_id)
if forgot:
raise AuthError(403, "User %s not in room %s" % (
user_id, room_id
))
defer.returnValue(member)
@defer.inlineCallbacks @defer.inlineCallbacks
def check_host_in_room(self, room_id, host): def check_host_in_room(self, room_id, host):
curr_state = yield self.state.get_current_state(room_id) curr_state = yield self.state.get_current_state(room_id)
@@ -161,6 +250,11 @@ class Auth(object):
user_id, room_id, repr(member) user_id, room_id, repr(member)
)) ))
def can_federate(self, event, auth_events):
creation_event = auth_events.get((EventTypes.Create, ""))
return creation_event.content.get("m.federate", True) is True
@log_function @log_function
def is_membership_change_allowed(self, event, auth_events): def is_membership_change_allowed(self, event, auth_events):
membership = event.content["membership"] membership = event.content["membership"]
@@ -176,6 +270,15 @@ class Auth(object):
target_user_id = event.state_key target_user_id = event.state_key
creating_domain = RoomID.from_string(event.room_id).domain
target_domain = UserID.from_string(target_user_id).domain
if creating_domain != target_domain:
if not self.can_federate(event, auth_events):
raise AuthError(
403,
"This room has been marked as unfederatable."
)
# get info about the caller # get info about the caller
key = (EventTypes.Member, event.user_id, ) key = (EventTypes.Member, event.user_id, )
caller = auth_events.get(key) caller = auth_events.get(key)
@@ -221,8 +324,17 @@ class Auth(object):
} }
) )
if Membership.INVITE == membership and "third_party_invite" in event.content:
if not self._verify_third_party_invite(event, auth_events):
raise AuthError(403, "You are not invited to this room.")
return True
if Membership.JOIN != membership: if Membership.JOIN != membership:
# JOIN is the only action you can perform if you're not in the room 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 if not caller_in_room: # caller isn't joined
raise AuthError( raise AuthError(
403, 403,
@@ -286,6 +398,66 @@ class Auth(object):
return True return True
def _verify_third_party_invite(self, 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 event.user_id != invite_event.user_id:
return False
try:
public_key = invite_event.content["public_key"]
if signed["mxid"] != event.state_key:
return False
if signed["token"] != token:
return False
for server, signature_block in signed["signatures"].items():
for key_name, encoded_signature in signature_block.items():
if not key_name.startswith("ed25519:"):
return False
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
return False
except (KeyError, SignatureVerifyException,):
return False
def _get_power_level_event(self, auth_events): def _get_power_level_event(self, auth_events):
key = (EventTypes.PowerLevels, "", ) key = (EventTypes.PowerLevels, "", )
return auth_events.get(key) return auth_events.get(key)
@@ -324,7 +496,7 @@ class Auth(object):
return default return default
@defer.inlineCallbacks @defer.inlineCallbacks
def get_user_by_req(self, request): def get_user_by_req(self, request, allow_guest=False):
""" Get a registered user's ID. """ Get a registered user's ID.
Args: Args:
@@ -362,14 +534,15 @@ class Auth(object):
request.authenticated_entity = user_id request.authenticated_entity = user_id
defer.returnValue((UserID.from_string(user_id), "")) defer.returnValue((UserID.from_string(user_id), "", False))
return return
except KeyError: except KeyError:
pass # normal users won't have the user_id query parameter set. pass # normal users won't have the user_id query parameter set.
user_info = yield self.get_user_by_access_token(access_token) user_info = yield self._get_user_by_access_token(access_token)
user = user_info["user"] user = user_info["user"]
token_id = user_info["token_id"] token_id = user_info["token_id"]
is_guest = user_info["is_guest"]
ip_addr = self.hs.get_ip_from_request(request) ip_addr = self.hs.get_ip_from_request(request)
user_agent = request.requestHeaders.getRawHeaders( user_agent = request.requestHeaders.getRawHeaders(
@@ -384,9 +557,14 @@ class Auth(object):
user_agent=user_agent user_agent=user_agent
) )
if is_guest and not allow_guest:
raise AuthError(
403, "Guest access not allowed", errcode=Codes.GUEST_ACCESS_FORBIDDEN
)
request.authenticated_entity = user.to_string() request.authenticated_entity = user.to_string()
defer.returnValue((user, token_id,)) defer.returnValue((user, token_id, is_guest,))
except KeyError: except KeyError:
raise AuthError( raise AuthError(
self.TOKEN_NOT_FOUND_HTTP_STATUS, "Missing access token.", self.TOKEN_NOT_FOUND_HTTP_STATUS, "Missing access token.",
@@ -394,7 +572,7 @@ class Auth(object):
) )
@defer.inlineCallbacks @defer.inlineCallbacks
def get_user_by_access_token(self, token): def _get_user_by_access_token(self, token):
""" Get a registered user's ID. """ Get a registered user's ID.
Args: Args:
@@ -404,6 +582,112 @@ class Auth(object):
Raises: Raises:
AuthError if no user by that token exists or the token is invalid. AuthError if no user by that token exists or the token is invalid.
""" """
try:
ret = yield self._get_user_from_macaroon(token)
except AuthError:
# TODO(daniel): Remove this fallback when all existing access tokens
# have been re-issued as macaroons.
ret = yield self._look_up_user_by_access_token(token)
defer.returnValue(ret)
@defer.inlineCallbacks
def _get_user_from_macaroon(self, macaroon_str):
try:
macaroon = pymacaroons.Macaroon.deserialize(macaroon_str)
self.validate_macaroon(macaroon, "access", False)
user_prefix = "user_id = "
user = None
guest = False
for caveat in macaroon.caveats:
if caveat.caveat_id.startswith(user_prefix):
user = UserID.from_string(caveat.caveat_id[len(user_prefix):])
elif caveat.caveat_id == "guest = true":
guest = True
if user is None:
raise AuthError(
self.TOKEN_NOT_FOUND_HTTP_STATUS, "No user caveat in macaroon",
errcode=Codes.UNKNOWN_TOKEN
)
if guest:
ret = {
"user": user,
"is_guest": True,
"token_id": None,
}
else:
# This codepath exists so that we can actually return a
# token ID, because we use token IDs in place of device
# identifiers throughout the codebase.
# TODO(daniel): Remove this fallback when device IDs are
# properly implemented.
ret = yield self._look_up_user_by_access_token(macaroon_str)
if ret["user"] != user:
logger.error(
"Macaroon user (%s) != DB user (%s)",
user,
ret["user"]
)
raise AuthError(
self.TOKEN_NOT_FOUND_HTTP_STATUS,
"User mismatch in macaroon",
errcode=Codes.UNKNOWN_TOKEN
)
defer.returnValue(ret)
except (pymacaroons.exceptions.MacaroonException, TypeError, ValueError):
raise AuthError(
self.TOKEN_NOT_FOUND_HTTP_STATUS, "Invalid macaroon passed.",
errcode=Codes.UNKNOWN_TOKEN
)
def validate_macaroon(self, macaroon, type_string, verify_expiry):
"""
validate that a Macaroon is understood by and was signed by this server.
Args:
macaroon(pymacaroons.Macaroon): The macaroon to validate
type_string(str): The kind of token this is (e.g. "access", "refresh")
verify_expiry(bool): Whether to verify whether the macaroon has expired.
This should really always be True, but no clients currently implement
token refresh, so we can't enforce expiry yet.
"""
v = pymacaroons.Verifier()
v.satisfy_exact("gen = 1")
v.satisfy_exact("type = " + type_string)
v.satisfy_general(lambda c: c.startswith("user_id = "))
v.satisfy_exact("guest = true")
if verify_expiry:
v.satisfy_general(self._verify_expiry)
else:
v.satisfy_general(lambda c: c.startswith("time < "))
v.verify(macaroon, self.hs.config.macaroon_secret_key)
v = pymacaroons.Verifier()
v.satisfy_general(self._verify_recognizes_caveats)
v.verify(macaroon, self.hs.config.macaroon_secret_key)
def _verify_expiry(self, caveat):
prefix = "time < "
if not caveat.startswith(prefix):
return False
expiry = int(caveat[len(prefix):])
now = self.hs.get_clock().time_msec()
return now < expiry
def _verify_recognizes_caveats(self, caveat):
first_space = caveat.find(" ")
if first_space < 0:
return False
second_space = caveat.find(" ", first_space + 1)
if second_space < 0:
return False
return caveat[:second_space + 1] in self._KNOWN_CAVEAT_PREFIXES
@defer.inlineCallbacks
def _look_up_user_by_access_token(self, token):
ret = yield self.store.get_user_by_access_token(token) ret = yield self.store.get_user_by_access_token(token)
if not ret: if not ret:
raise AuthError( raise AuthError(
@@ -413,8 +697,8 @@ class Auth(object):
user_info = { user_info = {
"user": UserID.from_string(ret.get("name")), "user": UserID.from_string(ret.get("name")),
"token_id": ret.get("token_id", None), "token_id": ret.get("token_id", None),
"is_guest": False,
} }
defer.returnValue(user_info) defer.returnValue(user_info)
@defer.inlineCallbacks @defer.inlineCallbacks
@@ -489,6 +773,16 @@ class Auth(object):
else: else:
if member_event: if member_event:
auth_ids.append(member_event.event_id) auth_ids.append(member_event.event_id)
if e_type == Membership.INVITE:
if "third_party_invite" in event.content:
key = (
EventTypes.ThirdPartyInvite,
event.content["third_party_invite"]["signed"]["token"]
)
third_party_invite = current_state.get(key)
if third_party_invite:
auth_ids.append(third_party_invite.event_id)
elif member_event: elif member_event:
if member_event.content["membership"] == Membership.JOIN: if member_event.content["membership"] == Membership.JOIN:
auth_ids.append(member_event.event_id) auth_ids.append(member_event.event_id)
@@ -566,7 +860,7 @@ class Auth(object):
redact_level = self._get_named_level(auth_events, "redact", 50) redact_level = self._get_named_level(auth_events, "redact", 50)
if user_level > redact_level: if user_level >= redact_level:
return False return False
redacter_domain = EventID.from_string(event.event_id).domain redacter_domain = EventID.from_string(event.event_id).domain

View File

@@ -27,16 +27,6 @@ class Membership(object):
LIST = (INVITE, JOIN, KNOCK, LEAVE, BAN) LIST = (INVITE, JOIN, KNOCK, LEAVE, BAN)
class Feedback(object):
"""Represents the types of feedback a user can send in response to a
message."""
DELIVERED = u"delivered"
READ = u"read"
LIST = (DELIVERED, READ)
class PresenceState(object): class PresenceState(object):
"""Represents the presence state of a user.""" """Represents the presence state of a user."""
OFFLINE = u"offline" OFFLINE = u"offline"
@@ -73,11 +63,12 @@ class EventTypes(object):
PowerLevels = "m.room.power_levels" PowerLevels = "m.room.power_levels"
Aliases = "m.room.aliases" Aliases = "m.room.aliases"
Redaction = "m.room.redaction" Redaction = "m.room.redaction"
Feedback = "m.room.message.feedback" ThirdPartyInvite = "m.room.third_party_invite"
RoomHistoryVisibility = "m.room.history_visibility" RoomHistoryVisibility = "m.room.history_visibility"
CanonicalAlias = "m.room.canonical_alias" CanonicalAlias = "m.room.canonical_alias"
RoomAvatar = "m.room.avatar" RoomAvatar = "m.room.avatar"
GuestAccess = "m.room.guest_access"
# These are used for validation # These are used for validation
Message = "m.room.message" Message = "m.room.message"
@@ -94,3 +85,4 @@ class RejectedReason(object):
class RoomCreationPreset(object): 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"

View File

@@ -33,6 +33,7 @@ class Codes(object):
NOT_FOUND = "M_NOT_FOUND" NOT_FOUND = "M_NOT_FOUND"
MISSING_TOKEN = "M_MISSING_TOKEN" MISSING_TOKEN = "M_MISSING_TOKEN"
UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN" UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
GUEST_ACCESS_FORBIDDEN = "M_GUEST_ACCESS_FORBIDDEN"
LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED" LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED" CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
CAPTCHA_INVALID = "M_CAPTCHA_INVALID" CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
@@ -47,7 +48,6 @@ class CodeMessageException(RuntimeError):
"""An exception with integer code and message string attributes.""" """An exception with integer code and message string attributes."""
def __init__(self, code, msg): def __init__(self, code, msg):
logger.info("%s: %s, %s", type(self).__name__, code, msg)
super(CodeMessageException, self).__init__("%d: %s" % (code, msg)) super(CodeMessageException, self).__init__("%d: %s" % (code, msg))
self.code = code self.code = code
self.msg = msg self.msg = msg
@@ -77,11 +77,6 @@ class SynapseError(CodeMessageException):
) )
class RoomError(SynapseError):
"""An error raised when a room event fails."""
pass
class RegistrationError(SynapseError): class RegistrationError(SynapseError):
"""An error raised when a registration event fails.""" """An error raised when a registration event fails."""
pass pass
@@ -125,6 +120,31 @@ class AuthError(SynapseError):
super(AuthError, self).__init__(*args, **kwargs) super(AuthError, self).__init__(*args, **kwargs)
class GuestAccessError(AuthError):
"""An error raised when a there is a problem with a guest user accessing
a room"""
def __init__(self, rooms, *args, **kwargs):
self.rooms = rooms
super(GuestAccessError, self).__init__(*args, **kwargs)
def error_dict(self):
return cs_error(
self.msg,
self.errcode,
rooms=self.rooms,
)
class EventSizeError(SynapseError):
"""An error raised when an event is too big."""
def __init__(self, *args, **kwargs):
if "errcode" not in kwargs:
kwargs["errcode"] = Codes.TOO_LARGE
super(EventSizeError, self).__init__(413, *args, **kwargs)
class EventStreamError(SynapseError): class EventStreamError(SynapseError):
"""An error raised when there a problem with the event stream.""" """An error raised when there a problem with the event stream."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@@ -24,7 +24,7 @@ class Filtering(object):
def get_user_filter(self, user_localpart, filter_id): def get_user_filter(self, user_localpart, filter_id):
result = self.store.get_user_filter(user_localpart, filter_id) result = self.store.get_user_filter(user_localpart, filter_id)
result.addCallback(Filter) result.addCallback(FilterCollection)
return result return result
def add_user_filter(self, user_localpart, user_filter): def add_user_filter(self, user_localpart, user_filter):
@@ -50,11 +50,11 @@ class Filtering(object):
# many definitions. # many definitions.
top_level_definitions = [ top_level_definitions = [
"public_user_data", "private_user_data", "server_data" "presence", "account_data"
] ]
room_level_definitions = [ room_level_definitions = [
"state", "events", "ephemeral" "state", "timeline", "ephemeral", "account_data"
] ]
for key in top_level_definitions: for key in top_level_definitions:
@@ -62,10 +62,29 @@ class Filtering(object):
self._check_definition(user_filter_json[key]) self._check_definition(user_filter_json[key])
if "room" in user_filter_json: if "room" in user_filter_json:
self._check_definition_room_lists(user_filter_json["room"])
for key in room_level_definitions: for key in room_level_definitions:
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])
def _check_definition_room_lists(self, definition):
"""Check that "rooms" and "not_rooms" are lists of room ids if they
are present
Args:
definition(dict): The filter definition
Raises:
SynapseError: If there was a problem with this definition.
"""
# check rooms are valid room IDs
room_id_keys = ["rooms", "not_rooms"]
for key in room_id_keys:
if key in definition:
if type(definition[key]) != list:
raise SynapseError(400, "Expected %s to be a list." % key)
for room_id in definition[key]:
RoomID.from_string(room_id)
def _check_definition(self, definition): def _check_definition(self, definition):
"""Check if the provided definition is valid. """Check if the provided definition is valid.
@@ -85,14 +104,7 @@ class Filtering(object):
400, "Expected JSON object, not %s" % (definition,) 400, "Expected JSON object, not %s" % (definition,)
) )
# check rooms are valid room IDs self._check_definition_room_lists(definition)
room_id_keys = ["rooms", "not_rooms"]
for key in room_id_keys:
if key in definition:
if type(definition[key]) != list:
raise SynapseError(400, "Expected %s to be a list." % key)
for room_id in definition[key]:
RoomID.from_string(room_id)
# check senders are valid user IDs # check senders are valid user IDs
user_id_keys = ["senders", "not_senders"] user_id_keys = ["senders", "not_senders"]
@@ -114,116 +126,147 @@ class Filtering(object):
if not isinstance(event_type, basestring): if not isinstance(event_type, basestring):
raise SynapseError(400, "Event type should be a string") raise SynapseError(400, "Event type should be a string")
if "format" in definition:
event_format = definition["format"]
if event_format not in ["federation", "events"]:
raise SynapseError(400, "Invalid format: %s" % (event_format,))
if "select" in definition: class FilterCollection(object):
event_select_list = definition["select"] def __init__(self, filter_json):
for select_key in event_select_list: self.filter_json = filter_json
if select_key not in ["event_id", "origin_server_ts",
"thread_id", "content", "content.body"]:
raise SynapseError(400, "Bad select: %s" % (select_key,))
if ("bundle_updates" in definition and room_filter_json = self.filter_json.get("room", {})
type(definition["bundle_updates"]) != bool):
raise SynapseError(400, "Bad bundle_updates: expected bool.") self.room_filter = Filter({
k: v for k, v in room_filter_json.items()
if k in ("rooms", "not_rooms")
})
self.room_timeline_filter = Filter(room_filter_json.get("timeline", {}))
self.room_state_filter = Filter(room_filter_json.get("state", {}))
self.room_ephemeral_filter = Filter(room_filter_json.get("ephemeral", {}))
self.room_account_data = Filter(room_filter_json.get("account_data", {}))
self.presence_filter = Filter(self.filter_json.get("presence", {}))
self.account_data = Filter(self.filter_json.get("account_data", {}))
self.include_leave = self.filter_json.get("room", {}).get(
"include_leave", False
)
def list_rooms(self):
return self.room_filter.list_rooms()
def timeline_limit(self):
return self.room_timeline_filter.limit()
def presence_limit(self):
return self.presence_filter.limit()
def ephemeral_limit(self):
return self.room_ephemeral_filter.limit()
def filter_presence(self, events):
return self.presence_filter.filter(events)
def filter_account_data(self, events):
return self.account_data.filter(events)
def filter_room_state(self, events):
return self.room_state_filter.filter(self.room_filter.filter(events))
def filter_room_timeline(self, events):
return self.room_timeline_filter.filter(self.room_filter.filter(events))
def filter_room_ephemeral(self, events):
return self.room_ephemeral_filter.filter(self.room_filter.filter(events))
def filter_room_account_data(self, events):
return self.room_account_data.filter(self.room_filter.filter(events))
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
def filter_public_user_data(self, events): def list_rooms(self):
return self._filter_on_key(events, ["public_user_data"]) """The list of room_id strings this filter restricts the output to
or None if the this filter doesn't list the room ids.
def filter_private_user_data(self, events):
return self._filter_on_key(events, ["private_user_data"])
def filter_room_state(self, events):
return self._filter_on_key(events, ["room", "state"])
def filter_room_events(self, events):
return self._filter_on_key(events, ["room", "events"])
def filter_room_ephemeral(self, events):
return self._filter_on_key(events, ["room", "ephemeral"])
def _filter_on_key(self, events, keys):
filter_json = self.filter_json
if not filter_json:
return events
try:
# extract the right definition from the filter
definition = filter_json
for key in keys:
definition = definition[key]
return self._filter_with_definition(events, definition)
except KeyError:
# return all events if definition isn't specified.
return events
def _filter_with_definition(self, events, definition):
return [e for e in events if self._passes_definition(definition, e)]
def _passes_definition(self, definition, event):
"""Check if the event passes through the given definition.
Args:
definition(dict): The definition to check against.
event(Event): The event to check.
Returns:
True if the event passes through the filter.
""" """
# Algorithm notes: if "rooms" in self.filter_json:
# For each key in the definition, check the event meets the criteria: return list(set(self.filter_json["rooms"]))
# * For types: Literal match or prefix match (if ends with wildcard) else:
# * For senders/rooms: Literal match only return None
# * "not_" checks take presedence (e.g. if "m.*" is in both 'types'
# and 'not_types' then it is treated as only being in 'not_types')
# room checks def check(self, event):
if hasattr(event, "room_id"): """Checks whether the filter matches the given event.
room_id = event.room_id
allow_rooms = definition.get("rooms", None) Returns:
reject_rooms = definition.get("not_rooms", None) bool: True if the event matches
if reject_rooms and room_id in reject_rooms: """
return False if isinstance(event, dict):
if allow_rooms and room_id not in allow_rooms: return self.check_fields(
event.get("room_id", None),
event.get("sender", None),
event.get("type", None),
)
else:
return self.check_fields(
getattr(event, "room_id", None),
getattr(event, "sender", None),
event.type,
)
def check_fields(self, room_id, sender, event_type):
"""Checks whether the filter matches the given event fields.
Returns:
bool: True if the event fields match
"""
literal_keys = {
"rooms": lambda v: room_id == v,
"senders": lambda v: sender == v,
"types": lambda v: _matches_wildcard(event_type, v)
}
for name, match_func in literal_keys.items():
not_name = "not_%s" % (name,)
disallowed_values = self.filter_json.get(not_name, [])
if any(map(match_func, disallowed_values)):
return False return False
# sender checks allowed_values = self.filter_json.get(name, None)
if hasattr(event, "sender"): if allowed_values is not None:
# Should we be including event.state_key for some event types? if not any(map(match_func, allowed_values)):
sender = event.sender
allow_senders = definition.get("senders", None)
reject_senders = definition.get("not_senders", None)
if reject_senders and sender in reject_senders:
return False
if allow_senders and sender not in allow_senders:
return False
# type checks
if "not_types" in definition:
for def_type in definition["not_types"]:
if self._event_matches_type(event, def_type):
return False return False
if "types" in definition:
included = False
for def_type in definition["types"]:
if self._event_matches_type(event, def_type):
included = True
break
if not included:
return False
return True return True
def _event_matches_type(self, event, def_type): def filter_rooms(self, room_ids):
if def_type.endswith("*"): """Apply the 'rooms' filter to a given list of rooms.
type_prefix = def_type[:-1]
return event.type.startswith(type_prefix) Args:
else: room_ids (list): A list of room_ids.
return event.type == def_type
Returns:
list: A list of room_ids that match the filter
"""
room_ids = set(room_ids)
disallowed_rooms = set(self.filter_json.get("not_rooms", []))
room_ids -= disallowed_rooms
allowed_rooms = self.filter_json.get("rooms", None)
if allowed_rooms is not None:
room_ids &= set(allowed_rooms)
return room_ids
def filter(self, events):
return filter(self.check, events)
def limit(self):
return self.filter_json.get("limit", 10)
def _matches_wildcard(actual_value, filter_value):
if filter_value.endswith("*"):
type_prefix = filter_value[:-1]
return actual_value.startswith(type_prefix)
else:
return actual_value == filter_value

View File

@@ -15,21 +15,34 @@
# limitations under the License. # limitations under the License.
import sys import sys
from synapse.rest import ClientRestResource
sys.dont_write_bytecode = True sys.dont_write_bytecode = True
from synapse.python_dependencies import check_requirements, DEPENDENCY_LINKS from synapse.python_dependencies import (
check_requirements, DEPENDENCY_LINKS, MissingRequirementError
)
if __name__ == '__main__': if __name__ == '__main__':
check_requirements() try:
check_requirements()
except MissingRequirementError as e:
message = "\n".join([
"Missing Requirement: %s" % (e.message,),
"To install run:",
" pip install --upgrade --force \"%s\"" % (e.dependency,),
"",
])
sys.stderr.writelines(message)
sys.exit(1)
from synapse.storage.engines import create_engine, IncorrectDatabaseSetup from synapse.storage.engines import create_engine, IncorrectDatabaseSetup
from synapse.storage import ( from synapse.storage import are_all_users_on_domain
are_all_users_on_domain, UpgradeDatabaseException, from synapse.storage.prepare_database import UpgradeDatabaseException
)
from synapse.server import HomeServer from synapse.server import HomeServer
from twisted.internet import reactor from twisted.internet import reactor, task, defer
from twisted.application import service from twisted.application import service
from twisted.enterprise import adbapi from twisted.enterprise import adbapi
from twisted.web.resource import Resource, EncodingResourceWrapper from twisted.web.resource import Resource, EncodingResourceWrapper
@@ -42,15 +55,13 @@ from synapse.rest.key.v1.server_key_resource import LocalKey
from synapse.rest.key.v2 import KeyApiV2Resource from synapse.rest.key.v2 import KeyApiV2Resource
from synapse.http.matrixfederationclient import MatrixFederationHttpClient from synapse.http.matrixfederationclient import MatrixFederationHttpClient
from synapse.api.urls import ( from synapse.api.urls import (
CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX,
SERVER_KEY_PREFIX, MEDIA_PREFIX, CLIENT_V2_ALPHA_PREFIX, STATIC_PREFIX, SERVER_KEY_PREFIX, MEDIA_PREFIX, STATIC_PREFIX,
SERVER_KEY_V2_PREFIX, SERVER_KEY_V2_PREFIX,
) )
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.rest.client.v1 import ClientV1RestResource
from synapse.rest.client.v2_alpha import ClientV2AlphaRestResource
from synapse.metrics.resource import MetricsResource, METRICS_PREFIX from synapse.metrics.resource import MetricsResource, METRICS_PREFIX
from synapse import events from synapse import events
@@ -72,12 +83,6 @@ import time
logger = logging.getLogger("synapse.app.homeserver") logger = logging.getLogger("synapse.app.homeserver")
class GzipFile(File):
def getChild(self, path, request):
child = File.getChild(self, path, request)
return EncodingResourceWrapper(child, [GzipEncoderFactory()])
def gz_wrap(r): def gz_wrap(r):
return EncodingResourceWrapper(r, [GzipEncoderFactory()]) return EncodingResourceWrapper(r, [GzipEncoderFactory()])
@@ -87,11 +92,8 @@ class SynapseHomeServer(HomeServer):
def build_http_client(self): def build_http_client(self):
return MatrixFederationHttpClient(self) return MatrixFederationHttpClient(self)
def build_resource_for_client(self): def build_client_resource(self):
return ClientV1RestResource(self) return ClientRestResource(self)
def build_resource_for_client_v2_alpha(self):
return ClientV2AlphaRestResource(self)
def build_resource_for_federation(self): def build_resource_for_federation(self):
return JsonResource(self) return JsonResource(self)
@@ -121,12 +123,15 @@ class SynapseHomeServer(HomeServer):
# (It can stay enabled for the API resources: they call # (It can stay enabled for the API resources: they call
# write() with the whole body and then finish() straight # write() with the whole body and then finish() straight
# after and so do not trigger the bug. # after and so do not trigger the bug.
# GzipFile was removed in commit 184ba09
# return GzipFile(webclient_path) # TODO configurable? # return GzipFile(webclient_path) # TODO configurable?
return File(webclient_path) # TODO configurable? return File(webclient_path) # TODO configurable?
def build_resource_for_static_content(self): def build_resource_for_static_content(self):
# This is old and should go away: not going to bother adding gzip # This is old and should go away: not going to bother adding gzip
return File("static") return File(
os.path.join(os.path.dirname(synapse.__file__), "static")
)
def build_resource_for_content_repo(self): def build_resource_for_content_repo(self):
return ContentRepoResource( return ContentRepoResource(
@@ -171,16 +176,15 @@ class SynapseHomeServer(HomeServer):
for res in listener_config["resources"]: for res in listener_config["resources"]:
for name in res["names"]: for name in res["names"]:
if name == "client": if name == "client":
client_resource = self.get_client_resource()
if res["compress"]: if res["compress"]:
client_v1 = gz_wrap(self.get_resource_for_client()) client_resource = gz_wrap(client_resource)
client_v2 = gz_wrap(self.get_resource_for_client_v2_alpha())
else:
client_v1 = self.get_resource_for_client()
client_v2 = self.get_resource_for_client_v2_alpha()
resources.update({ resources.update({
CLIENT_PREFIX: client_v1, "/_matrix/client/api/v1": client_resource,
CLIENT_V2_ALPHA_PREFIX: client_v2, "/_matrix/client/r0": client_resource,
"/_matrix/client/unstable": client_resource,
"/_matrix/client/v2_alpha": client_resource,
}) })
if name == "federation": if name == "federation":
@@ -221,7 +225,7 @@ class SynapseHomeServer(HomeServer):
listener_config, listener_config,
root_resource, root_resource,
), ),
self.tls_context_factory, self.tls_server_context_factory,
interface=bind_address interface=bind_address
) )
else: else:
@@ -365,7 +369,6 @@ def setup(config_options):
Args: Args:
config_options_options: The options passed to Synapse. Usually config_options_options: The options passed to Synapse. Usually
`sys.argv[1:]`. `sys.argv[1:]`.
should_run (bool): Whether to start the reactor.
Returns: Returns:
HomeServer HomeServer
@@ -388,7 +391,7 @@ def setup(config_options):
events.USE_FROZEN_DICTS = config.use_frozen_dicts events.USE_FROZEN_DICTS = config.use_frozen_dicts
tls_context_factory = context_factory.ServerContextFactory(config) tls_server_context_factory = context_factory.ServerContextFactory(config)
database_engine = create_engine(config.database_config["name"]) database_engine = create_engine(config.database_config["name"])
config.database_config["args"]["cp_openfun"] = database_engine.on_new_connection config.database_config["args"]["cp_openfun"] = database_engine.on_new_connection
@@ -396,7 +399,7 @@ def setup(config_options):
hs = SynapseHomeServer( hs = SynapseHomeServer(
config.server_name, config.server_name,
db_config=config.database_config, db_config=config.database_config,
tls_context_factory=tls_context_factory, tls_server_context_factory=tls_server_context_factory,
config=config, config=config,
content_addr=config.content_addr, content_addr=config.content_addr,
version_string=version_string, version_string=version_string,
@@ -432,6 +435,7 @@ def setup(config_options):
hs.get_pusherpool().start() hs.get_pusherpool().start()
hs.get_state_handler().start_caching() hs.get_state_handler().start_caching()
hs.get_datastore().start_profiling() hs.get_datastore().start_profiling()
hs.get_datastore().start_doing_background_updates()
hs.get_replication_layer().start_get_pdu_cache() hs.get_replication_layer().start_get_pdu_cache()
return hs return hs
@@ -491,13 +495,28 @@ class SynapseRequest(Request):
self.start_time = int(time.time() * 1000) self.start_time = int(time.time() * 1000)
def finished_processing(self): 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( self.site.access_logger.info(
"%s - %s - {%s}" "%s - %s - {%s}"
" Processed request: %dms %sB %s \"%s %s %s\" \"%s\"", " Processed request: %dms (%dms, %dms) (%dms/%d)"
" %sB %s \"%s %s %s\" \"%s\"",
self.getClientIP(), self.getClientIP(),
self.site.site_tag, self.site.site_tag,
self.authenticated_entity, self.authenticated_entity,
int(time.time() * 1000) - self.start_time, 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.sentLength,
self.code, self.code,
self.method, self.method,
@@ -665,6 +684,42 @@ def run(hs):
ThreadPool._worker = profile(ThreadPool._worker) ThreadPool._worker = profile(ThreadPool._worker)
reactor.run = profile(reactor.run) reactor.run = profile(reactor.run)
start_time = hs.get_clock().time()
@defer.inlineCallbacks
def phone_stats_home():
now = int(hs.get_clock().time())
uptime = int(now - start_time)
if uptime < 0:
uptime = 0
stats = {}
stats["homeserver"] = hs.config.server_name
stats["timestamp"] = now
stats["uptime_seconds"] = uptime
stats["total_users"] = yield hs.get_datastore().count_all_users()
all_rooms = yield hs.get_datastore().get_rooms(False)
stats["total_room_count"] = len(all_rooms)
stats["daily_active_users"] = yield hs.get_datastore().count_daily_users()
daily_messages = yield hs.get_datastore().count_daily_messages()
if daily_messages is not None:
stats["daily_messages"] = daily_messages
logger.info("Reporting stats to matrix.org: %s" % (stats,))
try:
yield hs.get_simple_http_client().put_json(
"https://matrix.org/report-usage-stats/push",
stats
)
except Exception as e:
logger.warn("Error reporting stats: %s", e)
if hs.config.report_stats:
phone_home_task = task.LoopingCall(phone_stats_home)
phone_home_task.start(60 * 60 * 24, now=False)
def in_thread(): def in_thread():
with LoggingContext("run"): with LoggingContext("run"):
change_resource_limit(hs.config.soft_file_limit) change_resource_limit(hs.config.soft_file_limit)

View File

@@ -16,57 +16,67 @@
import sys import sys
import os import os
import os.path
import subprocess import subprocess
import signal import signal
import yaml import yaml
SYNAPSE = ["python", "-B", "-m", "synapse.app.homeserver"] SYNAPSE = ["python", "-B", "-m", "synapse.app.homeserver"]
CONFIGFILE = "homeserver.yaml"
GREEN = "\x1b[1;32m" GREEN = "\x1b[1;32m"
RED = "\x1b[1;31m"
NORMAL = "\x1b[m" NORMAL = "\x1b[m"
if not os.path.exists(CONFIGFILE):
sys.stderr.write(
"No config file found\n"
"To generate a config file, run '%s -c %s --generate-config"
" --server-name=<server name>'\n" % (
" ".join(SYNAPSE), CONFIGFILE
)
)
sys.exit(1)
CONFIG = yaml.load(open(CONFIGFILE)) def start(configfile):
PIDFILE = CONFIG["pid_file"]
def start():
print "Starting ...", print "Starting ...",
args = SYNAPSE args = SYNAPSE
args.extend(["--daemonize", "-c", CONFIGFILE]) args.extend(["--daemonize", "-c", configfile])
subprocess.check_call(args)
print GREEN + "started" + NORMAL try:
subprocess.check_call(args)
print GREEN + "started" + NORMAL
except subprocess.CalledProcessError as e:
print (
RED +
"error starting (exit code: %d); see above for logs" % e.returncode +
NORMAL
)
def stop(): def stop(pidfile):
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)
print GREEN + "stopped" + NORMAL print GREEN + "stopped" + NORMAL
def main(): def main():
configfile = sys.argv[2] if len(sys.argv) == 3 else "homeserver.yaml"
if not os.path.exists(configfile):
sys.stderr.write(
"No config file found\n"
"To generate a config file, run '%s -c %s --generate-config"
" --server-name=<server name>'\n" % (
" ".join(SYNAPSE), configfile
)
)
sys.exit(1)
config = yaml.load(open(configfile))
pidfile = config["pid_file"]
action = sys.argv[1] if sys.argv[1:] else "usage" action = sys.argv[1] if sys.argv[1:] else "usage"
if action == "start": if action == "start":
start() start(configfile)
elif action == "stop": elif action == "stop":
stop() stop(pidfile)
elif action == "restart": elif action == "restart":
stop() stop(pidfile)
start() start(configfile)
else: else:
sys.stderr.write("Usage: %s [start|stop|restart]\n" % (sys.argv[0],)) sys.stderr.write("Usage: %s [start|stop|restart] [configfile]\n" % (sys.argv[0],))
sys.exit(1) sys.exit(1)

View File

@@ -224,8 +224,8 @@ class _Recoverer(object):
self.clock.call_later((2 ** self.backoff_counter), self.retry) self.clock.call_later((2 ** self.backoff_counter), self.retry)
def _backoff(self): def _backoff(self):
# cap the backoff to be around 18h => (2^16) = 65536 secs # cap the backoff to be around 8.5min => (2^9) = 512 secs
if self.backoff_counter < 16: if self.backoff_counter < 9:
self.backoff_counter += 1 self.backoff_counter += 1
self.recover() self.recover()

View File

@@ -14,6 +14,7 @@
# limitations under the License. # limitations under the License.
import argparse import argparse
import errno
import os import os
import yaml import yaml
import sys import sys
@@ -24,8 +25,29 @@ class ConfigError(Exception):
pass pass
class Config(object): # We split these messages out to allow packages to override with package
# specific instructions.
MISSING_REPORT_STATS_CONFIG_INSTRUCTIONS = """\
Please opt in or out of reporting anonymized homeserver usage statistics, by
setting the `report_stats` key in your config file to either True or False.
"""
MISSING_REPORT_STATS_SPIEL = """\
We would really appreciate it if you could help our project out by reporting
anonymized usage statistics from your homeserver. Only very basic aggregate
data (e.g. number of users) will be reported, but it helps us to track the
growth of the Matrix community, and helps us to make Matrix a success, as well
as to convince other networks that they should peer with us.
Thank you.
"""
MISSING_SERVER_NAME = """\
Missing mandatory `server_name` config option.
"""
class Config(object):
@staticmethod @staticmethod
def parse_size(value): def parse_size(value):
if isinstance(value, int) or isinstance(value, long): if isinstance(value, int) or isinstance(value, long):
@@ -81,8 +103,11 @@ class Config(object):
@classmethod @classmethod
def ensure_directory(cls, dir_path): def ensure_directory(cls, dir_path):
dir_path = cls.abspath(dir_path) dir_path = cls.abspath(dir_path)
if not os.path.exists(dir_path): try:
os.makedirs(dir_path) os.makedirs(dir_path)
except OSError, e:
if e.errno != errno.EEXIST:
raise
if not os.path.isdir(dir_path): if not os.path.isdir(dir_path):
raise ConfigError( raise ConfigError(
"%s is not a directory" % (dir_path,) "%s is not a directory" % (dir_path,)
@@ -111,11 +136,14 @@ class Config(object):
results.append(getattr(cls, name)(self, *args, **kargs)) results.append(getattr(cls, name)(self, *args, **kargs))
return results return results
def generate_config(self, config_dir_path, server_name): def generate_config(self, config_dir_path, server_name, report_stats=None):
default_config = "# vim:ft=yaml\n" default_config = "# vim:ft=yaml\n"
default_config += "\n\n".join(dedent(conf) for conf in self.invoke_all( default_config += "\n\n".join(dedent(conf) for conf in self.invoke_all(
"default_config", config_dir_path, server_name "default_config",
config_dir_path=config_dir_path,
server_name=server_name,
report_stats=report_stats,
)) ))
config = yaml.load(default_config) config = yaml.load(default_config)
@@ -139,6 +167,12 @@ class Config(object):
action="store_true", action="store_true",
help="Generate a config file for the server name" help="Generate a config file for the server name"
) )
config_parser.add_argument(
"--report-stats",
action="store",
help="Stuff",
choices=["yes", "no"]
)
config_parser.add_argument( config_parser.add_argument(
"--generate-keys", "--generate-keys",
action="store_true", action="store_true",
@@ -189,6 +223,11 @@ class Config(object):
config_files.append(config_path) config_files.append(config_path)
if config_args.generate_config: if config_args.generate_config:
if config_args.report_stats is None:
config_parser.error(
"Please specify either --report-stats=yes or --report-stats=no\n\n" +
MISSING_REPORT_STATS_SPIEL
)
if not config_files: if not config_files:
config_parser.error( config_parser.error(
"Must supply a config file.\nA config file can be automatically" "Must supply a config file.\nA config file can be automatically"
@@ -211,7 +250,9 @@ class Config(object):
os.makedirs(config_dir_path) os.makedirs(config_dir_path)
with open(config_path, "wb") as config_file: with open(config_path, "wb") as config_file:
config_bytes, config = obj.generate_config( config_bytes, config = obj.generate_config(
config_dir_path, server_name config_dir_path=config_dir_path,
server_name=server_name,
report_stats=(config_args.report_stats == "yes"),
) )
obj.invoke_all("generate_files", config) obj.invoke_all("generate_files", config)
config_file.write(config_bytes) config_file.write(config_bytes)
@@ -260,10 +301,22 @@ class Config(object):
yaml_config = cls.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:
sys.stderr.write("\n" + MISSING_SERVER_NAME + "\n")
sys.exit(1)
server_name = specified_config["server_name"] server_name = specified_config["server_name"]
_, config = obj.generate_config(config_dir_path, server_name) _, config = obj.generate_config(
config_dir_path=config_dir_path,
server_name=server_name
)
config.pop("log_config") config.pop("log_config")
config.update(specified_config) config.update(specified_config)
if "report_stats" not in config:
sys.stderr.write(
"\n" + MISSING_REPORT_STATS_CONFIG_INSTRUCTIONS + "\n" +
MISSING_REPORT_STATS_SPIEL + "\n")
sys.exit(1)
if generate_keys: if generate_keys:
obj.invoke_all("generate_files", config) obj.invoke_all("generate_files", config)

View File

@@ -20,7 +20,7 @@ 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", [])
def default_config(cls, config_dir_path, server_name): 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: []

View File

@@ -24,7 +24,7 @@ class CaptchaConfig(Config):
self.captcha_bypass_secret = config.get("captcha_bypass_secret") self.captcha_bypass_secret = config.get("captcha_bypass_secret")
self.recaptcha_siteverify_api = config["recaptcha_siteverify_api"] self.recaptcha_siteverify_api = config["recaptcha_siteverify_api"]
def default_config(self, config_dir_path, server_name): def default_config(self, **kwargs):
return """\ return """\
## Captcha ## ## Captcha ##

47
synapse/config/cas.py Normal file
View File

@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Copyright 2015 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 ._base import Config
class CasConfig(Config):
"""Cas Configuration
cas_server_url: URL of CAS server
"""
def read_config(self, config):
cas_config = config.get("cas_config", None)
if cas_config:
self.cas_enabled = cas_config.get("enabled", True)
self.cas_server_url = cas_config["server_url"]
self.cas_service_url = cas_config["service_url"]
self.cas_required_attributes = cas_config.get("required_attributes", {})
else:
self.cas_enabled = False
self.cas_server_url = None
self.cas_service_url = None
self.cas_required_attributes = {}
def default_config(self, config_dir_path, server_name, **kwargs):
return """
# Enable CAS for registration and login.
#cas_config:
# enabled: true
# server_url: "https://cas-server.com"
# service_url: "https://homesever.domain.com:8448"
# #required_attributes:
# # name: value
"""

View File

@@ -45,7 +45,7 @@ class DatabaseConfig(Config):
self.set_databasepath(config.get("database_path")) self.set_databasepath(config.get("database_path"))
def default_config(self, config, config_dir_path): def default_config(self, **kwargs):
database_path = self.abspath("homeserver.db") database_path = self.abspath("homeserver.db")
return """\ return """\
# Database configuration # Database configuration

View File

@@ -26,12 +26,15 @@ from .metrics import MetricsConfig
from .appservice import AppServiceConfig from .appservice import AppServiceConfig
from .key import KeyConfig from .key import KeyConfig
from .saml2 import SAML2Config from .saml2 import SAML2Config
from .cas import CasConfig
from .password import PasswordConfig
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig, class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig, RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
VoipConfig, RegistrationConfig, MetricsConfig, VoipConfig, RegistrationConfig, MetricsConfig,
AppServiceConfig, KeyConfig, SAML2Config, ): AppServiceConfig, KeyConfig, SAML2Config, CasConfig,
PasswordConfig,):
pass pass

View File

@@ -40,7 +40,7 @@ class KeyConfig(Config):
config["perspectives"] config["perspectives"]
) )
def default_config(self, config_dir_path, server_name): def default_config(self, config_dir_path, server_name, **kwargs):
base_key_name = os.path.join(config_dir_path, server_name) base_key_name = os.path.join(config_dir_path, server_name)
return """\ return """\
## Signing Keys ## ## Signing Keys ##

View File

@@ -21,6 +21,8 @@ import logging.config
import yaml import yaml
from string import Template from string import Template
import os import os
import signal
from synapse.util.debug import debug_deferreds
DEFAULT_LOG_CONFIG = Template(""" DEFAULT_LOG_CONFIG = Template("""
@@ -68,8 +70,10 @@ 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): def default_config(self, config_dir_path, server_name, **kwargs):
log_file = self.abspath("homeserver.log") log_file = self.abspath("homeserver.log")
log_config = self.abspath( log_config = self.abspath(
os.path.join(config_dir_path, server_name + ".log.config") os.path.join(config_dir_path, server_name + ".log.config")
@@ -83,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):
@@ -142,6 +151,19 @@ class LoggingConfig(Config):
handler = logging.handlers.RotatingFileHandler( handler = logging.handlers.RotatingFileHandler(
self.log_file, maxBytes=(1000 * 1000 * 100), backupCount=3 self.log_file, maxBytes=(1000 * 1000 * 100), backupCount=3
) )
def sighup(signum, stack):
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
# 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() handler = logging.StreamHandler()
handler.setFormatter(formatter) handler.setFormatter(formatter)

View File

@@ -19,13 +19,15 @@ from ._base import Config
class MetricsConfig(Config): class MetricsConfig(Config):
def read_config(self, config): def read_config(self, config):
self.enable_metrics = config["enable_metrics"] self.enable_metrics = config["enable_metrics"]
self.report_stats = config.get("report_stats", None)
self.metrics_port = config.get("metrics_port") self.metrics_port = config.get("metrics_port")
self.metrics_bind_host = config.get("metrics_bind_host", "127.0.0.1") self.metrics_bind_host = config.get("metrics_bind_host", "127.0.0.1")
def default_config(self, config_dir_path, server_name): def default_config(self, report_stats=None, **kwargs):
return """\ suffix = "" if report_stats is None else "report_stats: %(report_stats)s\n"
return ("""\
## Metrics ### ## Metrics ###
# Enable collection and rendering of performance metrics # Enable collection and rendering of performance metrics
enable_metrics: False enable_metrics: False
""" """ + suffix) % locals()

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Copyright 2015 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 ._base import Config
class PasswordConfig(Config):
"""Password login configuration
"""
def read_config(self, config):
password_config = config.get("password_config", {})
self.password_enabled = password_config.get("enabled", True)
def default_config(self, config_dir_path, server_name, **kwargs):
return """
# Enable password for login.
password_config:
enabled: true
"""

View File

@@ -27,7 +27,7 @@ class RatelimitConfig(Config):
self.federation_rc_reject_limit = config["federation_rc_reject_limit"] self.federation_rc_reject_limit = config["federation_rc_reject_limit"]
self.federation_rc_concurrent = config["federation_rc_concurrent"] self.federation_rc_concurrent = config["federation_rc_concurrent"]
def default_config(self, config_dir_path, server_name): def default_config(self, **kwargs):
return """\ return """\
## Ratelimiting ## ## Ratelimiting ##

View File

@@ -33,8 +33,10 @@ class RegistrationConfig(Config):
self.registration_shared_secret = config.get("registration_shared_secret") self.registration_shared_secret = config.get("registration_shared_secret")
self.macaroon_secret_key = config.get("macaroon_secret_key") self.macaroon_secret_key = config.get("macaroon_secret_key")
self.bcrypt_rounds = config.get("bcrypt_rounds", 12)
self.allow_guest_access = config.get("allow_guest_access", False)
def default_config(self, config_dir, server_name): def default_config(self, **kwargs):
registration_shared_secret = random_string_with_symbols(50) registration_shared_secret = random_string_with_symbols(50)
macaroon_secret_key = random_string_with_symbols(50) macaroon_secret_key = random_string_with_symbols(50)
return """\ return """\
@@ -48,6 +50,16 @@ class RegistrationConfig(Config):
registration_shared_secret: "%(registration_shared_secret)s" registration_shared_secret: "%(registration_shared_secret)s"
macaroon_secret_key: "%(macaroon_secret_key)s" macaroon_secret_key: "%(macaroon_secret_key)s"
# Set the number of bcrypt rounds used to generate password hash.
# Larger numbers increase the work factor needed to generate the hash.
# The default number of rounds is 12.
bcrypt_rounds: 12
# Allows users to register as guests without a password/email/etc, and
# participate in rooms hosted on this server which have been made
# accessible to anonymous users.
allow_guest_access: False
""" % locals() """ % locals()
def add_arguments(self, parser): def add_arguments(self, parser):

View File

@@ -60,7 +60,7 @@ class ContentRepositoryConfig(Config):
config["thumbnail_sizes"] config["thumbnail_sizes"]
) )
def default_config(self, config_dir_path, server_name): def default_config(self, **kwargs):
media_store = self.default_path("media_store") media_store = self.default_path("media_store")
uploads_path = self.default_path("uploads") uploads_path = self.default_path("uploads")
return """ return """

View File

@@ -33,7 +33,7 @@ class SAML2Config(Config):
def read_config(self, config): def read_config(self, config):
saml2_config = config.get("saml2_config", None) saml2_config = config.get("saml2_config", None)
if saml2_config: if saml2_config:
self.saml2_enabled = True self.saml2_enabled = saml2_config.get("enabled", True)
self.saml2_config_path = saml2_config["config_path"] self.saml2_config_path = saml2_config["config_path"]
self.saml2_idp_redirect_url = saml2_config["idp_redirect_url"] self.saml2_idp_redirect_url = saml2_config["idp_redirect_url"]
else: else:
@@ -41,7 +41,7 @@ class SAML2Config(Config):
self.saml2_config_path = None self.saml2_config_path = None
self.saml2_idp_redirect_url = None self.saml2_idp_redirect_url = None
def default_config(self, config_dir_path, server_name): def default_config(self, config_dir_path, server_name, **kwargs):
return """ return """
# Enable SAML2 for registration and login. Uses pysaml2 # Enable SAML2 for registration and login. Uses pysaml2
# config_path: Path to the sp_conf.py configuration file # config_path: Path to the sp_conf.py configuration file
@@ -49,6 +49,7 @@ class SAML2Config(Config):
# the user back to /login/saml2 with proper info. # the user back to /login/saml2 with proper info.
# See pysaml2 docs for format of config. # See pysaml2 docs for format of config.
#saml2_config: #saml2_config:
# enabled: true
# config_path: "%s/sp_conf.py" # config_path: "%s/sp_conf.py"
# idp_redirect_url: "http://%s/idp" # idp_redirect_url: "http://%s/idp"
""" % (config_dir_path, server_name) """ % (config_dir_path, server_name)

View File

@@ -26,6 +26,7 @@ class ServerConfig(Config):
self.soft_file_limit = config["soft_file_limit"] self.soft_file_limit = config["soft_file_limit"]
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.use_frozen_dicts = config.get("use_frozen_dicts", True) self.use_frozen_dicts = config.get("use_frozen_dicts", True)
self.listeners = config.get("listeners", []) self.listeners = config.get("listeners", [])
@@ -117,7 +118,7 @@ class ServerConfig(Config):
self.content_addr = content_addr self.content_addr = content_addr
def default_config(self, config_dir_path, server_name): 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])
unsecure_port = bind_port - 400 unsecure_port = bind_port - 400
@@ -132,6 +133,7 @@ class ServerConfig(Config):
# The domain name of the server, with optional explicit port. # The domain name of the server, with optional explicit port.
# This is used by remote servers to connect to this server, # This is used by remote servers to connect to this server,
# e.g. matrix.org, localhost:8080, etc. # e.g. matrix.org, localhost:8080, etc.
# This is also the last part of your UserID.
server_name: "%(server_name)s" server_name: "%(server_name)s"
# When running as a daemon, the file to store the pid in # When running as a daemon, the file to store the pid in

View File

@@ -42,7 +42,15 @@ class TlsConfig(Config):
config.get("tls_dh_params_path"), "tls_dh_params" config.get("tls_dh_params_path"), "tls_dh_params"
) )
def default_config(self, config_dir_path, server_name): # This config option applies to non-federation HTTP clients
# (e.g. for talking to recaptcha, identity servers, and such)
# It should never be used in production, and is intended for
# use only when running tests.
self.use_insecure_ssl_client_just_for_testing_do_not_use = config.get(
"use_insecure_ssl_client_just_for_testing_do_not_use"
)
def default_config(self, config_dir_path, server_name, **kwargs):
base_key_name = os.path.join(config_dir_path, server_name) base_key_name = os.path.join(config_dir_path, server_name)
tls_certificate_path = base_key_name + ".tls.crt" tls_certificate_path = base_key_name + ".tls.crt"

View File

@@ -22,7 +22,7 @@ class VoipConfig(Config):
self.turn_shared_secret = config["turn_shared_secret"] self.turn_shared_secret = config["turn_shared_secret"]
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, config_dir_path, server_name): def default_config(self, **kwargs):
return """\ return """\
## Turn ## ## Turn ##

View File

@@ -228,10 +228,11 @@ class Keyring(object):
def do_iterations(): def do_iterations():
merged_results = {} merged_results = {}
missing_keys = { missing_keys = {}
group.server_name: set(group.key_ids) for group in group_id_to_group.values():
for group in group_id_to_group.values() missing_keys.setdefault(group.server_name, set()).update(
} group.key_ids
)
for fn in key_fetch_fns: for fn in key_fetch_fns:
results = yield fn(missing_keys.items()) results = yield fn(missing_keys.items())
@@ -382,28 +383,24 @@ class Keyring(object):
def get_server_verify_key_v2_indirect(self, server_names_and_key_ids, def get_server_verify_key_v2_indirect(self, server_names_and_key_ids,
perspective_name, perspective_name,
perspective_keys): perspective_keys):
limiter = yield get_retry_limiter( # TODO(mark): Set the minimum_valid_until_ts to that needed by
perspective_name, self.clock, self.store # the events being validated or the current time if validating
) # an incoming request.
query_response = yield self.client.post_json(
with limiter: destination=perspective_name,
# TODO(mark): Set the minimum_valid_until_ts to that needed by path=b"/_matrix/key/v2/query",
# the events being validated or the current time if validating data={
# an incoming request. u"server_keys": {
query_response = yield self.client.post_json( server_name: {
destination=perspective_name, key_id: {
path=b"/_matrix/key/v2/query", u"minimum_valid_until_ts": 0
data={ } for key_id in key_ids
u"server_keys": {
server_name: {
key_id: {
u"minimum_valid_until_ts": 0
} for key_id in key_ids
}
for server_name, key_ids in server_names_and_key_ids
} }
}, for server_name, key_ids in server_names_and_key_ids
) }
},
long_retries=True,
)
keys = {} keys = {}
@@ -470,7 +467,7 @@ class Keyring(object):
continue continue
(response, tls_certificate) = yield fetch_server_key( (response, tls_certificate) = yield fetch_server_key(
server_name, self.hs.tls_context_factory, server_name, self.hs.tls_server_context_factory,
path=(b"/_matrix/key/v2/server/%s" % ( path=(b"/_matrix/key/v2/server/%s" % (
urllib.quote(requested_key_id), urllib.quote(requested_key_id),
)).encode("ascii"), )).encode("ascii"),
@@ -604,7 +601,7 @@ class Keyring(object):
# Try to fetch the key from the remote server. # Try to fetch the key from the remote server.
(response, tls_certificate) = yield fetch_server_key( (response, tls_certificate) = yield fetch_server_key(
server_name, self.hs.tls_context_factory server_name, self.hs.tls_server_context_factory
) )
# Check the response. # Check the response.

View File

@@ -66,7 +66,6 @@ def prune_event(event):
"users_default", "users_default",
"events", "events",
"events_default", "events_default",
"events_default",
"state_default", "state_default",
"ban", "ban",
"kick", "kick",
@@ -101,19 +100,20 @@ def format_event_raw(d):
def format_event_for_client_v1(d): def format_event_for_client_v1(d):
d["user_id"] = d.pop("sender", None) d = format_event_for_client_v2(d)
move_keys = ("age", "redacted_because", "replaces_state", "prev_content") sender = d.get("sender")
for key in move_keys: if sender is not None:
d["user_id"] = sender
copy_keys = (
"age", "redacted_because", "replaces_state", "prev_content",
"invite_room_state",
)
for key in copy_keys:
if key in d["unsigned"]: if key in d["unsigned"]:
d[key] = d["unsigned"][key] d[key] = d["unsigned"][key]
drop_keys = (
"auth_events", "prev_events", "hashes", "signatures", "depth",
"unsigned", "origin", "prev_state"
)
for key in drop_keys:
d.pop(key, None)
return d return d
@@ -127,10 +127,9 @@ def format_event_for_client_v2(d):
return d return d
def format_event_for_client_v2_without_event_id(d): def format_event_for_client_v2_without_room_id(d):
d = format_event_for_client_v2(d) d = format_event_for_client_v2(d)
d.pop("room_id", None) d.pop("room_id", None)
d.pop("event_id", None)
return d return d
@@ -152,7 +151,8 @@ def serialize_event(e, time_now_ms, as_client_event=True,
if "redacted_because" in e.unsigned: if "redacted_because" in e.unsigned:
d["unsigned"]["redacted_because"] = serialize_event( d["unsigned"]["redacted_because"] = serialize_event(
e.unsigned["redacted_because"], time_now_ms e.unsigned["redacted_because"], time_now_ms,
event_format=event_format
) )
if token_id is not None: if token_id is not None:

View File

@@ -17,6 +17,7 @@
from twisted.internet import defer from twisted.internet import defer
from .federation_base import FederationBase from .federation_base import FederationBase
from synapse.api.constants import Membership
from .units import Edu from .units import Edu
from synapse.api.errors import ( from synapse.api.errors import (
@@ -356,19 +357,55 @@ class FederationClient(FederationBase):
defer.returnValue(signed_auth) defer.returnValue(signed_auth)
@defer.inlineCallbacks @defer.inlineCallbacks
def make_join(self, destinations, room_id, user_id): def make_membership_event(self, destinations, room_id, user_id, membership,
content={},):
"""
Creates an m.room.member event, with context, without participating in the room.
Does so by asking one of the already participating servers to create an
event with proper context.
Note that this does not append any events to any graphs.
Args:
destinations (str): Candidate homeservers which are probably
participating in the room.
room_id (str): The room in which the event will happen.
user_id (str): The user whose membership is being evented.
membership (str): The "membership" property of the event. Must be
one of "join" or "leave".
content (object): Any additional data to put into the content field
of the event.
Return:
A tuple of (origin (str), event (object)) where origin is the remote
homeserver which generated the event.
"""
valid_memberships = {Membership.JOIN, Membership.LEAVE}
if membership not in valid_memberships:
raise RuntimeError(
"make_membership_event called with membership='%s', must be one of %s" %
(membership, ",".join(valid_memberships))
)
for destination in destinations: for destination in destinations:
if destination == self.server_name: if destination == self.server_name:
continue continue
try: try:
ret = yield self.transport_layer.make_join( ret = yield self.transport_layer.make_membership_event(
destination, room_id, user_id destination, room_id, user_id, membership
) )
pdu_dict = ret["event"] pdu_dict = ret["event"]
logger.debug("Got response to make_join: %s", pdu_dict) logger.debug("Got response to make_%s: %s", membership, pdu_dict)
pdu_dict["content"].update(content)
# The protoevent received over the JSON wire may not have all
# the required fields. Lets just gloss over that because
# there's some we never care about
if "prev_state" not in pdu_dict:
pdu_dict["prev_state"] = []
defer.returnValue( defer.returnValue(
(destination, self.event_from_pdu_json(pdu_dict)) (destination, self.event_from_pdu_json(pdu_dict))
@@ -378,8 +415,8 @@ class FederationClient(FederationBase):
raise raise
except Exception as e: except Exception as e:
logger.warn( logger.warn(
"Failed to make_join via %s: %s", "Failed to make_%s via %s: %s",
destination, e.message membership, destination, e.message
) )
raise RuntimeError("Failed to send to any server.") raise RuntimeError("Failed to send to any server.")
@@ -485,6 +522,33 @@ class FederationClient(FederationBase):
defer.returnValue(pdu) defer.returnValue(pdu)
@defer.inlineCallbacks
def send_leave(self, destinations, pdu):
for destination in destinations:
if destination == self.server_name:
continue
try:
time_now = self._clock.time_msec()
_, content = yield self.transport_layer.send_leave(
destination=destination,
room_id=pdu.room_id,
event_id=pdu.event_id,
content=pdu.get_pdu_json(time_now),
)
logger.debug("Got content: %s", content)
defer.returnValue(None)
except CodeMessageException:
raise
except Exception as e:
logger.exception(
"Failed to send_leave via %s: %s",
destination, e.message
)
raise RuntimeError("Failed to send to any server.")
@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):
""" """
@@ -643,3 +707,26 @@ class FederationClient(FederationBase):
event.internal_metadata.outlier = outlier event.internal_metadata.outlier = outlier
return event return event
@defer.inlineCallbacks
def forward_third_party_invite(self, destinations, room_id, event_dict):
for destination in destinations:
if destination == self.server_name:
continue
try:
yield self.transport_layer.exchange_third_party_invite(
destination=destination,
room_id=room_id,
event_dict=event_dict,
)
defer.returnValue(None)
except CodeMessageException:
raise
except Exception as e:
logger.exception(
"Failed to send_third_party_invite via %s: %s",
destination, e.message
)
raise RuntimeError("Failed to send to any server.")

View File

@@ -254,6 +254,20 @@ class FederationServer(FederationBase):
], ],
})) }))
@defer.inlineCallbacks
def on_make_leave_request(self, room_id, user_id):
pdu = yield self.handler.on_make_leave_request(room_id, user_id)
time_now = self._clock.time_msec()
defer.returnValue({"event": pdu.get_pdu_json(time_now)})
@defer.inlineCallbacks
def on_send_leave_request(self, origin, content):
logger.debug("on_send_leave_request: content: %s", content)
pdu = self.event_from_pdu_json(content)
logger.debug("on_send_leave_request: pdu sigs: %s", pdu.signatures)
yield self.handler.on_send_leave_request(origin, pdu)
defer.returnValue((200, {}))
@defer.inlineCallbacks @defer.inlineCallbacks
def on_event_auth(self, origin, room_id, event_id): def on_event_auth(self, origin, room_id, event_id):
time_now = self._clock.time_msec() time_now = self._clock.time_msec()
@@ -529,3 +543,15 @@ class FederationServer(FederationBase):
event.internal_metadata.outlier = outlier event.internal_metadata.outlier = outlier
return event return event
@defer.inlineCallbacks
def exchange_third_party_invite(self, invite):
ret = yield self.handler.exchange_third_party_invite(invite)
defer.returnValue(ret)
@defer.inlineCallbacks
def on_exchange_third_party_invite_request(self, origin, room_id, event_dict):
ret = yield self.handler.on_exchange_third_party_invite_request(
origin, room_id, event_dict
)
defer.returnValue(ret)

View File

@@ -202,6 +202,7 @@ class TransactionQueue(object):
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @log_function
def _attempt_new_transaction(self, destination): def _attempt_new_transaction(self, destination):
# list of (pending_pdu, deferred, order)
if destination in self.pending_transactions: if destination in self.pending_transactions:
# XXX: pending_transactions can get stuck on by a never-ending # XXX: pending_transactions can get stuck on by a never-ending
# request at which point pending_pdus_by_dest just keeps growing. # request at which point pending_pdus_by_dest just keeps growing.
@@ -213,9 +214,6 @@ class TransactionQueue(object):
) )
return return
logger.debug("TX [%s] _attempt_new_transaction", destination)
# list of (pending_pdu, deferred, order)
pending_pdus = self.pending_pdus_by_dest.pop(destination, []) pending_pdus = self.pending_pdus_by_dest.pop(destination, [])
pending_edus = self.pending_edus_by_dest.pop(destination, []) pending_edus = self.pending_edus_by_dest.pop(destination, [])
pending_failures = self.pending_failures_by_dest.pop(destination, []) pending_failures = self.pending_failures_by_dest.pop(destination, [])
@@ -228,20 +226,22 @@ class TransactionQueue(object):
logger.debug("TX [%s] Nothing to send", destination) logger.debug("TX [%s] Nothing to send", destination)
return return
# 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
]
try: try:
self.pending_transactions[destination] = 1 self.pending_transactions[destination] = 1
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( limiter = yield get_retry_limiter(

View File

@@ -14,6 +14,7 @@
# limitations under the License. # limitations under the License.
from twisted.internet import defer from twisted.internet import defer
from synapse.api.constants import Membership
from synapse.api.urls import FEDERATION_PREFIX as PREFIX from synapse.api.urls import FEDERATION_PREFIX as PREFIX
from synapse.util.logutils import log_function from synapse.util.logutils import log_function
@@ -135,6 +136,7 @@ class TransportLayerClient(object):
path=PREFIX + "/send/%s/" % transaction.transaction_id, path=PREFIX + "/send/%s/" % transaction.transaction_id,
data=json_data, data=json_data,
json_data_callback=json_data_callback, json_data_callback=json_data_callback,
long_retries=True,
) )
logger.debug( logger.debug(
@@ -160,13 +162,19 @@ class TransportLayerClient(object):
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @log_function
def make_join(self, destination, room_id, user_id, retry_on_dns_fail=True): def make_membership_event(self, destination, room_id, user_id, membership):
path = PREFIX + "/make_join/%s/%s" % (room_id, user_id) valid_memberships = {Membership.JOIN, Membership.LEAVE}
if membership not in valid_memberships:
raise RuntimeError(
"make_membership_event called with membership='%s', must be one of %s" %
(membership, ",".join(valid_memberships))
)
path = PREFIX + "/make_%s/%s/%s" % (membership, room_id, user_id)
content = yield self.client.get_json( content = yield self.client.get_json(
destination=destination, destination=destination,
path=path, path=path,
retry_on_dns_fail=retry_on_dns_fail, retry_on_dns_fail=True,
) )
defer.returnValue(content) defer.returnValue(content)
@@ -184,6 +192,19 @@ class TransportLayerClient(object):
defer.returnValue(response) defer.returnValue(response)
@defer.inlineCallbacks
@log_function
def send_leave(self, destination, room_id, event_id, content):
path = PREFIX + "/send_leave/%s/%s" % (room_id, event_id)
response = yield self.client.put_json(
destination=destination,
path=path,
data=content,
)
defer.returnValue(response)
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @log_function
def send_invite(self, destination, room_id, event_id, content): def send_invite(self, destination, room_id, event_id, content):
@@ -197,6 +218,19 @@ class TransportLayerClient(object):
defer.returnValue(response) defer.returnValue(response)
@defer.inlineCallbacks
@log_function
def exchange_third_party_invite(self, destination, room_id, event_dict):
path = PREFIX + "/exchange_third_party_invite/%s" % (room_id,)
response = yield self.client.put_json(
destination=destination,
path=path,
data=event_dict,
)
defer.returnValue(response)
@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):

View File

@@ -165,7 +165,7 @@ class BaseFederationServlet(object):
if code is None: if code is None:
continue continue
server.register_path(method, pattern, self._wrap(code)) server.register_paths(method, (pattern,), self._wrap(code))
class FederationSendServlet(BaseFederationServlet): class FederationSendServlet(BaseFederationServlet):
@@ -296,6 +296,24 @@ class FederationMakeJoinServlet(BaseFederationServlet):
defer.returnValue((200, content)) defer.returnValue((200, content))
class FederationMakeLeaveServlet(BaseFederationServlet):
PATH = "/make_leave/([^/]*)/([^/]*)"
@defer.inlineCallbacks
def on_GET(self, origin, content, query, context, user_id):
content = yield self.handler.on_make_leave_request(context, user_id)
defer.returnValue((200, content))
class FederationSendLeaveServlet(BaseFederationServlet):
PATH = "/send_leave/([^/]*)/([^/]*)"
@defer.inlineCallbacks
def on_PUT(self, origin, content, query, room_id, txid):
content = yield self.handler.on_send_leave_request(origin, content)
defer.returnValue((200, content))
class FederationEventAuthServlet(BaseFederationServlet): class FederationEventAuthServlet(BaseFederationServlet):
PATH = "/event_auth/([^/]*)/([^/]*)" PATH = "/event_auth/([^/]*)/([^/]*)"
@@ -325,6 +343,17 @@ class FederationInviteServlet(BaseFederationServlet):
defer.returnValue((200, content)) defer.returnValue((200, content))
class FederationThirdPartyInviteExchangeServlet(BaseFederationServlet):
PATH = "/exchange_third_party_invite/([^/]*)"
@defer.inlineCallbacks
def on_PUT(self, origin, content, query, room_id):
content = yield self.handler.on_exchange_third_party_invite_request(
origin, room_id, content
)
defer.returnValue((200, content))
class FederationClientKeysQueryServlet(BaseFederationServlet): class FederationClientKeysQueryServlet(BaseFederationServlet):
PATH = "/user/keys/query" PATH = "/user/keys/query"
@@ -378,6 +407,30 @@ class FederationGetMissingEventsServlet(BaseFederationServlet):
defer.returnValue((200, content)) defer.returnValue((200, content))
class On3pidBindServlet(BaseFederationServlet):
PATH = "/3pid/onbind"
@defer.inlineCallbacks
def on_POST(self, request):
content_bytes = request.content.read()
content = json.loads(content_bytes)
if "invites" in content:
last_exception = None
for invite in content["invites"]:
try:
yield self.handler.exchange_third_party_invite(invite)
except Exception as e:
last_exception = e
if last_exception:
raise last_exception
defer.returnValue((200, {}))
# Avoid doing remote HS authorization checks which are done by default by
# BaseFederationServlet.
def _wrap(self, code):
return code
SERVLET_CLASSES = ( SERVLET_CLASSES = (
FederationPullServlet, FederationPullServlet,
FederationEventServlet, FederationEventServlet,
@@ -385,12 +438,16 @@ SERVLET_CLASSES = (
FederationBackfillServlet, FederationBackfillServlet,
FederationQueryServlet, FederationQueryServlet,
FederationMakeJoinServlet, FederationMakeJoinServlet,
FederationMakeLeaveServlet,
FederationEventServlet, FederationEventServlet,
FederationSendJoinServlet, FederationSendJoinServlet,
FederationSendLeaveServlet,
FederationInviteServlet, FederationInviteServlet,
FederationQueryAuthServlet, FederationQueryAuthServlet,
FederationGetMissingEventsServlet, FederationGetMissingEventsServlet,
FederationEventAuthServlet, FederationEventAuthServlet,
FederationClientKeysQueryServlet, FederationClientKeysQueryServlet,
FederationClientKeysClaimServlet, FederationClientKeysClaimServlet,
FederationThirdPartyInviteExchangeServlet,
On3pidBindServlet,
) )

View File

@@ -17,7 +17,7 @@ from synapse.appservice.scheduler import AppServiceScheduler
from synapse.appservice.api import ApplicationServiceApi from synapse.appservice.api import ApplicationServiceApi
from .register import RegistrationHandler from .register import RegistrationHandler
from .room import ( from .room import (
RoomCreationHandler, RoomMemberHandler, RoomListHandler RoomCreationHandler, RoomMemberHandler, RoomListHandler, RoomContextHandler,
) )
from .message import MessageHandler from .message import MessageHandler
from .events import EventStreamHandler, EventHandler from .events import EventStreamHandler, EventHandler
@@ -32,6 +32,7 @@ from .sync import SyncHandler
from .auth import AuthHandler from .auth import AuthHandler
from .identity import IdentityHandler from .identity import IdentityHandler
from .receipts import ReceiptsHandler from .receipts import ReceiptsHandler
from .search import SearchHandler
class Handlers(object): class Handlers(object):
@@ -68,3 +69,5 @@ class Handlers(object):
self.sync_handler = SyncHandler(hs) self.sync_handler = SyncHandler(hs)
self.auth_handler = AuthHandler(hs) self.auth_handler = AuthHandler(hs)
self.identity_handler = IdentityHandler(hs) self.identity_handler = IdentityHandler(hs)
self.search_handler = SearchHandler(hs)
self.room_context_handler = RoomContextHandler(hs)

View File

@@ -29,6 +29,12 @@ logger = logging.getLogger(__name__)
class BaseHandler(object): class BaseHandler(object):
"""
Common base class for the event handlers.
:type store: synapse.storage.events.StateStore
:type state_handler: synapse.state.StateHandler
"""
def __init__(self, hs): def __init__(self, hs):
self.store = hs.get_datastore() self.store = hs.get_datastore()
@@ -45,6 +51,82 @@ class BaseHandler(object):
self.event_builder_factory = hs.get_event_builder_factory() self.event_builder_factory = hs.get_event_builder_factory()
@defer.inlineCallbacks
def _filter_events_for_client(self, user_id, events, is_guest=False,
require_all_visible_for_guests=True):
# Assumes that user has at some point joined the room if not is_guest.
def allowed(event, membership, visibility):
if visibility == "world_readable":
return True
if is_guest:
return False
if membership == Membership.JOIN:
return True
if event.type == EventTypes.RoomHistoryVisibility:
return not is_guest
if visibility == "shared":
return True
elif visibility == "joined":
return membership == Membership.JOIN
elif visibility == "invited":
return membership == Membership.INVITE
return True
event_id_to_state = yield self.store.get_state_for_events(
frozenset(e.event_id for e in events),
types=(
(EventTypes.RoomHistoryVisibility, ""),
(EventTypes.Member, user_id),
)
)
events_to_return = []
for event in events:
state = event_id_to_state[event.event_id]
membership_event = state.get((EventTypes.Member, user_id), None)
if membership_event:
was_forgotten_at_event = yield self.store.was_forgotten_at(
membership_event.state_key,
membership_event.room_id,
membership_event.event_id
)
if was_forgotten_at_event:
membership = None
else:
membership = membership_event.membership
else:
membership = None
visibility_event = state.get((EventTypes.RoomHistoryVisibility, ""), None)
if visibility_event:
visibility = visibility_event.content.get("history_visibility", "shared")
else:
visibility = "shared"
should_include = allowed(event, membership, visibility)
if should_include:
events_to_return.append(event)
if (require_all_visible_for_guests
and is_guest
and len(events_to_return) < len(events)):
# This indicates that some events in the requested range were not
# visible to guest users. To be safe, we reject the entire request,
# so that we don't have to worry about interpreting visibility
# boundaries.
raise AuthError(403, "User %s does not have permission" % (
user_id
))
defer.returnValue(events_to_return)
def ratelimit(self, user_id): def ratelimit(self, user_id):
time_now = self.clock.time() time_now = self.clock.time()
allowed, time_allowed = self.ratelimiter.send_message( allowed, time_allowed = self.ratelimiter.send_message(
@@ -107,6 +189,8 @@ class BaseHandler(object):
if not suppress_auth: if not suppress_auth:
self.auth.check(event, auth_events=context.current_state) 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: if event.type == EventTypes.CanonicalAlias:
# Check the alias is acually valid (at this time at least) # Check the alias is acually valid (at this time at least)
room_alias_str = event.content.get("alias", None) room_alias_str = event.content.get("alias", None)
@@ -123,24 +207,39 @@ class BaseHandler(object):
) )
) )
(event_stream_id, max_stream_id) = yield self.store.persist_event(
event, context=context
)
federation_handler = self.hs.get_handlers().federation_handler federation_handler = self.hs.get_handlers().federation_handler
if event.type == EventTypes.Member: if event.type == EventTypes.Member:
if event.content["membership"] == Membership.INVITE: if event.content["membership"] == Membership.INVITE:
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 (
EventTypes.JoinRules,
EventTypes.CanonicalAlias,
EventTypes.RoomAvatar,
EventTypes.Name,
)
]
invitee = UserID.from_string(event.state_key) invitee = UserID.from_string(event.state_key)
if not self.hs.is_mine(invitee): if not self.hs.is_mine(invitee):
# TODO: Can we add signature from remote server in a nicer # TODO: Can we add signature from remote server in a nicer
# way? If we have been invited by a remote server, we need # way? If we have been invited by a remote server, we need
# to get them to sign the event. # to get them to sign the event.
returned_invite = yield federation_handler.send_invite( returned_invite = yield federation_handler.send_invite(
invitee.domain, invitee.domain,
event, event,
) )
event.unsigned.pop("room_state", None)
# TODO: Make sure the signatures actually are correct. # TODO: Make sure the signatures actually are correct.
event.signatures.update( event.signatures.update(
returned_invite.signatures returned_invite.signatures
@@ -161,6 +260,10 @@ class BaseHandler(object):
"You don't have permission to redact events" "You don't have permission to redact events"
) )
(event_stream_id, max_stream_id) = yield self.store.persist_event(
event, context=context
)
destinations = set(extra_destinations) destinations = set(extra_destinations)
for k, s in context.current_state.items(): for k, s in context.current_state.items():
try: try:
@@ -189,6 +292,64 @@ class BaseHandler(object):
notify_d.addErrback(log_failure) notify_d.addErrback(log_failure)
# If invite, remove room_state from unsigned before sending.
event.unsigned.pop("invite_room_state", None)
federation_handler.handle_new_event( federation_handler.handle_new_event(
event, destinations=destinations, event, destinations=destinations,
) )
@defer.inlineCallbacks
def maybe_kick_guest_users(self, event, current_state):
# Technically this function invalidates current_state by changing it.
# Hopefully this isn't that important to the caller.
if event.type == EventTypes.GuestAccess:
guest_access = event.content.get("guest_access", "forbidden")
if guest_access != "can_join":
yield self.kick_guest_users(current_state)
@defer.inlineCallbacks
def kick_guest_users(self, current_state):
for member_event in current_state:
try:
if member_event.type != EventTypes.Member:
continue
if not self.hs.is_mine(UserID.from_string(member_event.state_key)):
continue
if member_event.content["membership"] not in {
Membership.JOIN,
Membership.INVITE
}:
continue
if (
"kind" not in member_event.content
or member_event.content["kind"] != "guest"
):
continue
# We make the user choose to leave, rather than have the
# event-sender kick them. This is partially because we don't
# need to worry about power levels, and partially because guest
# users are a concept which doesn't hugely work over federation,
# and having homeservers have their own users leave keeps more
# of that decision-making and control local to the guest-having
# homeserver.
message_handler = self.hs.get_handlers().message_handler
yield message_handler.create_and_send_event(
{
"type": EventTypes.Member,
"state_key": member_event.state_key,
"content": {
"membership": Membership.LEAVE,
"kind": "guest"
},
"room_id": member_event.room_id,
"sender": member_event.state_key
},
ratelimit=False,
)
except Exception as e:
logger.warn("Error kicking guest user: %s" % (e,))

View File

@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
# Copyright 2015 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 twisted.internet import defer
class AccountDataEventSource(object):
def __init__(self, hs):
self.store = hs.get_datastore()
def get_current_key(self, direction='f'):
return self.store.get_max_account_data_stream_id()
@defer.inlineCallbacks
def get_new_events(self, user, from_key, **kwargs):
user_id = user.to_string()
last_stream_id = from_key
current_stream_id = yield self.store.get_max_account_data_stream_id()
results = []
tags = yield self.store.get_updated_tags(user_id, last_stream_id)
for room_id, room_tags in tags.items():
results.append({
"type": "m.tag",
"content": {"tags": room_tags},
"room_id": room_id,
})
account_data, room_account_data = (
yield self.store.get_updated_account_data_for_user(user_id, last_stream_id)
)
for account_data_type, content in account_data.items():
results.append({
"type": account_data_type,
"content": content,
})
for room_id, account_data in room_account_data.items():
for account_data_type, content in account_data.items():
results.append({
"type": account_data_type,
"content": content,
"room_id": room_id,
})
defer.returnValue((results, current_stream_id))
@defer.inlineCallbacks
def get_pagination_rows(self, user, config, key):
defer.returnValue(([], config.to_id))

View File

@@ -30,34 +30,27 @@ class AdminHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def get_whois(self, user): def get_whois(self, user):
res = yield self.store.get_user_ip_and_agents(user) connections = []
d = {} sessions = yield self.store.get_user_ip_and_agents(user)
for r in res: for session in sessions:
# Note that device_id is always None connections.append({
device = d.setdefault(r["device_id"], {}) "ip": session["ip"],
session = device.setdefault(r["access_token"], []) "last_seen": session["last_seen"],
session.append({ "user_agent": session["user_agent"],
"ip": r["ip"],
"user_agent": r["user_agent"],
"last_seen": r["last_seen"],
}) })
ret = { ret = {
"user_id": user.to_string(), "user_id": user.to_string(),
"devices": [ "devices": {
{ "": {
"device_id": k,
"sessions": [ "sessions": [
{ {
# "access_token": x, TODO (erikj) "connections": connections,
"connections": y,
} }
for x, y in v.items()
] ]
} },
for k, v in d.items() },
],
} }
defer.returnValue(ret) defer.returnValue(ret)

View File

@@ -18,8 +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 LoginError, Codes from synapse.api.errors import AuthError, LoginError, Codes
from synapse.http.client import SimpleHttpClient
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
@@ -45,7 +44,9 @@ class AuthHandler(BaseHandler):
LoginType.EMAIL_IDENTITY: self._check_email_identity, LoginType.EMAIL_IDENTITY: self._check_email_identity,
LoginType.DUMMY: self._check_dummy_auth, LoginType.DUMMY: self._check_dummy_auth,
} }
self.bcrypt_rounds = hs.config.bcrypt_rounds
self.sessions = {} self.sessions = {}
self.INVALID_TOKEN_HTTP_STATUS = 401
@defer.inlineCallbacks @defer.inlineCallbacks
def check_auth(self, flows, clientdict, clientip): def check_auth(self, flows, clientdict, clientip):
@@ -187,7 +188,7 @@ class AuthHandler(BaseHandler):
# TODO: get this from the homeserver rather than creating a new one for # TODO: get this from the homeserver rather than creating a new one for
# each request # each request
try: try:
client = SimpleHttpClient(self.hs) client = self.hs.get_simple_http_client()
resp_body = yield client.post_urlencoded_get_json( resp_body = yield client.post_urlencoded_get_json(
self.hs.config.recaptcha_siteverify_api, self.hs.config.recaptcha_siteverify_api,
args={ args={
@@ -296,6 +297,39 @@ class AuthHandler(BaseHandler):
refresh_token = yield self.issue_refresh_token(user_id) refresh_token = yield self.issue_refresh_token(user_id)
defer.returnValue((user_id, access_token, refresh_token)) defer.returnValue((user_id, access_token, refresh_token))
@defer.inlineCallbacks
def get_login_tuple_for_user_id(self, user_id):
"""
Gets login tuple for the user with the given user ID.
The user is assumed to have been authenticated by some other
machanism (e.g. CAS)
Args:
user_id (str): User ID
Returns:
A tuple of:
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.
"""
user_id, ignored = yield self._find_user_id_and_pwd_hash(user_id)
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
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
@@ -340,12 +374,15 @@ class AuthHandler(BaseHandler):
yield self.store.add_refresh_token_to_user(user_id, refresh_token) yield self.store.add_refresh_token_to_user(user_id, refresh_token)
defer.returnValue(refresh_token) defer.returnValue(refresh_token)
def generate_access_token(self, user_id): def generate_access_token(self, user_id, extra_caveats=None):
extra_caveats = extra_caveats or []
macaroon = self._generate_base_macaroon(user_id) macaroon = self._generate_base_macaroon(user_id)
macaroon.add_first_party_caveat("type = access") macaroon.add_first_party_caveat("type = access")
now = self.hs.get_clock().time_msec() now = self.hs.get_clock().time_msec()
expiry = now + (60 * 60 * 1000) expiry = now + (60 * 60 * 1000)
macaroon.add_first_party_caveat("time < %d" % (expiry,)) macaroon.add_first_party_caveat("time < %d" % (expiry,))
for caveat in extra_caveats:
macaroon.add_first_party_caveat(caveat)
return macaroon.serialize() return macaroon.serialize()
def generate_refresh_token(self, user_id): def generate_refresh_token(self, user_id):
@@ -358,6 +395,23 @@ class AuthHandler(BaseHandler):
)) ))
return m.serialize() 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):
try:
macaroon = pymacaroons.Macaroon.deserialize(login_token)
auth_api = self.hs.get_auth()
auth_api.validate_macaroon(macaroon, "login", True)
return self._get_user_from_macaroon(macaroon)
except (pymacaroons.exceptions.MacaroonException, TypeError, ValueError):
raise AuthError(401, "Invalid token", errcode=Codes.UNKNOWN_TOKEN)
def _generate_base_macaroon(self, user_id): def _generate_base_macaroon(self, user_id):
macaroon = pymacaroons.Macaroon( macaroon = pymacaroons.Macaroon(
location=self.hs.config.server_name, location=self.hs.config.server_name,
@@ -367,6 +421,16 @@ class AuthHandler(BaseHandler):
macaroon.add_first_party_caveat("user_id = %s" % (user_id,)) macaroon.add_first_party_caveat("user_id = %s" % (user_id,))
return macaroon 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): def set_password(self, user_id, newpassword):
password_hash = self.hash(newpassword) password_hash = self.hash(newpassword)
@@ -401,7 +465,7 @@ class AuthHandler(BaseHandler):
Returns: Returns:
Hashed password (str). Hashed password (str).
""" """
return bcrypt.hashpw(password, bcrypt.gensalt()) return bcrypt.hashpw(password, 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.

View File

@@ -28,6 +28,18 @@ import random
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def started_user_eventstream(distributor, user):
return distributor.fire("started_user_eventstream", user)
def stopped_user_eventstream(distributor, user):
return distributor.fire("stopped_user_eventstream", user)
def user_joined_room(distributor, user, room_id):
return distributor.fire("user_joined_room", user, room_id)
class EventStreamHandler(BaseHandler): class EventStreamHandler(BaseHandler):
def __init__(self, hs): def __init__(self, hs):
@@ -46,11 +58,66 @@ class EventStreamHandler(BaseHandler):
self.notifier = hs.get_notifier() self.notifier = hs.get_notifier()
@defer.inlineCallbacks
def started_stream(self, user):
"""Tells the presence handler that we have started an eventstream for
the user:
Args:
user (User): The user who started a stream.
Returns:
A deferred that completes once their presence has been updated.
"""
if user not in self._streams_per_user:
# Make sure we set the streams per user to 1 here rather than
# setting it to zero and incrementing the value below.
# Otherwise this may race with stopped_stream causing the
# user to be erased from the map before we have a chance
# to increment it.
self._streams_per_user[user] = 1
if user in self._stop_timer_per_user:
try:
self.clock.cancel_call_later(
self._stop_timer_per_user.pop(user)
)
except:
logger.exception("Failed to cancel event timer")
else:
yield started_user_eventstream(self.distributor, user)
else:
self._streams_per_user[user] += 1
def stopped_stream(self, user):
"""If there are no streams for a user this starts a timer that will
notify the presence handler that we haven't got an event stream for
the user unless the user starts a new stream in 30 seconds.
Args:
user (User): The user who stopped a stream.
"""
self._streams_per_user[user] -= 1
if not self._streams_per_user[user]:
del self._streams_per_user[user]
# 30 seconds of grace to allow the client to reconnect again
# before we think they're gone
def _later():
logger.debug("_later stopped_user_eventstream %s", user)
self._stop_timer_per_user.pop(user, None)
return stopped_user_eventstream(self.distributor, user)
logger.debug("Scheduling _later: for %s", user)
self._stop_timer_per_user[user] = (
self.clock.call_later(30, _later)
)
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @log_function
def get_stream(self, auth_user_id, pagin_config, timeout=0, def get_stream(self, auth_user_id, pagin_config, timeout=0,
as_client_event=True, affect_presence=True, as_client_event=True, affect_presence=True,
only_room_events=False): only_room_events=False, room_id=None, is_guest=False):
"""Fetches the events stream for a given user. """Fetches the events stream for a given user.
If `only_room_events` is `True` only room events will be returned. If `only_room_events` is `True` only room events will be returned.
@@ -59,31 +126,7 @@ class EventStreamHandler(BaseHandler):
try: try:
if affect_presence: if affect_presence:
if auth_user not in self._streams_per_user: yield self.started_stream(auth_user)
self._streams_per_user[auth_user] = 0
if auth_user in self._stop_timer_per_user:
try:
self.clock.cancel_call_later(
self._stop_timer_per_user.pop(auth_user)
)
except:
logger.exception("Failed to cancel event timer")
else:
yield self.distributor.fire(
"started_user_eventstream", auth_user
)
self._streams_per_user[auth_user] += 1
rm_handler = self.hs.get_handlers().room_member_handler
app_service = yield self.store.get_app_service_by_user_id(
auth_user.to_string()
)
if app_service:
rooms = yield self.store.get_app_service_rooms(app_service)
room_ids = set(r.room_id for r in rooms)
else:
room_ids = yield rm_handler.get_joined_rooms_for_user(auth_user)
if timeout: if timeout:
# If they've set a timeout set a minimum limit. # If they've set a timeout set a minimum limit.
@@ -93,9 +136,13 @@ class EventStreamHandler(BaseHandler):
# thundering herds on restart. # thundering herds on restart.
timeout = random.randint(int(timeout*0.9), int(timeout*1.1)) timeout = random.randint(int(timeout*0.9), int(timeout*1.1))
if is_guest:
yield user_joined_room(self.distributor, auth_user, room_id)
events, tokens = yield self.notifier.get_events_for( events, tokens = yield self.notifier.get_events_for(
auth_user, room_ids, pagin_config, timeout, auth_user, pagin_config, timeout,
only_room_events=only_room_events only_room_events=only_room_events,
is_guest=is_guest, guest_room_id=room_id
) )
time_now = self.clock.time_msec() time_now = self.clock.time_msec()
@@ -114,27 +161,7 @@ class EventStreamHandler(BaseHandler):
finally: finally:
if affect_presence: if affect_presence:
self._streams_per_user[auth_user] -= 1 self.stopped_stream(auth_user)
if not self._streams_per_user[auth_user]:
del self._streams_per_user[auth_user]
# 10 seconds of grace to allow the client to reconnect again
# before we think they're gone
def _later():
logger.debug(
"_later stopped_user_eventstream %s", auth_user
)
self._stop_timer_per_user.pop(auth_user, None)
return self.distributor.fire(
"stopped_user_eventstream", auth_user
)
logger.debug("Scheduling _later: for %s", auth_user)
self._stop_timer_per_user[auth_user] = (
self.clock.call_later(30, _later)
)
class EventHandler(BaseHandler): class EventHandler(BaseHandler):

View File

@@ -21,6 +21,7 @@ from synapse.api.errors import (
AuthError, FederationError, StoreError, CodeMessageException, SynapseError, AuthError, FederationError, StoreError, CodeMessageException, SynapseError,
) )
from synapse.api.constants import EventTypes, Membership, RejectedReason from synapse.api.constants import EventTypes, Membership, RejectedReason
from synapse.events.validator import EventValidator
from synapse.util import unwrapFirstError from synapse.util import unwrapFirstError
from synapse.util.logcontext import PreserveLoggingContext from synapse.util.logcontext import PreserveLoggingContext
from synapse.util.logutils import log_function from synapse.util.logutils import log_function
@@ -40,10 +41,13 @@ from twisted.internet import defer
import itertools import itertools
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def user_joined_room(distributor, user, room_id):
return distributor.fire("user_joined_room", user, room_id)
class FederationHandler(BaseHandler): class FederationHandler(BaseHandler):
"""Handles events that originated from federation. """Handles events that originated from federation.
Responsible for: Responsible for:
@@ -58,22 +62,18 @@ class FederationHandler(BaseHandler):
def __init__(self, hs): def __init__(self, hs):
super(FederationHandler, self).__init__(hs) super(FederationHandler, self).__init__(hs)
self.distributor.observe( self.hs = hs
"user_joined_room",
self._on_user_joined self.distributor.observe("user_joined_room", self.user_joined_room)
)
self.waiting_for_join_list = {} self.waiting_for_join_list = {}
self.store = hs.get_datastore() self.store = hs.get_datastore()
self.replication_layer = hs.get_replication_layer() self.replication_layer = hs.get_replication_layer()
self.state_handler = hs.get_state_handler() self.state_handler = hs.get_state_handler()
# self.auth_handler = gs.get_auth_handler()
self.server_name = hs.hostname self.server_name = hs.hostname
self.keyring = hs.get_keyring() self.keyring = hs.get_keyring()
self.lock_manager = hs.get_room_lock_manager()
self.replication_layer.set_handler(self) self.replication_layer.set_handler(self)
# When joining a room we need to queue any events for that room up # When joining a room we need to queue any events for that room up
@@ -125,60 +125,72 @@ class FederationHandler(BaseHandler):
) )
if not is_in_room and not event.internal_metadata.is_outlier(): if not is_in_room and not event.internal_metadata.is_outlier():
logger.debug("Got event for room we're not in.") logger.debug("Got event for room we're not in.")
current_state = state
event_ids = set() try:
if state: event_stream_id, max_stream_id = yield self._persist_auth_tree(
event_ids |= {e.event_id for e in state} auth_chain, state, event
if auth_chain: )
event_ids |= {e.event_id for e in auth_chain} except AuthError as e:
raise FederationError(
"ERROR",
e.code,
e.msg,
affected=event.event_id,
)
seen_ids = set( else:
(yield self.store.have_events(event_ids)).keys() event_ids = set()
) if state:
event_ids |= {e.event_id for e in state}
if auth_chain:
event_ids |= {e.event_id for e in auth_chain}
if state and auth_chain is not None: seen_ids = set(
# If we have any state or auth_chain given to us by the replication (yield self.store.have_events(event_ids)).keys()
# layer, then we should handle them (if we haven't before.)
event_infos = []
for e in itertools.chain(auth_chain, state):
if e.event_id in seen_ids:
continue
e.internal_metadata.outlier = True
auth_ids = [e_id for e_id, _ in e.auth_events]
auth = {
(e.type, e.state_key): e for e in auth_chain
if e.event_id in auth_ids
}
event_infos.append({
"event": e,
"auth_events": auth,
})
seen_ids.add(e.event_id)
yield self._handle_new_events(
origin,
event_infos,
outliers=True
) )
try: if state and auth_chain is not None:
_, event_stream_id, max_stream_id = yield self._handle_new_event( # If we have any state or auth_chain given to us by the replication
origin, # layer, then we should handle them (if we haven't before.)
event,
state=state, event_infos = []
backfilled=backfilled,
current_state=current_state, for e in itertools.chain(auth_chain, state):
) if e.event_id in seen_ids:
except AuthError as e: continue
raise FederationError( e.internal_metadata.outlier = True
"ERROR", auth_ids = [e_id for e_id, _ in e.auth_events]
e.code, auth = {
e.msg, (e.type, e.state_key): e for e in auth_chain
affected=event.event_id, if e.event_id in auth_ids or e.type == EventTypes.Create
) }
event_infos.append({
"event": e,
"auth_events": auth,
})
seen_ids.add(e.event_id)
yield self._handle_new_events(
origin,
event_infos,
outliers=True
)
try:
context, event_stream_id, max_stream_id = yield self._handle_new_event(
origin,
event,
state=state,
backfilled=backfilled,
current_state=current_state,
)
except AuthError as e:
raise FederationError(
"ERROR",
e.code,
e.msg,
affected=event.event_id,
)
# if we're receiving valid events from an origin, # if we're receiving valid events from an origin,
# it's probably a good idea to mark it as not in retry-state # it's probably a good idea to mark it as not in retry-state
@@ -222,15 +234,18 @@ class FederationHandler(BaseHandler):
if event.type == EventTypes.Member: if event.type == EventTypes.Member:
if event.membership == Membership.JOIN: if event.membership == Membership.JOIN:
user = UserID.from_string(event.state_key) prev_state = context.current_state.get((event.type, event.state_key))
yield self.distributor.fire( if not prev_state or prev_state.membership != Membership.JOIN:
"user_joined_room", user=user, room_id=event.room_id # Only fire user_joined_room if the user has acutally
) # joined the room. Don't bother if the user is just
# changing their profile info.
user = UserID.from_string(event.state_key)
yield user_joined_room(self.distributor, user, event.room_id)
@defer.inlineCallbacks @defer.inlineCallbacks
def _filter_events_for_server(self, server_name, room_id, events): def _filter_events_for_server(self, server_name, room_id, events):
event_to_state = yield self.store.get_state_for_events( event_to_state = yield self.store.get_state_for_events(
room_id, frozenset(e.event_id for e in events), frozenset(e.event_id for e in events),
types=( types=(
(EventTypes.RoomHistoryVisibility, ""), (EventTypes.RoomHistoryVisibility, ""),
(EventTypes.Member, None), (EventTypes.Member, None),
@@ -553,7 +568,7 @@ class FederationHandler(BaseHandler):
@log_function @log_function
@defer.inlineCallbacks @defer.inlineCallbacks
def do_invite_join(self, target_hosts, room_id, joinee, content, snapshot): def do_invite_join(self, target_hosts, room_id, joinee, content):
""" Attempts to join the `joinee` to the room `room_id` via the """ Attempts to join the `joinee` to the room `room_id` via the
server `target_host`. server `target_host`.
@@ -569,49 +584,19 @@ class FederationHandler(BaseHandler):
yield self.store.clean_room_for_join(room_id) yield self.store.clean_room_for_join(room_id)
origin, pdu = yield self.replication_layer.make_join( origin, event = yield self._make_and_verify_event(
target_hosts, target_hosts,
room_id, room_id,
joinee joinee,
"join",
content,
) )
logger.debug("Got response to make_join: %s", pdu)
event = pdu
# We should assert some things.
# FIXME: Do this in a nicer way
assert(event.type == EventTypes.Member)
assert(event.user_id == joinee)
assert(event.state_key == joinee)
assert(event.room_id == room_id)
event.internal_metadata.outlier = False
self.room_queues[room_id] = [] self.room_queues[room_id] = []
builder = self.event_builder_factory.new(
unfreeze(event.get_pdu_json())
)
handled_events = set() handled_events = set()
try: try:
builder.event_id = self.event_builder_factory.create_event_id() event = self._sign_event(event)
builder.origin = self.hs.hostname
builder.content = content
if not hasattr(event, "signatures"):
builder.signatures = {}
add_hashes_and_signatures(
builder,
self.hs.hostname,
self.hs.config.signing_key[0],
)
new_event = builder.build()
# Try the host we successfully got a response to /make_join/ # Try the host we successfully got a response to /make_join/
# request first. # request first.
try: try:
@@ -619,11 +604,7 @@ class FederationHandler(BaseHandler):
target_hosts.insert(0, origin) target_hosts.insert(0, origin)
except ValueError: except ValueError:
pass pass
ret = yield self.replication_layer.send_join(target_hosts, event)
ret = yield self.replication_layer.send_join(
target_hosts,
new_event
)
origin = ret["origin"] origin = ret["origin"]
state = ret["state"] state = ret["state"]
@@ -632,12 +613,12 @@ class FederationHandler(BaseHandler):
handled_events.update([s.event_id for s in state]) handled_events.update([s.event_id for s in state])
handled_events.update([a.event_id for a in auth_chain]) handled_events.update([a.event_id for a in auth_chain])
handled_events.add(new_event.event_id) handled_events.add(event.event_id)
logger.debug("do_invite_join auth_chain: %s", auth_chain) logger.debug("do_invite_join auth_chain: %s", auth_chain)
logger.debug("do_invite_join state: %s", state) logger.debug("do_invite_join state: %s", state)
logger.debug("do_invite_join event: %s", new_event) logger.debug("do_invite_join event: %s", event)
try: try:
yield self.store.store_room( yield self.store.store_room(
@@ -649,47 +630,20 @@ class FederationHandler(BaseHandler):
# FIXME # FIXME
pass pass
ev_infos = [] event_stream_id, max_stream_id = yield self._persist_auth_tree(
for e in itertools.chain(state, auth_chain): auth_chain, state, event
if e.event_id == event.event_id:
continue
e.internal_metadata.outlier = True
auth_ids = [e_id for e_id, _ in e.auth_events]
ev_infos.append({
"event": e,
"auth_events": {
(e.type, e.state_key): e for e in auth_chain
if e.event_id in auth_ids
}
})
yield self._handle_new_events(origin, ev_infos, outliers=True)
auth_ids = [e_id for e_id, _ in event.auth_events]
auth_events = {
(e.type, e.state_key): e for e in auth_chain
if e.event_id in auth_ids
}
_, event_stream_id, max_stream_id = yield self._handle_new_event(
origin,
new_event,
state=state,
current_state=state,
auth_events=auth_events,
) )
with PreserveLoggingContext(): with PreserveLoggingContext():
d = self.notifier.on_new_room_event( d = self.notifier.on_new_room_event(
new_event, event_stream_id, max_stream_id, event, event_stream_id, max_stream_id,
extra_users=[joinee] extra_users=[joinee]
) )
def log_failure(f): def log_failure(f):
logger.warn( logger.warn(
"Failed to notify about %s: %s", "Failed to notify about %s: %s",
new_event.event_id, f.value event.event_id, f.value
) )
d.addErrback(log_failure) d.addErrback(log_failure)
@@ -714,12 +668,14 @@ class FederationHandler(BaseHandler):
@log_function @log_function
def on_make_join_request(self, room_id, user_id): def on_make_join_request(self, room_id, user_id):
""" We've received a /make_join/ request, so we create a partial """ We've received a /make_join/ request, so we create a partial
join event for the room and return that. We don *not* persist or join event for the room and return that. We do *not* persist or
process it until the other server has signed it and sent it back. process it until the other server has signed it and sent it back.
""" """
event_content = {"membership": Membership.JOIN}
builder = self.event_builder_factory.new({ builder = self.event_builder_factory.new({
"type": EventTypes.Member, "type": EventTypes.Member,
"content": {"membership": Membership.JOIN}, "content": event_content,
"room_id": room_id, "room_id": room_id,
"sender": user_id, "sender": user_id,
"state_key": user_id, "state_key": user_id,
@@ -781,9 +737,7 @@ class FederationHandler(BaseHandler):
if event.type == EventTypes.Member: if event.type == EventTypes.Member:
if event.content["membership"] == Membership.JOIN: if event.content["membership"] == Membership.JOIN:
user = UserID.from_string(event.state_key) user = UserID.from_string(event.state_key)
yield self.distributor.fire( yield user_joined_room(self.distributor, user, event.room_id)
"user_joined_room", user=user, room_id=event.room_id
)
new_pdu = event new_pdu = event
@@ -864,6 +818,168 @@ class FederationHandler(BaseHandler):
defer.returnValue(event) defer.returnValue(event)
@defer.inlineCallbacks
def do_remotely_reject_invite(self, target_hosts, room_id, user_id):
origin, event = yield self._make_and_verify_event(
target_hosts,
room_id,
user_id,
"leave"
)
signed_event = self._sign_event(event)
# Try the host we successfully got a response to /make_join/
# request first.
try:
target_hosts.remove(origin)
target_hosts.insert(0, origin)
except ValueError:
pass
yield self.replication_layer.send_leave(
target_hosts,
signed_event
)
defer.returnValue(None)
@defer.inlineCallbacks
def _make_and_verify_event(self, target_hosts, room_id, user_id, membership,
content={},):
origin, pdu = yield self.replication_layer.make_membership_event(
target_hosts,
room_id,
user_id,
membership,
content,
)
logger.debug("Got response to make_%s: %s", membership, pdu)
event = pdu
# We should assert some things.
# FIXME: Do this in a nicer way
assert(event.type == EventTypes.Member)
assert(event.user_id == user_id)
assert(event.state_key == user_id)
assert(event.room_id == room_id)
defer.returnValue((origin, event))
def _sign_event(self, event):
event.internal_metadata.outlier = False
builder = self.event_builder_factory.new(
unfreeze(event.get_pdu_json())
)
builder.event_id = self.event_builder_factory.create_event_id()
builder.origin = self.hs.hostname
if not hasattr(event, "signatures"):
builder.signatures = {}
add_hashes_and_signatures(
builder,
self.hs.hostname,
self.hs.config.signing_key[0],
)
return builder.build()
@defer.inlineCallbacks
@log_function
def on_make_leave_request(self, room_id, user_id):
""" We've received a /make_leave/ request, so we create a partial
join event for the room and return that. We do *not* persist or
process it until the other server has signed it and sent it back.
"""
builder = self.event_builder_factory.new({
"type": EventTypes.Member,
"content": {"membership": Membership.LEAVE},
"room_id": room_id,
"sender": user_id,
"state_key": user_id,
})
event, context = yield self._create_new_client_event(
builder=builder,
)
self.auth.check(event, auth_events=context.current_state)
defer.returnValue(event)
@defer.inlineCallbacks
@log_function
def on_send_leave_request(self, origin, pdu):
""" We have received a leave event for a room. Fully process it."""
event = pdu
logger.debug(
"on_send_leave_request: Got event: %s, signatures: %s",
event.event_id,
event.signatures,
)
event.internal_metadata.outlier = False
context, event_stream_id, max_stream_id = yield self._handle_new_event(
origin, event
)
logger.debug(
"on_send_leave_request: After _handle_new_event: %s, sigs: %s",
event.event_id,
event.signatures,
)
extra_users = []
if event.type == EventTypes.Member:
target_user_id = event.state_key
target_user = UserID.from_string(target_user_id)
extra_users.append(target_user)
with PreserveLoggingContext():
d = self.notifier.on_new_room_event(
event, event_stream_id, max_stream_id, extra_users=extra_users
)
def log_failure(f):
logger.warn(
"Failed to notify about %s: %s",
event.event_id, f.value
)
d.addErrback(log_failure)
new_pdu = event
destinations = set()
for k, s in context.current_state.items():
try:
if k[0] == EventTypes.Member:
if s.content["membership"] == Membership.LEAVE:
destinations.add(
UserID.from_string(s.state_key).domain
)
except:
logger.warn(
"Failed to get destination from event %s", s.event_id
)
destinations.discard(origin)
logger.debug(
"on_send_leave_request: Sending event: %s, signatures: %s",
event.event_id,
event.signatures,
)
self.replication_layer.send_pdu(new_pdu, destinations)
defer.returnValue(None)
@defer.inlineCallbacks @defer.inlineCallbacks
def get_state_for_pdu(self, origin, room_id, event_id, do_auth=True): def get_state_for_pdu(self, origin, room_id, event_id, do_auth=True):
yield run_on_reactor() yield run_on_reactor()
@@ -968,7 +1084,7 @@ class FederationHandler(BaseHandler):
return self.store.get_min_depth(context) return self.store.get_min_depth(context)
@log_function @log_function
def _on_user_joined(self, user, room_id): def user_joined_room(self, user, room_id):
waiters = self.waiting_for_join_list.get( waiters = self.waiting_for_join_list.get(
(user.to_string(), room_id), (user.to_string(), room_id),
[] []
@@ -986,8 +1102,6 @@ class FederationHandler(BaseHandler):
context = yield self._prep_event( context = yield self._prep_event(
origin, event, origin, event,
state=state, state=state,
backfilled=backfilled,
current_state=current_state,
auth_events=auth_events, auth_events=auth_events,
) )
@@ -1010,7 +1124,6 @@ class FederationHandler(BaseHandler):
origin, origin,
ev_info["event"], ev_info["event"],
state=ev_info.get("state"), state=ev_info.get("state"),
backfilled=backfilled,
auth_events=ev_info.get("auth_events"), auth_events=ev_info.get("auth_events"),
) )
for ev_info in event_infos for ev_info in event_infos
@@ -1027,8 +1140,77 @@ class FederationHandler(BaseHandler):
) )
@defer.inlineCallbacks @defer.inlineCallbacks
def _prep_event(self, origin, event, state=None, backfilled=False, def _persist_auth_tree(self, auth_events, state, event):
current_state=None, auth_events=None): """Checks the auth chain is valid (and passes auth checks) for the
state and event. Then persists the auth chain and state atomically.
Persists the event seperately.
Returns:
2-tuple of (event_stream_id, max_stream_id) from the persist_event
call for `event`
"""
events_to_context = {}
for e in itertools.chain(auth_events, state):
ctx = yield self.state_handler.compute_event_context(
e, outlier=True,
)
events_to_context[e.event_id] = ctx
e.internal_metadata.outlier = True
event_map = {
e.event_id: e
for e in auth_events
}
create_event = None
for e in auth_events:
if (e.type, e.state_key) == (EventTypes.Create, ""):
create_event = e
break
for e in itertools.chain(auth_events, state, [event]):
auth_for_e = {
(event_map[e_id].type, event_map[e_id].state_key): event_map[e_id]
for e_id, _ in e.auth_events
}
if create_event:
auth_for_e[(EventTypes.Create, "")] = create_event
try:
self.auth.check(e, auth_events=auth_for_e)
except AuthError as err:
logger.warn(
"Rejecting %s because %s",
e.event_id, err.msg
)
if e == event:
raise
events_to_context[e.event_id].rejected = RejectedReason.AUTH_ERROR
yield self.store.persist_events(
[
(e, events_to_context[e.event_id])
for e in itertools.chain(auth_events, state)
],
is_new_state=False,
)
new_event_context = yield self.state_handler.compute_event_context(
event, old_state=state, outlier=False,
)
event_stream_id, max_stream_id = yield self.store.persist_event(
event, new_event_context,
backfilled=False,
is_new_state=True,
current_state=state,
)
defer.returnValue((event_stream_id, max_stream_id))
@defer.inlineCallbacks
def _prep_event(self, origin, event, state=None, auth_events=None):
outlier = event.internal_metadata.is_outlier() outlier = event.internal_metadata.is_outlier()
context = yield self.state_handler.compute_event_context( context = yield self.state_handler.compute_event_context(
@@ -1061,6 +1243,10 @@ class FederationHandler(BaseHandler):
context.rejected = RejectedReason.AUTH_ERROR context.rejected = RejectedReason.AUTH_ERROR
if event.type == EventTypes.GuestAccess:
full_context = yield self.store.get_current_state(room_id=event.room_id)
yield self.maybe_kick_guest_users(event, full_context)
defer.returnValue(context) defer.returnValue(context)
@defer.inlineCallbacks @defer.inlineCallbacks
@@ -1166,7 +1352,7 @@ class FederationHandler(BaseHandler):
auth_ids = [e_id for e_id, _ in e.auth_events] auth_ids = [e_id for e_id, _ in e.auth_events]
auth = { auth = {
(e.type, e.state_key): e for e in remote_auth_chain (e.type, e.state_key): e for e in remote_auth_chain
if e.event_id in auth_ids if e.event_id in auth_ids or e.type == EventTypes.Create
} }
e.internal_metadata.outlier = True e.internal_metadata.outlier = True
@@ -1284,6 +1470,7 @@ class FederationHandler(BaseHandler):
(e.type, e.state_key): e (e.type, e.state_key): e
for e in result["auth_chain"] for e in result["auth_chain"]
if e.event_id in auth_ids if e.event_id in auth_ids
or event.type == EventTypes.Create
} }
ev.internal_metadata.outlier = True ev.internal_metadata.outlier = True
@@ -1458,50 +1645,114 @@ class FederationHandler(BaseHandler):
}) })
@defer.inlineCallbacks @defer.inlineCallbacks
def _handle_auth_events(self, origin, auth_events): @log_function
auth_ids_to_deferred = {} def exchange_third_party_invite(self, invite):
sender = invite["sender"]
room_id = invite["room_id"]
def process_auth_ev(ev): if "signed" not in invite or "token" not in invite["signed"]:
auth_ids = [e_id for e_id, _ in ev.auth_events] logger.info(
"Discarding received notification of third party invite "
"without signed: %s" % (invite,)
)
return
prev_ds = [ third_party_invite = {
auth_ids_to_deferred[i] "signed": invite["signed"],
for i in auth_ids }
if i in auth_ids_to_deferred
]
d = defer.Deferred() event_dict = {
"type": EventTypes.Member,
"content": {
"membership": Membership.INVITE,
"third_party_invite": third_party_invite,
},
"room_id": room_id,
"sender": sender,
"state_key": invite["mxid"],
}
auth_ids_to_deferred[ev.event_id] = d if (yield self.auth.check_host_in_room(room_id, self.hs.hostname)):
builder = self.event_builder_factory.new(event_dict)
EventValidator().validate_new(builder)
event, context = yield self._create_new_client_event(builder=builder)
@defer.inlineCallbacks event, context = yield self.add_display_name_to_third_party_invite(
def f(*_): event_dict, event, context
ev.internal_metadata.outlier = True )
try: self.auth.check(event, context.current_state)
auth = { yield self._validate_keyserver(event, auth_events=context.current_state)
(e.type, e.state_key): e for e in auth_events member_handler = self.hs.get_handlers().room_member_handler
if e.event_id in auth_ids yield member_handler.change_membership(event, context)
} else:
destinations = set([x.split(":", 1)[-1] for x in (sender, room_id)])
yield self.replication_layer.forward_third_party_invite(
destinations,
room_id,
event_dict,
)
yield self._handle_new_event( @defer.inlineCallbacks
origin, ev, auth_events=auth @log_function
) def on_exchange_third_party_invite_request(self, origin, room_id, event_dict):
except: builder = self.event_builder_factory.new(event_dict)
logger.exception(
"Failed to handle auth event %s",
ev.event_id,
)
d.callback(None) event, context = yield self._create_new_client_event(
builder=builder,
)
if prev_ds: event, context = yield self.add_display_name_to_third_party_invite(
dx = defer.DeferredList(prev_ds) event_dict, event, context
dx.addBoth(f) )
else:
f()
for e in auth_events: self.auth.check(event, auth_events=context.current_state)
process_auth_ev(e) yield self._validate_keyserver(event, auth_events=context.current_state)
yield defer.DeferredList(auth_ids_to_deferred.values()) returned_invite = yield self.send_invite(origin, event)
# TODO: Make sure the signatures actually are correct.
event.signatures.update(returned_invite.signatures)
member_handler = self.hs.get_handlers().room_member_handler
yield member_handler.change_membership(event, context)
@defer.inlineCallbacks
def add_display_name_to_third_party_invite(self, event_dict, event, context):
key = (
EventTypes.ThirdPartyInvite,
event.content["third_party_invite"]["signed"]["token"]
)
original_invite = context.current_state.get(key)
if not original_invite:
logger.info(
"Could not find invite event for third_party_invite - "
"discarding: %s" % (event_dict,)
)
return
display_name = original_invite.content["display_name"]
event_dict["content"]["third_party_invite"]["display_name"] = display_name
builder = self.event_builder_factory.new(event_dict)
EventValidator().validate_new(builder)
event, context = yield self._create_new_client_event(builder=builder)
defer.returnValue((event, context))
@defer.inlineCallbacks
def _validate_keyserver(self, event, auth_events):
token = event.content["third_party_invite"]["signed"]["token"]
invite_event = auth_events.get(
(EventTypes.ThirdPartyInvite, token,)
)
try:
response = yield self.hs.get_simple_http_client().get_json(
invite_event.content["key_validity_url"],
{"public_key": invite_event.content["public_key"]}
)
except Exception:
raise SynapseError(
502,
"Third party certificate could not be checked"
)
if "valid" not in response or not response["valid"]:
raise AuthError(403, "Third party certificate was invalid")

View File

@@ -20,7 +20,6 @@ from synapse.api.errors import (
CodeMessageException CodeMessageException
) )
from ._base import BaseHandler from ._base import BaseHandler
from synapse.http.client import SimpleHttpClient
from synapse.util.async import run_on_reactor from synapse.util.async import run_on_reactor
from synapse.api.errors import SynapseError from synapse.api.errors import SynapseError
@@ -35,13 +34,12 @@ class IdentityHandler(BaseHandler):
def __init__(self, hs): def __init__(self, hs):
super(IdentityHandler, self).__init__(hs) super(IdentityHandler, self).__init__(hs)
self.http_client = hs.get_simple_http_client()
@defer.inlineCallbacks @defer.inlineCallbacks
def threepid_from_creds(self, creds): def threepid_from_creds(self, creds):
yield run_on_reactor() yield run_on_reactor()
# TODO: get this from the homeserver rather than creating a new one for
# each request
http_client = SimpleHttpClient(self.hs)
# XXX: make this configurable! # XXX: make this configurable!
# trustedIdServers = ['matrix.org', 'localhost:8090'] # trustedIdServers = ['matrix.org', 'localhost:8090']
trustedIdServers = ['matrix.org', 'vector.im'] trustedIdServers = ['matrix.org', 'vector.im']
@@ -67,7 +65,7 @@ class IdentityHandler(BaseHandler):
data = {} data = {}
try: try:
data = yield http_client.get_json( data = yield self.http_client.get_json(
"https://%s%s" % ( "https://%s%s" % (
id_server, id_server,
"/_matrix/identity/api/v1/3pid/getValidated3pid" "/_matrix/identity/api/v1/3pid/getValidated3pid"
@@ -85,7 +83,6 @@ class IdentityHandler(BaseHandler):
def bind_threepid(self, creds, mxid): def bind_threepid(self, creds, mxid):
yield run_on_reactor() yield run_on_reactor()
logger.debug("binding threepid %r to %s", creds, mxid) logger.debug("binding threepid %r to %s", creds, mxid)
http_client = SimpleHttpClient(self.hs)
data = None data = None
if 'id_server' in creds: if 'id_server' in creds:
@@ -103,7 +100,7 @@ class IdentityHandler(BaseHandler):
raise SynapseError(400, "No client_secret in creds") raise SynapseError(400, "No client_secret in creds")
try: try:
data = yield http_client.post_urlencoded_get_json( data = yield self.http_client.post_urlencoded_get_json(
"https://%s%s" % ( "https://%s%s" % (
id_server, "/_matrix/identity/api/v1/3pid/bind" id_server, "/_matrix/identity/api/v1/3pid/bind"
), ),
@@ -121,7 +118,6 @@ class IdentityHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def requestEmailToken(self, id_server, email, client_secret, send_attempt, **kwargs): def requestEmailToken(self, id_server, email, client_secret, send_attempt, **kwargs):
yield run_on_reactor() yield run_on_reactor()
http_client = SimpleHttpClient(self.hs)
params = { params = {
'email': email, 'email': email,
@@ -131,7 +127,7 @@ class IdentityHandler(BaseHandler):
params.update(kwargs) params.update(kwargs)
try: try:
data = yield http_client.post_urlencoded_get_json( data = yield self.http_client.post_urlencoded_get_json(
"https://%s%s" % ( "https://%s%s" % (
id_server, id_server,
"/_matrix/identity/api/v1/validate/email/requestToken" "/_matrix/identity/api/v1/validate/email/requestToken"

View File

@@ -16,21 +16,28 @@
from twisted.internet import defer from twisted.internet import defer
from synapse.api.constants import EventTypes, Membership from synapse.api.constants import EventTypes, Membership
from synapse.api.errors import RoomError, SynapseError from synapse.api.errors import SynapseError, AuthError, Codes
from synapse.streams.config import PaginationConfig from synapse.streams.config import PaginationConfig
from synapse.events.utils import serialize_event from synapse.events.utils import serialize_event
from synapse.events.validator import EventValidator from synapse.events.validator import EventValidator
from synapse.util import unwrapFirstError from synapse.util import unwrapFirstError
from synapse.util.logcontext import PreserveLoggingContext from synapse.util.logcontext import PreserveLoggingContext
from synapse.types import UserID, RoomStreamToken from synapse.util.caches.snapshot_cache import SnapshotCache
from synapse.types import UserID, RoomStreamToken, StreamToken
from ._base import BaseHandler from ._base import BaseHandler
from canonicaljson import encode_canonical_json
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def collect_presencelike_data(distributor, user, content):
return distributor.fire("collect_presencelike_data", user, content)
class MessageHandler(BaseHandler): class MessageHandler(BaseHandler):
def __init__(self, hs): def __init__(self, hs):
@@ -39,6 +46,7 @@ class MessageHandler(BaseHandler):
self.state = hs.get_state_handler() self.state = hs.get_state_handler()
self.clock = hs.get_clock() self.clock = hs.get_clock()
self.validator = EventValidator() self.validator = EventValidator()
self.snapshot_cache = SnapshotCache()
@defer.inlineCallbacks @defer.inlineCallbacks
def get_message(self, msg_id=None, room_id=None, sender_id=None, def get_message(self, msg_id=None, room_id=None, sender_id=None,
@@ -71,34 +79,64 @@ class MessageHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def get_messages(self, user_id=None, room_id=None, pagin_config=None, def get_messages(self, user_id=None, room_id=None, pagin_config=None,
feedback=False, as_client_event=True): as_client_event=True, is_guest=False):
"""Get messages in a room. """Get messages in a room.
Args: Args:
user_id (str): The user requesting messages. user_id (str): The user requesting messages.
room_id (str): The room they want messages from. room_id (str): The room they want messages from.
pagin_config (synapse.api.streams.PaginationConfig): The pagination pagin_config (synapse.api.streams.PaginationConfig): The pagination
config rules to apply, if any. config rules to apply, if any.
feedback (bool): True to get compressed feedback with the messages
as_client_event (bool): True to get events in client-server format. as_client_event (bool): True to get events in client-server format.
is_guest (bool): Whether the requesting user is a guest (as opposed
to a fully registered user).
Returns: Returns:
dict: Pagination API results dict: Pagination API results
""" """
yield self.auth.check_joined_room(room_id, user_id)
data_source = self.hs.get_event_sources().sources["room"] data_source = self.hs.get_event_sources().sources["room"]
if not pagin_config.from_token: if pagin_config.from_token:
room_token = pagin_config.from_token.room_key
else:
pagin_config.from_token = ( pagin_config.from_token = (
yield self.hs.get_event_sources().get_current_token( yield self.hs.get_event_sources().get_current_token(
direction='b' direction='b'
) )
) )
room_token = pagin_config.from_token.room_key
room_token = RoomStreamToken.parse(pagin_config.from_token.room_key) room_token = RoomStreamToken.parse(room_token)
if room_token.topological is None: if room_token.topological is None:
raise SynapseError(400, "Invalid token") raise SynapseError(400, "Invalid token")
pagin_config.from_token = pagin_config.from_token.copy_and_replace(
"room_key", str(room_token)
)
source_config = pagin_config.get_source_config("room")
if not is_guest:
member_event = yield self.auth.check_user_was_in_room(room_id, user_id)
if member_event.membership == Membership.LEAVE:
# If they have left the room then clamp the token to be before
# they left the room.
# If they're a guest, we'll just 403 them if they're asking for
# events they can't see.
leave_token = yield self.store.get_topological_token_for_event(
member_event.event_id
)
leave_token = RoomStreamToken.parse(leave_token)
if leave_token.topological < room_token.topological:
source_config.from_key = str(leave_token)
if source_config.direction == "f":
if source_config.to_key is None:
source_config.to_key = str(leave_token)
else:
to_token = RoomStreamToken.parse(source_config.to_key)
if leave_token.topological < to_token.topological:
source_config.to_key = str(leave_token)
yield self.hs.get_handlers().federation_handler.maybe_backfill( yield self.hs.get_handlers().federation_handler.maybe_backfill(
room_id, room_token.topological room_id, room_token.topological
) )
@@ -106,7 +144,7 @@ class MessageHandler(BaseHandler):
user = UserID.from_string(user_id) user = UserID.from_string(user_id)
events, next_key = yield data_source.get_pagination_rows( events, next_key = yield data_source.get_pagination_rows(
user, pagin_config.get_source_config("room"), room_id user, source_config, room_id
) )
next_token = pagin_config.from_token.copy_and_replace( next_token = pagin_config.from_token.copy_and_replace(
@@ -120,7 +158,7 @@ class MessageHandler(BaseHandler):
"end": next_token.to_string(), "end": next_token.to_string(),
}) })
events = yield self._filter_events_for_client(user_id, room_id, events) events = yield self._filter_events_for_client(user_id, events, is_guest=is_guest)
time_now = self.clock.time_msec() time_now = self.clock.time_msec()
@@ -135,55 +173,9 @@ class MessageHandler(BaseHandler):
defer.returnValue(chunk) defer.returnValue(chunk)
@defer.inlineCallbacks
def _filter_events_for_client(self, user_id, room_id, events):
event_id_to_state = yield self.store.get_state_for_events(
room_id, frozenset(e.event_id for e in events),
types=(
(EventTypes.RoomHistoryVisibility, ""),
(EventTypes.Member, user_id),
)
)
def allowed(event, state):
if event.type == EventTypes.RoomHistoryVisibility:
return True
membership_ev = state.get((EventTypes.Member, user_id), None)
if membership_ev:
membership = membership_ev.membership
else:
membership = Membership.LEAVE
if membership == Membership.JOIN:
return True
history = state.get((EventTypes.RoomHistoryVisibility, ''), None)
if history:
visibility = history.content.get("history_visibility", "shared")
else:
visibility = "shared"
if visibility == "public":
return True
elif visibility == "shared":
return True
elif visibility == "joined":
return membership == Membership.JOIN
elif visibility == "invited":
return membership == Membership.INVITE
return True
defer.returnValue([
event
for event in events
if allowed(event, event_id_to_state[event.event_id])
])
@defer.inlineCallbacks @defer.inlineCallbacks
def create_and_send_event(self, event_dict, ratelimit=True, def create_and_send_event(self, event_dict, ratelimit=True,
token_id=None, txn_id=None): token_id=None, txn_id=None, is_guest=False):
""" Given a dict from a client, create and handle a new event. """ Given a dict from a client, create and handle a new event.
Creates an FrozenEvent object, filling out auth_events, prev_events, Creates an FrozenEvent object, filling out auth_events, prev_events,
@@ -211,10 +203,8 @@ class MessageHandler(BaseHandler):
if membership == Membership.JOIN: if membership == Membership.JOIN:
joinee = UserID.from_string(builder.state_key) joinee = UserID.from_string(builder.state_key)
# If event doesn't include a display name, add one. # If event doesn't include a display name, add one.
yield self.distributor.fire( yield collect_presencelike_data(
"collect_presencelike_data", self.distributor, joinee, builder.content
joinee,
builder.content
) )
if token_id is not None: if token_id is not None:
@@ -227,9 +217,19 @@ class MessageHandler(BaseHandler):
builder=builder, builder=builder,
) )
if event.is_state():
prev_state = context.current_state.get((event.type, event.state_key))
if prev_state and event.user_id == prev_state.user_id:
prev_content = encode_canonical_json(prev_state.content)
next_content = encode_canonical_json(event.content)
if prev_content == next_content:
# Duplicate suppression for state updates with same sender
# and content.
defer.returnValue(prev_state)
if event.type == EventTypes.Member: if event.type == EventTypes.Member:
member_handler = self.hs.get_handlers().room_member_handler member_handler = self.hs.get_handlers().room_member_handler
yield member_handler.change_membership(event, context) yield member_handler.change_membership(event, context, is_guest=is_guest)
else: else:
yield self.handle_new_client_event( yield self.handle_new_client_event(
event=event, event=event,
@@ -245,7 +245,7 @@ class MessageHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def get_room_data(self, user_id=None, room_id=None, def get_room_data(self, user_id=None, room_id=None,
event_type=None, state_key=""): event_type=None, state_key="", is_guest=False):
""" Get data from a room. """ Get data from a room.
Args: Args:
@@ -255,29 +255,55 @@ class MessageHandler(BaseHandler):
Raises: Raises:
SynapseError if something went wrong. SynapseError if something went wrong.
""" """
have_joined = yield self.auth.check_joined_room(room_id, user_id) membership, membership_event_id = yield self._check_in_room_or_world_readable(
if not have_joined: room_id, user_id, is_guest
raise RoomError(403, "User not in room.")
data = yield self.state_handler.get_current_state(
room_id, event_type, state_key
) )
if membership == Membership.JOIN:
data = yield self.state_handler.get_current_state(
room_id, event_type, state_key
)
elif membership == Membership.LEAVE:
key = (event_type, state_key)
room_state = yield self.store.get_state_for_events(
[membership_event_id], [key]
)
data = room_state[membership_event_id].get(key)
defer.returnValue(data) defer.returnValue(data)
@defer.inlineCallbacks @defer.inlineCallbacks
def get_feedback(self, event_id): def _check_in_room_or_world_readable(self, room_id, user_id, is_guest):
# yield self.auth.check_joined_room(room_id, user_id) try:
# check_user_was_in_room will return the most recent membership
# Pull out the feedback from the db # event for the user if:
fb = yield self.store.get_feedback(event_id) # * The user is a non-guest user, and was ever in the room
# * The user is a guest user, and has joined the room
if fb: # else it will throw.
defer.returnValue(fb) member_event = yield self.auth.check_user_was_in_room(room_id, user_id)
defer.returnValue(None) defer.returnValue((member_event.membership, member_event.event_id))
return
except AuthError, auth_error:
visibility = yield self.state_handler.get_current_state(
room_id, EventTypes.RoomHistoryVisibility, ""
)
if (
visibility and
visibility.content["history_visibility"] == "world_readable"
):
defer.returnValue((Membership.JOIN, None))
return
if not is_guest:
raise auth_error
raise AuthError(
403, "Guest access not allowed", errcode=Codes.GUEST_ACCESS_FORBIDDEN
)
@defer.inlineCallbacks @defer.inlineCallbacks
def get_state_events(self, user_id, room_id): def get_state_events(self, user_id, room_id, is_guest=False):
"""Retrieve all state events for a given room. """Retrieve all state events for a given room. If the user is
joined to the room then return the current state. If the user has
left the room return the state events from when they left.
Args: Args:
user_id(str): The user requesting state events. user_id(str): The user requesting state events.
@@ -285,18 +311,25 @@ class MessageHandler(BaseHandler):
Returns: Returns:
A list of dicts representing state events. [{}, {}, {}] A list of dicts representing state events. [{}, {}, {}]
""" """
yield self.auth.check_joined_room(room_id, user_id) membership, membership_event_id = yield self._check_in_room_or_world_readable(
room_id, user_id, is_guest
# TODO: This is duplicating logic from snapshot_all_rooms )
current_state = yield self.state_handler.get_current_state(room_id)
now = self.clock.time_msec() if membership == Membership.JOIN:
defer.returnValue( room_state = yield self.state_handler.get_current_state(room_id)
[serialize_event(c, now) for c in current_state.values()] elif membership == Membership.LEAVE:
room_state = yield self.store.get_state_for_events(
[membership_event_id], None
)
room_state = room_state[membership_event_id]
now = self.clock.time_msec()
defer.returnValue(
[serialize_event(c, now) for c in room_state.values()]
) )
@defer.inlineCallbacks
def snapshot_all_rooms(self, user_id=None, pagin_config=None, def snapshot_all_rooms(self, user_id=None, pagin_config=None,
feedback=False, as_client_event=True): as_client_event=True, include_archived=False):
"""Retrieve a snapshot of all rooms the user is invited or has joined. """Retrieve a snapshot of all rooms the user is invited or has joined.
This snapshot may include messages for all rooms where the user is This snapshot may include messages for all rooms where the user is
@@ -306,17 +339,42 @@ class MessageHandler(BaseHandler):
user_id (str): The ID of the user making the request. user_id (str): The ID of the user making the request.
pagin_config (synapse.api.streams.PaginationConfig): The pagination pagin_config (synapse.api.streams.PaginationConfig): The pagination
config used to determine how many messages *PER ROOM* to return. config used to determine how many messages *PER ROOM* to return.
feedback (bool): True to get feedback along with these messages.
as_client_event (bool): True to get events in client-server format. as_client_event (bool): True to get events in client-server format.
include_archived (bool): True to get rooms that the user has left
Returns: Returns:
A list of dicts with "room_id" and "membership" keys for all rooms A list of dicts with "room_id" and "membership" keys for all rooms
the user is currently invited or joined in on. Rooms where the user the user is currently invited or joined in on. Rooms where the user
is joined on, may return a "messages" key with messages, depending is joined on, may return a "messages" key with messages, depending
on the specified PaginationConfig. on the specified PaginationConfig.
""" """
key = (
user_id,
pagin_config.from_token,
pagin_config.to_token,
pagin_config.direction,
pagin_config.limit,
as_client_event,
include_archived,
)
now_ms = self.clock.time_msec()
result = self.snapshot_cache.get(now_ms, key)
if result is not None:
return result
return self.snapshot_cache.set(now_ms, key, self._snapshot_all_rooms(
user_id, pagin_config, as_client_event, include_archived
))
@defer.inlineCallbacks
def _snapshot_all_rooms(self, user_id=None, pagin_config=None,
as_client_event=True, include_archived=False):
memberships = [Membership.INVITE, Membership.JOIN]
if include_archived:
memberships.append(Membership.LEAVE)
room_list = yield self.store.get_rooms_for_user_where_membership_is( room_list = yield self.store.get_rooms_for_user_where_membership_is(
user_id=user_id, user_id=user_id, membership_list=memberships
membership_list=[Membership.INVITE, Membership.JOIN]
) )
user = UserID.from_string(user_id) user = UserID.from_string(user_id)
@@ -336,6 +394,12 @@ class MessageHandler(BaseHandler):
user, pagination_config.get_source_config("receipt"), None user, pagination_config.get_source_config("receipt"), None
) )
tags_by_room = yield self.store.get_tags_for_user(user_id)
account_data, account_data_by_room = (
yield self.store.get_account_data_for_user(user_id)
)
public_room_ids = yield self.store.get_public_room_ids() public_room_ids = yield self.store.get_public_room_ids()
limit = pagin_config.limit limit = pagin_config.limit
@@ -354,28 +418,45 @@ class MessageHandler(BaseHandler):
} }
if event.membership == Membership.INVITE: if event.membership == Membership.INVITE:
time_now = self.clock.time_msec()
d["inviter"] = event.sender d["inviter"] = event.sender
invite_event = yield self.store.get_event(event.event_id)
d["invite"] = serialize_event(invite_event, time_now, as_client_event)
rooms_ret.append(d) rooms_ret.append(d)
if event.membership != Membership.JOIN: if event.membership not in (Membership.JOIN, Membership.LEAVE):
return return
try: try:
if event.membership == Membership.JOIN:
room_end_token = now_token.room_key
deferred_room_state = self.state_handler.get_current_state(
event.room_id
)
elif event.membership == Membership.LEAVE:
room_end_token = "s%d" % (event.stream_ordering,)
deferred_room_state = self.store.get_state_for_events(
[event.event_id], None
)
deferred_room_state.addCallback(
lambda states: states[event.event_id]
)
(messages, token), current_state = yield defer.gatherResults( (messages, token), current_state = yield defer.gatherResults(
[ [
self.store.get_recent_events_for_room( self.store.get_recent_events_for_room(
event.room_id, event.room_id,
limit=limit, limit=limit,
end_token=now_token.room_key, end_token=room_end_token,
),
self.state_handler.get_current_state(
event.room_id
), ),
deferred_room_state,
] ]
).addErrback(unwrapFirstError) ).addErrback(unwrapFirstError)
messages = yield self._filter_events_for_client( messages = yield self._filter_events_for_client(
user_id, event.room_id, messages user_id, messages
) )
start_token = now_token.copy_and_replace("room_key", token[0]) start_token = now_token.copy_and_replace("room_key", token[0])
@@ -395,6 +476,23 @@ class MessageHandler(BaseHandler):
serialize_event(c, time_now, as_client_event) serialize_event(c, time_now, as_client_event)
for c in current_state.values() for c in current_state.values()
] ]
account_data_events = []
tags = tags_by_room.get(event.room_id)
if tags:
account_data_events.append({
"type": "m.tag",
"content": {"tags": tags},
})
account_data = account_data_by_room.get(event.room_id, {})
for account_data_type, content in account_data.items():
account_data_events.append({
"type": account_data_type,
"content": content,
})
d["account_data"] = account_data_events
except: except:
logger.exception("Failed to get snapshot") logger.exception("Failed to get snapshot")
@@ -407,9 +505,17 @@ class MessageHandler(BaseHandler):
consumeErrors=True consumeErrors=True
).addErrback(unwrapFirstError) ).addErrback(unwrapFirstError)
account_data_events = []
for account_data_type, content in account_data.items():
account_data_events.append({
"type": account_data_type,
"content": content,
})
ret = { ret = {
"rooms": rooms_ret, "rooms": rooms_ret,
"presence": presence, "presence": presence,
"account_data": account_data_events,
"receipts": receipt, "receipts": receipt,
"end": now_token.to_string(), "end": now_token.to_string(),
} }
@@ -417,15 +523,107 @@ class MessageHandler(BaseHandler):
defer.returnValue(ret) defer.returnValue(ret)
@defer.inlineCallbacks @defer.inlineCallbacks
def room_initial_sync(self, user_id, room_id, pagin_config=None, def room_initial_sync(self, user_id, room_id, pagin_config=None, is_guest=False):
feedback=False): """Capture the a snapshot of a room. If user is currently a member of
current_state = yield self.state.get_current_state( the room this will be what is currently in the room. If the user left
room_id=room_id, the room this will be what was in the room when they left.
Args:
user_id(str): The user to get a snapshot for.
room_id(str): The room to get a snapshot of.
pagin_config(synapse.streams.config.PaginationConfig):
The pagination config used to determine how many messages to
return.
Raises:
AuthError if the user wasn't in the room.
Returns:
A JSON serialisable dict with the snapshot of the room.
"""
membership, member_event_id = yield self._check_in_room_or_world_readable(
room_id,
user_id,
is_guest
) )
yield self.auth.check_joined_room( if membership == Membership.JOIN:
room_id, user_id, result = yield self._room_initial_sync_joined(
current_state=current_state user_id, room_id, pagin_config, membership, is_guest
)
elif membership == Membership.LEAVE:
result = yield self._room_initial_sync_parted(
user_id, room_id, pagin_config, membership, member_event_id, is_guest
)
account_data_events = []
tags = yield self.store.get_tags_for_room(user_id, room_id)
if tags:
account_data_events.append({
"type": "m.tag",
"content": {"tags": tags},
})
account_data = yield self.store.get_account_data_for_room(user_id, room_id)
for account_data_type, content in account_data.items():
account_data_events.append({
"type": account_data_type,
"content": content,
})
result["account_data"] = account_data_events
defer.returnValue(result)
@defer.inlineCallbacks
def _room_initial_sync_parted(self, user_id, room_id, pagin_config,
membership, member_event_id, is_guest):
room_state = yield self.store.get_state_for_events(
[member_event_id], None
)
room_state = room_state[member_event_id]
limit = pagin_config.limit if pagin_config else None
if limit is None:
limit = 10
stream_token = yield self.store.get_stream_token_for_event(
member_event_id
)
messages, token = yield self.store.get_recent_events_for_room(
room_id,
limit=limit,
end_token=stream_token
)
messages = yield self._filter_events_for_client(
user_id, messages, is_guest=is_guest
)
start_token = StreamToken(token[0], 0, 0, 0, 0)
end_token = StreamToken(token[1], 0, 0, 0, 0)
time_now = self.clock.time_msec()
defer.returnValue({
"membership": membership,
"room_id": room_id,
"messages": {
"chunk": [serialize_event(m, time_now) for m in messages],
"start": start_token.to_string(),
"end": end_token.to_string(),
},
"state": [serialize_event(s, time_now) for s in room_state.values()],
"presence": [],
"receipts": [],
})
@defer.inlineCallbacks
def _room_initial_sync_joined(self, user_id, room_id, pagin_config,
membership, is_guest):
current_state = yield self.state.get_current_state(
room_id=room_id,
) )
# TODO(paul): I wish I was called with user objects not user_id # TODO(paul): I wish I was called with user objects not user_id
@@ -439,8 +637,6 @@ class MessageHandler(BaseHandler):
for x in current_state.values() for x in current_state.values()
] ]
member_event = current_state.get((EventTypes.Member, user_id,))
now_token = yield self.hs.get_event_sources().get_current_token() now_token = yield self.hs.get_event_sources().get_current_token()
limit = pagin_config.limit if pagin_config else None limit = pagin_config.limit if pagin_config else None
@@ -466,12 +662,19 @@ class MessageHandler(BaseHandler):
defer.returnValue(states.values()) defer.returnValue(states.values())
receipts_handler = self.hs.get_handlers().receipts_handler @defer.inlineCallbacks
def get_receipts():
receipts_handler = self.hs.get_handlers().receipts_handler
receipts = yield receipts_handler.get_receipts_for_room(
room_id,
now_token.receipt_key
)
defer.returnValue(receipts)
presence, receipts, (messages, token) = yield defer.gatherResults( presence, receipts, (messages, token) = yield defer.gatherResults(
[ [
get_presence(), get_presence(),
receipts_handler.get_receipts_for_room(room_id, now_token.receipt_key), get_receipts(),
self.store.get_recent_events_for_room( self.store.get_recent_events_for_room(
room_id, room_id,
limit=limit, limit=limit,
@@ -482,7 +685,7 @@ class MessageHandler(BaseHandler):
).addErrback(unwrapFirstError) ).addErrback(unwrapFirstError)
messages = yield self._filter_events_for_client( messages = yield self._filter_events_for_client(
user_id, room_id, messages user_id, messages, is_guest=is_guest, require_all_visible_for_guests=False
) )
start_token = now_token.copy_and_replace("room_key", token[0]) start_token = now_token.copy_and_replace("room_key", token[0])
@@ -490,8 +693,7 @@ class MessageHandler(BaseHandler):
time_now = self.clock.time_msec() time_now = self.clock.time_msec()
defer.returnValue({ ret = {
"membership": member_event.membership,
"room_id": room_id, "room_id": room_id,
"messages": { "messages": {
"chunk": [serialize_event(m, time_now) for m in messages], "chunk": [serialize_event(m, time_now) for m in messages],
@@ -501,4 +703,8 @@ class MessageHandler(BaseHandler):
"state": state, "state": state,
"presence": presence, "presence": presence,
"receipts": receipts, "receipts": receipts,
}) }
if not is_guest:
ret["membership"] = membership
defer.returnValue(ret)

View File

@@ -62,6 +62,14 @@ def partitionbool(l, func):
return ret.get(True, []), ret.get(False, []) return ret.get(True, []), ret.get(False, [])
def user_presence_changed(distributor, user, statuscache):
return distributor.fire("user_presence_changed", user, statuscache)
def collect_presencelike_data(distributor, user, content):
return distributor.fire("collect_presencelike_data", user, content)
class PresenceHandler(BaseHandler): class PresenceHandler(BaseHandler):
STATE_LEVELS = { STATE_LEVELS = {
@@ -361,9 +369,7 @@ class PresenceHandler(BaseHandler):
yield self.store.set_presence_state( yield self.store.set_presence_state(
target_user.localpart, state_to_store target_user.localpart, state_to_store
) )
yield self.distributor.fire( yield collect_presencelike_data(self.distributor, target_user, state)
"collect_presencelike_data", target_user, state
)
if now_level > was_level: if now_level > was_level:
state["last_active"] = self.clock.time_msec() state["last_active"] = self.clock.time_msec()
@@ -378,7 +384,7 @@ class PresenceHandler(BaseHandler):
# TODO(paul): perform a presence push as part of start/stop poll so # TODO(paul): perform a presence push as part of start/stop poll so
# we don't have to do this all the time # we don't have to do this all the time
self.changed_presencelike_data(target_user, state) yield self.changed_presencelike_data(target_user, state)
def bump_presence_active_time(self, user, now=None): def bump_presence_active_time(self, user, now=None):
if now is None: if now is None:
@@ -422,12 +428,12 @@ class PresenceHandler(BaseHandler):
@log_function @log_function
def started_user_eventstream(self, user): def started_user_eventstream(self, user):
# TODO(paul): Use "last online" state # TODO(paul): Use "last online" state
self.set_state(user, user, {"presence": PresenceState.ONLINE}) return self.set_state(user, user, {"presence": PresenceState.ONLINE})
@log_function @log_function
def stopped_user_eventstream(self, user): def stopped_user_eventstream(self, user):
# TODO(paul): Save current state as "last online" state # TODO(paul): Save current state as "last online" state
self.set_state(user, user, {"presence": PresenceState.OFFLINE}) return self.set_state(user, user, {"presence": PresenceState.OFFLINE})
@defer.inlineCallbacks @defer.inlineCallbacks
def user_joined_room(self, user, room_id): def user_joined_room(self, user, room_id):
@@ -467,7 +473,7 @@ class PresenceHandler(BaseHandler):
) )
@defer.inlineCallbacks @defer.inlineCallbacks
def send_invite(self, observer_user, observed_user): def send_presence_invite(self, observer_user, observed_user):
"""Request the presence of a local or remote user for a local user""" """Request the presence of a local or remote user for a local user"""
if not self.hs.is_mine(observer_user): if not self.hs.is_mine(observer_user):
raise SynapseError(400, "User is not hosted on this Home Server") raise SynapseError(400, "User is not hosted on this Home Server")
@@ -878,7 +884,7 @@ class PresenceHandler(BaseHandler):
room_ids=room_ids, room_ids=room_ids,
statuscache=statuscache, statuscache=statuscache,
) )
yield self.distributor.fire("user_presence_changed", user, statuscache) yield user_presence_changed(self.distributor, user, statuscache)
@defer.inlineCallbacks @defer.inlineCallbacks
def incoming_presence(self, origin, content): def incoming_presence(self, origin, content):
@@ -950,7 +956,8 @@ class PresenceHandler(BaseHandler):
) )
while len(self._remote_offline_serials) > MAX_OFFLINE_SERIALS: while len(self._remote_offline_serials) > MAX_OFFLINE_SERIALS:
self._remote_offline_serials.pop() # remove the oldest self._remote_offline_serials.pop() # remove the oldest
del self._user_cachemap[user] if user in self._user_cachemap:
del self._user_cachemap[user]
else: else:
# Remove the user from remote_offline_serials now that they're # Remove the user from remote_offline_serials now that they're
# no longer offline # no longer offline
@@ -1115,9 +1122,7 @@ class PresenceHandler(BaseHandler):
self._user_cachemap[user].get_state()["last_active"] self._user_cachemap[user].get_state()["last_active"]
) )
yield self.distributor.fire( yield collect_presencelike_data(self.distributor, user, state)
"collect_presencelike_data", user, state
)
if "last_active" in state: if "last_active" in state:
state = dict(state) state = dict(state)
@@ -1142,8 +1147,9 @@ class PresenceEventSource(object):
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @log_function
def get_new_events_for_user(self, user, from_key, limit): def get_new_events(self, user, from_key, room_ids=None, **kwargs):
from_key = int(from_key) from_key = int(from_key)
room_ids = room_ids or []
presence = self.hs.get_handlers().presence_handler presence = self.hs.get_handlers().presence_handler
cachemap = presence._user_cachemap cachemap = presence._user_cachemap
@@ -1161,7 +1167,6 @@ class PresenceEventSource(object):
user_ids_to_check |= set( user_ids_to_check |= set(
UserID.from_string(p["observed_user_id"]) for p in presence_list UserID.from_string(p["observed_user_id"]) for p in presence_list
) )
room_ids = yield presence.get_joined_rooms_for_user(user)
for room_id in set(room_ids) & set(presence._room_serials): for room_id in set(room_ids) & set(presence._room_serials):
if presence._room_serials[room_id] > from_key: if presence._room_serials[room_id] > from_key:
joined = yield presence.get_joined_users_for_room_id(room_id) joined = yield presence.get_joined_users_for_room_id(room_id)
@@ -1263,6 +1268,11 @@ class UserPresenceCache(object):
self.state = {"presence": PresenceState.OFFLINE} self.state = {"presence": PresenceState.OFFLINE}
self.serial = None self.serial = None
def __repr__(self):
return "UserPresenceCache(state=%r, serial=%r)" % (
self.state, self.serial
)
def update(self, state, serial): def update(self, state, serial):
assert("mtime_age" not in state) assert("mtime_age" not in state)

View File

@@ -28,6 +28,14 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def changed_presencelike_data(distributor, user, state):
return distributor.fire("changed_presencelike_data", user, state)
def collect_presencelike_data(distributor, user, content):
return distributor.fire("collect_presencelike_data", user, content)
class ProfileHandler(BaseHandler): class ProfileHandler(BaseHandler):
def __init__(self, hs): def __init__(self, hs):
@@ -95,11 +103,9 @@ class ProfileHandler(BaseHandler):
target_user.localpart, new_displayname target_user.localpart, new_displayname
) )
yield self.distributor.fire( yield changed_presencelike_data(self.distributor, target_user, {
"changed_presencelike_data", target_user, { "displayname": new_displayname,
"displayname": new_displayname, })
}
)
yield self._update_join_states(target_user) yield self._update_join_states(target_user)
@@ -144,11 +150,9 @@ class ProfileHandler(BaseHandler):
target_user.localpart, new_avatar_url target_user.localpart, new_avatar_url
) )
yield self.distributor.fire( yield changed_presencelike_data(self.distributor, target_user, {
"changed_presencelike_data", target_user, { "avatar_url": new_avatar_url,
"avatar_url": new_avatar_url, })
}
)
yield self._update_join_states(target_user) yield self._update_join_states(target_user)
@@ -208,9 +212,7 @@ class ProfileHandler(BaseHandler):
"membership": Membership.JOIN, "membership": Membership.JOIN,
} }
yield self.distributor.fire( yield collect_presencelike_data(self.distributor, user, content)
"collect_presencelike_data", user, content
)
msg_handler = self.hs.get_handlers().message_handler msg_handler = self.hs.get_handlers().message_handler
try: try:

View File

@@ -156,13 +156,7 @@ class ReceiptsHandler(BaseHandler):
if not result: if not result:
defer.returnValue([]) defer.returnValue([])
event = { defer.returnValue(result)
"type": "m.receipt",
"room_id": room_id,
"content": result,
}
defer.returnValue([event])
class ReceiptEventSource(object): class ReceiptEventSource(object):
@@ -170,17 +164,15 @@ class ReceiptEventSource(object):
self.store = hs.get_datastore() self.store = hs.get_datastore()
@defer.inlineCallbacks @defer.inlineCallbacks
def get_new_events_for_user(self, user, from_key, limit): def get_new_events(self, from_key, room_ids, **kwargs):
from_key = int(from_key) from_key = int(from_key)
to_key = yield self.get_current_key() to_key = yield self.get_current_key()
if from_key == to_key: if from_key == to_key:
defer.returnValue(([], to_key)) defer.returnValue(([], to_key))
rooms = yield self.store.get_rooms_for_user(user.to_string())
rooms = [room.room_id for room in rooms]
events = yield self.store.get_linearized_receipts_for_rooms( events = yield self.store.get_linearized_receipts_for_rooms(
rooms, room_ids,
from_key=from_key, from_key=from_key,
to_key=to_key, to_key=to_key,
) )

View File

@@ -31,6 +31,10 @@ import urllib
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def registered_user(distributor, user):
return distributor.fire("registered_user", user)
class RegistrationHandler(BaseHandler): class RegistrationHandler(BaseHandler):
def __init__(self, hs): def __init__(self, hs):
@@ -38,6 +42,7 @@ class RegistrationHandler(BaseHandler):
self.distributor = hs.get_distributor() self.distributor = hs.get_distributor()
self.distributor.declare("registered_user") self.distributor.declare("registered_user")
self.captcha_client = CaptchaServerHttpClient(hs)
@defer.inlineCallbacks @defer.inlineCallbacks
def check_username(self, localpart): def check_username(self, localpart):
@@ -64,7 +69,7 @@ class RegistrationHandler(BaseHandler):
) )
@defer.inlineCallbacks @defer.inlineCallbacks
def register(self, localpart=None, password=None): def register(self, localpart=None, password=None, generate_token=True):
"""Registers a new client on the server. """Registers a new client on the server.
Args: Args:
@@ -89,33 +94,35 @@ class RegistrationHandler(BaseHandler):
user = UserID(localpart, self.hs.hostname) user = UserID(localpart, self.hs.hostname)
user_id = user.to_string() user_id = user.to_string()
token = self.auth_handler().generate_access_token(user_id) token = None
if generate_token:
token = self.auth_handler().generate_access_token(user_id)
yield self.store.register( yield self.store.register(
user_id=user_id, user_id=user_id,
token=token, token=token,
password_hash=password_hash password_hash=password_hash
) )
yield self.distributor.fire("registered_user", user) yield registered_user(self.distributor, user)
else: else:
# autogen a random user ID # autogen a random user ID
attempts = 0 attempts = 0
user_id = None user_id = None
token = None token = None
while not user_id and not token: while not user_id:
try: try:
localpart = self._generate_user_id() localpart = self._generate_user_id()
user = UserID(localpart, self.hs.hostname) user = UserID(localpart, self.hs.hostname)
user_id = user.to_string() user_id = user.to_string()
yield self.check_user_id_is_valid(user_id) yield self.check_user_id_is_valid(user_id)
if generate_token:
token = self.auth_handler().generate_access_token(user_id) token = self.auth_handler().generate_access_token(user_id)
yield self.store.register( yield self.store.register(
user_id=user_id, user_id=user_id,
token=token, token=token,
password_hash=password_hash) password_hash=password_hash)
self.distributor.fire("registered_user", user) yield registered_user(self.distributor, user)
except SynapseError: except SynapseError:
# if user id is taken, just generate another # if user id is taken, just generate another
user_id = None user_id = None
@@ -125,25 +132,9 @@ class RegistrationHandler(BaseHandler):
raise RegistrationError( raise RegistrationError(
500, "Cannot generate user ID.") 500, "Cannot generate user ID.")
# create a default avatar for the user # We used to generate default identicons here, but nowadays
# XXX: ideally clients would explicitly specify one, but given they don't # we want clients to generate their own as part of their branding
# and we want consistent and pretty identicons for random users, we'll # rather than there being consistent matrix-wide ones, so we don't.
# do it here.
try:
auth_user = UserID.from_string(user_id)
media_repository = self.hs.get_resource_for_media_repository()
identicon_resource = media_repository.getChildWithDefault("identicon", None)
upload_resource = media_repository.getChildWithDefault("upload", None)
identicon_bytes = identicon_resource.generate_identicon(user_id, 320, 320)
content_uri = yield upload_resource.create_content(
"image/png", None, identicon_bytes, len(identicon_bytes), auth_user
)
profile_handler = self.hs.get_handlers().profile_handler
profile_handler.set_avatar_url(
auth_user, auth_user, ("%s#auto" % (content_uri,))
)
except NotImplementedError:
pass # make tests pass without messing around creating default avatars
defer.returnValue((user_id, token)) defer.returnValue((user_id, token))
@@ -165,7 +156,7 @@ class RegistrationHandler(BaseHandler):
token=token, token=token,
password_hash="" password_hash=""
) )
self.distributor.fire("registered_user", user) registered_user(self.distributor, user)
defer.returnValue((user_id, token)) defer.returnValue((user_id, token))
@defer.inlineCallbacks @defer.inlineCallbacks
@@ -213,7 +204,7 @@ class RegistrationHandler(BaseHandler):
token=token, token=token,
password_hash=None password_hash=None
) )
yield self.distributor.fire("registered_user", user) yield registered_user(self.distributor, user)
except Exception, e: except Exception, e:
yield self.store.add_access_token_to_user(user_id, token) yield self.store.add_access_token_to_user(user_id, token)
# Ignore Registration errors # Ignore Registration errors
@@ -300,10 +291,7 @@ class RegistrationHandler(BaseHandler):
""" """
Used only by c/s api v1 Used only by c/s api v1
""" """
# TODO: get this from the homeserver rather than creating a new one for data = yield self.captcha_client.post_urlencoded_get_raw(
# each request
client = CaptchaServerHttpClient(self.hs)
data = yield client.post_urlencoded_get_raw(
"http://www.google.com:80/recaptcha/api/verify", "http://www.google.com:80/recaptcha/api/verify",
args={ args={
'privatekey': private_key, 'privatekey': private_key,

View File

@@ -22,26 +22,50 @@ from synapse.types import UserID, RoomAlias, RoomID
from synapse.api.constants import ( from synapse.api.constants import (
EventTypes, Membership, JoinRules, RoomCreationPreset, EventTypes, Membership, JoinRules, RoomCreationPreset,
) )
from synapse.api.errors import StoreError, SynapseError from synapse.api.errors import AuthError, StoreError, SynapseError
from synapse.util import stringutils, unwrapFirstError from synapse.util import stringutils, unwrapFirstError
from synapse.util.async import run_on_reactor from synapse.util.async import run_on_reactor
from synapse.events.utils import serialize_event
from signedjson.sign import verify_signed_json
from signedjson.key import decode_verify_key_bytes
from collections import OrderedDict from collections import OrderedDict
from unpaddedbase64 import decode_base64
import logging import logging
import math
import string import string
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
id_server_scheme = "https://"
def collect_presencelike_data(distributor, user, content):
return distributor.fire("collect_presencelike_data", user, content)
def user_left_room(distributor, user, room_id):
return distributor.fire("user_left_room", user=user, room_id=room_id)
def user_joined_room(distributor, user, room_id):
return distributor.fire("user_joined_room", user=user, room_id=room_id)
class RoomCreationHandler(BaseHandler): class RoomCreationHandler(BaseHandler):
PRESETS_DICT = { PRESETS_DICT = {
RoomCreationPreset.PRIVATE_CHAT: { RoomCreationPreset.PRIVATE_CHAT: {
"join_rules": JoinRules.INVITE, "join_rules": JoinRules.INVITE,
"history_visibility": "invited", "history_visibility": "shared",
"original_invitees_have_ops": False, "original_invitees_have_ops": False,
}, },
RoomCreationPreset.TRUSTED_PRIVATE_CHAT: {
"join_rules": JoinRules.INVITE,
"history_visibility": "shared",
"original_invitees_have_ops": True,
},
RoomCreationPreset.PUBLIC_CHAT: { RoomCreationPreset.PUBLIC_CHAT: {
"join_rules": JoinRules.PUBLIC, "join_rules": JoinRules.PUBLIC,
"history_visibility": "shared", "history_visibility": "shared",
@@ -150,12 +174,16 @@ class RoomCreationHandler(BaseHandler):
for val in raw_initial_state: for val in raw_initial_state:
initial_state[(val["type"], val.get("state_key", ""))] = val["content"] initial_state[(val["type"], val.get("state_key", ""))] = val["content"]
creation_content = config.get("creation_content", {})
user = UserID.from_string(user_id) user = UserID.from_string(user_id)
creation_events = self._create_events_for_new_room( creation_events = self._create_events_for_new_room(
user, room_id, user, room_id,
preset_config=preset_config, preset_config=preset_config,
invite_list=invite_list, invite_list=invite_list,
initial_state=initial_state, initial_state=initial_state,
creation_content=creation_content,
room_alias=room_alias,
) )
msg_handler = self.hs.get_handlers().message_handler msg_handler = self.hs.get_handlers().message_handler
@@ -203,7 +231,8 @@ class RoomCreationHandler(BaseHandler):
defer.returnValue(result) defer.returnValue(result)
def _create_events_for_new_room(self, creator, room_id, preset_config, def _create_events_for_new_room(self, creator, room_id, preset_config,
invite_list, initial_state): invite_list, initial_state, creation_content,
room_alias):
config = RoomCreationHandler.PRESETS_DICT[preset_config] config = RoomCreationHandler.PRESETS_DICT[preset_config]
creator_id = creator.to_string() creator_id = creator.to_string()
@@ -225,9 +254,10 @@ class RoomCreationHandler(BaseHandler):
return e return e
creation_content.update({"creator": creator.to_string()})
creation_event = create( creation_event = create(
etype=EventTypes.Create, etype=EventTypes.Create,
content={"creator": creator.to_string()}, content=creation_content,
) )
join_event = create( join_event = create(
@@ -272,6 +302,14 @@ class RoomCreationHandler(BaseHandler):
returned_events.append(power_levels_event) returned_events.append(power_levels_event)
if room_alias and (EventTypes.CanonicalAlias, '') not in initial_state:
room_alias_event = create(
etype=EventTypes.CanonicalAlias,
content={"alias": room_alias.to_string()},
)
returned_events.append(room_alias_event)
if (EventTypes.JoinRules, '') not in initial_state: if (EventTypes.JoinRules, '') not in initial_state:
join_rules_event = create( join_rules_event = create(
etype=EventTypes.JoinRules, etype=EventTypes.JoinRules,
@@ -343,42 +381,7 @@ class RoomMemberHandler(BaseHandler):
remotedomains.add(member.domain) remotedomains.add(member.domain)
@defer.inlineCallbacks @defer.inlineCallbacks
def get_room_members_as_pagination_chunk(self, room_id=None, user_id=None, def change_membership(self, event, context, do_auth=True, is_guest=False):
limit=0, start_tok=None,
end_tok=None):
"""Retrieve a list of room members in the room.
Args:
room_id (str): The room to get the member list for.
user_id (str): The ID of the user making the request.
limit (int): The max number of members to return.
start_tok (str): Optional. The start token if known.
end_tok (str): Optional. The end token if known.
Returns:
dict: A Pagination streamable dict.
Raises:
SynapseError if something goes wrong.
"""
yield self.auth.check_joined_room(room_id, user_id)
member_list = yield self.store.get_room_members(room_id=room_id)
time_now = self.clock.time_msec()
event_list = [
serialize_event(entry, time_now)
for entry in member_list
]
chunk_data = {
"start": "START", # FIXME (erikj): START is no longer valid
"end": "END",
"chunk": event_list
}
# TODO honor Pagination stream params
# TODO snapshot this list to return on subsequent requests when
# paginating
defer.returnValue(chunk_data)
@defer.inlineCallbacks
def change_membership(self, event, context, do_auth=True):
""" Change the membership status of a user in a room. """ Change the membership status of a user in a room.
Args: Args:
@@ -399,9 +402,38 @@ class RoomMemberHandler(BaseHandler):
# if this HS is not currently in the room, i.e. we have to do the # if this HS is not currently in the room, i.e. we have to do the
# invite/join dance. # invite/join dance.
if event.membership == Membership.JOIN: if event.membership == Membership.JOIN:
if is_guest:
guest_access = context.current_state.get(
(EventTypes.GuestAccess, ""),
None
)
is_guest_access_allowed = (
guest_access
and guest_access.content
and "guest_access" in guest_access.content
and guest_access.content["guest_access"] == "can_join"
)
if not is_guest_access_allowed:
raise AuthError(403, "Guest access not allowed")
yield self._do_join(event, context, do_auth=do_auth) yield self._do_join(event, context, do_auth=do_auth)
else: else:
# This is not a JOIN, so we can handle it normally. if event.membership == Membership.LEAVE:
is_host_in_room = yield self.is_host_in_room(room_id, context)
if not is_host_in_room:
# Rejecting an invite, rather than leaving a joined room
handler = self.hs.get_handlers().federation_handler
inviter = yield self.get_inviter(event)
if not inviter:
# return the same error as join_room_alias does
raise SynapseError(404, "No known servers")
yield handler.do_remotely_reject_invite(
[inviter.domain],
room_id,
event.user_id
)
defer.returnValue({"room_id": room_id})
return
# FIXME: This isn't idempotency. # FIXME: This isn't idempotency.
if prev_state and prev_state.membership == event.membership: if prev_state and prev_state.membership == event.membership:
@@ -418,14 +450,12 @@ class RoomMemberHandler(BaseHandler):
if prev_state and prev_state.membership == Membership.JOIN: if prev_state and prev_state.membership == Membership.JOIN:
user = UserID.from_string(event.user_id) user = UserID.from_string(event.user_id)
self.distributor.fire( user_left_room(self.distributor, user, event.room_id)
"user_left_room", user=user, room_id=event.room_id
)
defer.returnValue({"room_id": room_id}) defer.returnValue({"room_id": room_id})
@defer.inlineCallbacks @defer.inlineCallbacks
def join_room_alias(self, joinee, room_alias, do_auth=True, content={}): def join_room_alias(self, joinee, room_alias, content={}):
directory_handler = self.hs.get_handlers().directory_handler directory_handler = self.hs.get_handlers().directory_handler
mapping = yield directory_handler.get_association(room_alias) mapping = yield directory_handler.get_association(room_alias)
@@ -438,9 +468,7 @@ class RoomMemberHandler(BaseHandler):
raise SynapseError(404, "No known servers") raise SynapseError(404, "No known servers")
# If event doesn't include a display name, add one. # If event doesn't include a display name, add one.
yield self.distributor.fire( yield collect_presencelike_data(self.distributor, joinee, content)
"collect_presencelike_data", joinee, content
)
content.update({"membership": Membership.JOIN}) content.update({"membership": Membership.JOIN})
builder = self.event_builder_factory.new({ builder = self.event_builder_factory.new({
@@ -459,8 +487,6 @@ class RoomMemberHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def _do_join(self, event, context, room_hosts=None, do_auth=True): def _do_join(self, event, context, room_hosts=None, do_auth=True):
joinee = UserID.from_string(event.state_key)
# room_id = RoomID.from_string(event.room_id, self.hs)
room_id = event.room_id room_id = event.room_id
# XXX: We don't do an auth check if we are doing an invite # XXX: We don't do an auth check if we are doing an invite
@@ -468,8 +494,67 @@ class RoomMemberHandler(BaseHandler):
# that we are allowed to join when we decide whether or not we # that we are allowed to join when we decide whether or not we
# need to do the invite/join dance. # need to do the invite/join dance.
is_host_in_room = yield self.is_host_in_room(room_id, context)
if is_host_in_room:
should_do_dance = False
elif room_hosts: # TODO: Shouldn't this be remote_room_host?
should_do_dance = True
else:
inviter = yield self.get_inviter(event)
if not inviter:
# return the same error as join_room_alias does
raise SynapseError(404, "No known servers")
should_do_dance = not self.hs.is_mine(inviter)
room_hosts = [inviter.domain]
if should_do_dance:
handler = self.hs.get_handlers().federation_handler
yield handler.do_invite_join(
room_hosts,
room_id,
event.user_id,
event.content,
)
else:
logger.debug("Doing normal join")
yield self._do_local_membership_update(
event,
membership=event.content["membership"],
context=context,
do_auth=do_auth,
)
prev_state = context.current_state.get((event.type, event.state_key))
if not prev_state or prev_state.membership != Membership.JOIN:
# Only fire user_joined_room if the user has acutally joined the
# room. Don't bother if the user is just changing their profile
# info.
user = UserID.from_string(event.user_id)
yield user_joined_room(self.distributor, user, room_id)
@defer.inlineCallbacks
def get_inviter(self, event):
# TODO(markjh): get prev_state from snapshot
prev_state = yield self.store.get_room_member(
event.user_id, event.room_id
)
if prev_state and prev_state.membership == Membership.INVITE:
defer.returnValue(UserID.from_string(prev_state.user_id))
return
elif "third_party_invite" in event.content:
if "sender" in event.content["third_party_invite"]:
inviter = UserID.from_string(
event.content["third_party_invite"]["sender"]
)
defer.returnValue(inviter)
defer.returnValue(None)
@defer.inlineCallbacks
def is_host_in_room(self, room_id, context):
is_host_in_room = yield self.auth.check_host_in_room( is_host_in_room = yield self.auth.check_host_in_room(
event.room_id, room_id,
self.hs.hostname self.hs.hostname
) )
if not is_host_in_room: if not is_host_in_room:
@@ -484,75 +569,7 @@ class RoomMemberHandler(BaseHandler):
create_event = context.current_state.get(("m.room.create", "")) create_event = context.current_state.get(("m.room.create", ""))
if create_event: if create_event:
is_host_in_room = True is_host_in_room = True
defer.returnValue(is_host_in_room)
if is_host_in_room:
should_do_dance = False
elif room_hosts: # TODO: Shouldn't this be remote_room_host?
should_do_dance = True
else:
# TODO(markjh): get prev_state from snapshot
prev_state = yield self.store.get_room_member(
joinee.to_string(), room_id
)
if prev_state and prev_state.membership == Membership.INVITE:
inviter = UserID.from_string(prev_state.user_id)
should_do_dance = not self.hs.is_mine(inviter)
room_hosts = [inviter.domain]
else:
# return the same error as join_room_alias does
raise SynapseError(404, "No known servers")
if should_do_dance:
handler = self.hs.get_handlers().federation_handler
yield handler.do_invite_join(
room_hosts,
room_id,
event.user_id,
event.content, # FIXME To get a non-frozen dict
context
)
else:
logger.debug("Doing normal join")
yield self._do_local_membership_update(
event,
membership=event.content["membership"],
context=context,
do_auth=do_auth,
)
user = UserID.from_string(event.user_id)
yield self.distributor.fire(
"user_joined_room", user=user, room_id=room_id
)
@defer.inlineCallbacks
def _should_invite_join(self, room_id, prev_state, do_auth):
logger.debug("_should_invite_join: room_id: %s", room_id)
# XXX: We don't do an auth check if we are doing an invite
# join dance for now, since we're kinda implicitly checking
# that we are allowed to join when we decide whether or not we
# need to do the invite/join dance.
# Only do an invite join dance if a) we were invited,
# b) the person inviting was from a differnt HS and c) we are
# not currently in the room
room_host = None
if prev_state and prev_state.membership == Membership.INVITE:
room = yield self.store.get_room(room_id)
inviter = UserID.from_string(
prev_state.sender
)
is_remote_invite_join = not self.hs.is_mine(inviter) and not room
room_host = inviter.domain
else:
is_remote_invite_join = False
defer.returnValue((is_remote_invite_join, room_host))
@defer.inlineCallbacks @defer.inlineCallbacks
def get_joined_rooms_for_user(self, user): def get_joined_rooms_for_user(self, user):
@@ -583,13 +600,224 @@ class RoomMemberHandler(BaseHandler):
suppress_auth=(not do_auth), suppress_auth=(not do_auth),
) )
@defer.inlineCallbacks
def do_3pid_invite(
self,
room_id,
inviter,
medium,
address,
id_server,
token_id,
txn_id
):
invitee = yield self._lookup_3pid(
id_server, medium, address
)
if invitee:
# make sure it looks like a user ID; it'll throw if it's invalid.
UserID.from_string(invitee)
yield self.hs.get_handlers().message_handler.create_and_send_event(
{
"type": EventTypes.Member,
"content": {
"membership": unicode("invite")
},
"room_id": room_id,
"sender": inviter.to_string(),
"state_key": invitee,
},
token_id=token_id,
txn_id=txn_id,
)
else:
yield self._make_and_store_3pid_invite(
id_server,
medium,
address,
room_id,
inviter,
token_id,
txn_id=txn_id
)
@defer.inlineCallbacks
def _lookup_3pid(self, id_server, medium, address):
"""Looks up a 3pid in the passed identity server.
Args:
id_server (str): The server name (including port, if required)
of the identity server to use.
medium (str): The type of the third party identifier (e.g. "email").
address (str): The third party identifier (e.g. "foo@example.com").
Returns:
(str) the matrix ID of the 3pid, or None if it is not recognized.
"""
try:
data = yield self.hs.get_simple_http_client().get_json(
"%s%s/_matrix/identity/api/v1/lookup" % (id_server_scheme, id_server,),
{
"medium": medium,
"address": address,
}
)
if "mxid" in data:
if "signatures" not in data:
raise AuthError(401, "No signatures on 3pid binding")
self.verify_any_signature(data, id_server)
defer.returnValue(data["mxid"])
except IOError as e:
logger.warn("Error from identity server lookup: %s" % (e,))
defer.returnValue(None)
@defer.inlineCallbacks
def verify_any_signature(self, data, server_hostname):
if server_hostname not in data["signatures"]:
raise AuthError(401, "No signature from server %s" % (server_hostname,))
for key_name, signature in data["signatures"][server_hostname].items():
key_data = yield self.hs.get_simple_http_client().get_json(
"%s%s/_matrix/identity/api/v1/pubkey/%s" %
(id_server_scheme, server_hostname, key_name,),
)
if "public_key" not in key_data:
raise AuthError(401, "No public key named %s from %s" %
(key_name, server_hostname,))
verify_signed_json(
data,
server_hostname,
decode_verify_key_bytes(key_name, decode_base64(key_data["public_key"]))
)
return
@defer.inlineCallbacks
def _make_and_store_3pid_invite(
self,
id_server,
medium,
address,
room_id,
user,
token_id,
txn_id
):
room_state = yield self.hs.get_state_handler().get_current_state(room_id)
inviter_display_name = ""
inviter_avatar_url = ""
member_event = room_state.get((EventTypes.Member, user.to_string()))
if member_event:
inviter_display_name = member_event.content.get("displayname", "")
inviter_avatar_url = member_event.content.get("avatar_url", "")
canonical_room_alias = ""
canonical_alias_event = room_state.get((EventTypes.CanonicalAlias, ""))
if canonical_alias_event:
canonical_room_alias = canonical_alias_event.content.get("alias", "")
room_name = ""
room_name_event = room_state.get((EventTypes.Name, ""))
if room_name_event:
room_name = room_name_event.content.get("name", "")
room_join_rules = ""
join_rules_event = room_state.get((EventTypes.JoinRules, ""))
if join_rules_event:
room_join_rules = join_rules_event.content.get("join_rule", "")
room_avatar_url = ""
room_avatar_event = room_state.get((EventTypes.RoomAvatar, ""))
if room_avatar_event:
room_avatar_url = room_avatar_event.content.get("url", "")
token, public_key, key_validity_url, display_name = (
yield self._ask_id_server_for_third_party_invite(
id_server=id_server,
medium=medium,
address=address,
room_id=room_id,
inviter_user_id=user.to_string(),
room_alias=canonical_room_alias,
room_avatar_url=room_avatar_url,
room_join_rules=room_join_rules,
room_name=room_name,
inviter_display_name=inviter_display_name,
inviter_avatar_url=inviter_avatar_url
)
)
msg_handler = self.hs.get_handlers().message_handler
yield msg_handler.create_and_send_event(
{
"type": EventTypes.ThirdPartyInvite,
"content": {
"display_name": display_name,
"key_validity_url": key_validity_url,
"public_key": public_key,
},
"room_id": room_id,
"sender": user.to_string(),
"state_key": token,
},
token_id=token_id,
txn_id=txn_id,
)
@defer.inlineCallbacks
def _ask_id_server_for_third_party_invite(
self,
id_server,
medium,
address,
room_id,
inviter_user_id,
room_alias,
room_avatar_url,
room_join_rules,
room_name,
inviter_display_name,
inviter_avatar_url
):
is_url = "%s%s/_matrix/identity/api/v1/store-invite" % (
id_server_scheme, id_server,
)
data = yield self.hs.get_simple_http_client().post_urlencoded_get_json(
is_url,
{
"medium": medium,
"address": address,
"room_id": room_id,
"room_alias": room_alias,
"room_avatar_url": room_avatar_url,
"room_join_rules": room_join_rules,
"room_name": room_name,
"sender": inviter_user_id,
"sender_display_name": inviter_display_name,
"sender_avatar_url": inviter_avatar_url,
}
)
# TODO: Check for success
token = data["token"]
public_key = data["public_key"]
display_name = data["display_name"]
key_validity_url = "%s%s/_matrix/identity/api/v1/pubkey/isvalid" % (
id_server_scheme, id_server,
)
defer.returnValue((token, public_key, key_validity_url, display_name))
def forget(self, user, room_id):
return self.store.forget(user.to_string(), room_id)
class RoomListHandler(BaseHandler): class RoomListHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def get_public_room_list(self): def get_public_room_list(self):
chunk = yield self.store.get_rooms(is_public=True) chunk = yield self.store.get_rooms(is_public=True)
results = yield defer.gatherResults(
room_members = yield defer.gatherResults(
[ [
self.store.get_users_in_room(room["room_id"]) self.store.get_users_in_room(room["room_id"])
for room in chunk for room in chunk
@@ -597,19 +825,104 @@ class RoomListHandler(BaseHandler):
consumeErrors=True, consumeErrors=True,
).addErrback(unwrapFirstError) ).addErrback(unwrapFirstError)
avatar_urls = yield defer.gatherResults(
[
self.get_room_avatar_url(room["room_id"])
for room in chunk
],
consumeErrors=True,
).addErrback(unwrapFirstError)
for i, room in enumerate(chunk): for i, room in enumerate(chunk):
room["num_joined_members"] = len(results[i]) room["num_joined_members"] = len(room_members[i])
if avatar_urls[i]:
room["avatar_url"] = avatar_urls[i]
# FIXME (erikj): START is no longer a valid value # FIXME (erikj): START is no longer a valid value
defer.returnValue({"start": "START", "end": "END", "chunk": chunk}) defer.returnValue({"start": "START", "end": "END", "chunk": chunk})
@defer.inlineCallbacks
def get_room_avatar_url(self, room_id):
event = yield self.hs.get_state_handler().get_current_state(
room_id, "m.room.avatar"
)
if event and "url" in event.content:
defer.returnValue(event.content["url"])
class RoomContextHandler(BaseHandler):
@defer.inlineCallbacks
def get_event_context(self, user, room_id, event_id, limit, is_guest):
"""Retrieves events, pagination tokens and state around a given event
in a room.
Args:
user (UserID)
room_id (str)
event_id (str)
limit (int): The maximum number of events to return in total
(excluding state).
Returns:
dict
"""
before_limit = math.floor(limit/2.)
after_limit = limit - before_limit
now_token = yield self.hs.get_event_sources().get_current_token()
results = yield self.store.get_events_around(
room_id, event_id, before_limit, after_limit
)
results["events_before"] = yield self._filter_events_for_client(
user.to_string(),
results["events_before"],
is_guest=is_guest,
require_all_visible_for_guests=False
)
results["events_after"] = yield self._filter_events_for_client(
user.to_string(),
results["events_after"],
is_guest=is_guest,
require_all_visible_for_guests=False
)
if results["events_after"]:
last_event_id = results["events_after"][-1].event_id
else:
last_event_id = event_id
state = yield self.store.get_state_for_events(
[last_event_id], None
)
results["state"] = state[last_event_id].values()
results["start"] = now_token.copy_and_replace(
"room_key", results["start"]
).to_string()
results["end"] = now_token.copy_and_replace(
"room_key", results["end"]
).to_string()
defer.returnValue(results)
class RoomEventSource(object): class RoomEventSource(object):
def __init__(self, hs): def __init__(self, hs):
self.store = hs.get_datastore() self.store = hs.get_datastore()
@defer.inlineCallbacks @defer.inlineCallbacks
def get_new_events_for_user(self, user, from_key, limit): def get_new_events(
self,
user,
from_key,
limit,
room_ids,
is_guest,
):
# We just ignore the key for now. # We just ignore the key for now.
to_key = yield self.get_current_key() to_key = yield self.get_current_key()
@@ -629,8 +942,9 @@ class RoomEventSource(object):
user_id=user.to_string(), user_id=user.to_string(),
from_key=from_key, from_key=from_key,
to_key=to_key, to_key=to_key,
room_id=None,
limit=limit, limit=limit,
room_ids=room_ids,
is_guest=is_guest,
) )
defer.returnValue((events, end_key)) defer.returnValue((events, end_key))
@@ -646,7 +960,6 @@ class RoomEventSource(object):
to_key=config.to_key, to_key=config.to_key,
direction=config.direction, direction=config.direction,
limit=config.limit, limit=config.limit,
with_feedback=True
) )
defer.returnValue((events, next_key)) defer.returnValue((events, next_key))

391
synapse/handlers/search.py Normal file
View File

@@ -0,0 +1,391 @@
# -*- coding: utf-8 -*-
# Copyright 2015 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 twisted.internet import defer
from ._base import BaseHandler
from synapse.api.constants import Membership, EventTypes
from synapse.api.filtering import Filter
from synapse.api.errors import SynapseError
from synapse.events.utils import serialize_event
from unpaddedbase64 import decode_base64, encode_base64
import itertools
import logging
logger = logging.getLogger(__name__)
class SearchHandler(BaseHandler):
def __init__(self, hs):
super(SearchHandler, self).__init__(hs)
@defer.inlineCallbacks
def search(self, user, content, batch=None):
"""Performs a full text search for a user.
Args:
user (UserID)
content (dict): Search parameters
batch (str): The next_batch parameter. Used for pagination.
Returns:
dict to be returned to the client with results of search
"""
batch_group = None
batch_group_key = None
batch_token = None
if batch:
try:
b = decode_base64(batch)
batch_group, batch_group_key, batch_token = b.split("\n")
assert batch_group is not None
assert batch_group_key is not None
assert batch_token is not None
except:
raise SynapseError(400, "Invalid batch")
try:
room_cat = content["search_categories"]["room_events"]
# The actual thing to query in FTS
search_term = room_cat["search_term"]
# Which "keys" to search over in FTS query
keys = room_cat.get("keys", [
"content.body", "content.name", "content.topic",
])
# Filter to apply to results
filter_dict = room_cat.get("filter", {})
# What to order results by (impacts whether pagination can be doen)
order_by = room_cat.get("order_by", "rank")
# Return the current state of the rooms?
include_state = room_cat.get("include_state", False)
# Include context around each event?
event_context = room_cat.get(
"event_context", None
)
# Group results together? May allow clients to paginate within a
# group
group_by = room_cat.get("groupings", {}).get("group_by", {})
group_keys = [g["key"] for g in group_by]
if event_context is not None:
before_limit = int(event_context.get(
"before_limit", 5
))
after_limit = int(event_context.get(
"after_limit", 5
))
# Return the historic display name and avatar for the senders
# of the events?
include_profile = bool(event_context.get("include_profile", False))
except KeyError:
raise SynapseError(400, "Invalid search query")
if order_by not in ("rank", "recent"):
raise SynapseError(400, "Invalid order by: %r" % (order_by,))
if set(group_keys) - {"room_id", "sender"}:
raise SynapseError(
400,
"Invalid group by keys: %r" % (set(group_keys) - {"room_id", "sender"},)
)
search_filter = Filter(filter_dict)
# TODO: Search through left rooms too
rooms = yield self.store.get_rooms_for_user_where_membership_is(
user.to_string(),
membership_list=[Membership.JOIN],
# membership_list=[Membership.JOIN, Membership.LEAVE, Membership.Ban],
)
room_ids = set(r.room_id for r in rooms)
room_ids = search_filter.filter_rooms(room_ids)
if batch_group == "room_id":
room_ids.intersection_update({batch_group_key})
if not room_ids:
defer.returnValue({
"search_categories": {
"room_events": {
"results": [],
"count": 0,
"highlights": [],
}
}
})
rank_map = {} # event_id -> rank of event
allowed_events = []
room_groups = {} # Holds result of grouping by room, if applicable
sender_group = {} # Holds result of grouping by sender, if applicable
# Holds the next_batch for the entire result set if one of those exists
global_next_batch = None
highlights = set()
count = None
if order_by == "rank":
search_result = yield self.store.search_msgs(
room_ids, search_term, keys
)
count = search_result["count"]
if search_result["highlights"]:
highlights.update(search_result["highlights"])
results = search_result["results"]
results_map = {r["event"].event_id: r for r in results}
rank_map.update({r["event"].event_id: r["rank"] for r in results})
filtered_events = search_filter.filter([r["event"] for r in results])
events = yield self._filter_events_for_client(
user.to_string(), filtered_events
)
events.sort(key=lambda e: -rank_map[e.event_id])
allowed_events = events[:search_filter.limit()]
for e in allowed_events:
rm = room_groups.setdefault(e.room_id, {
"results": [],
"order": rank_map[e.event_id],
})
rm["results"].append(e.event_id)
s = sender_group.setdefault(e.sender, {
"results": [],
"order": rank_map[e.event_id],
})
s["results"].append(e.event_id)
elif order_by == "recent":
room_events = []
i = 0
pagination_token = batch_token
# We keep looping and we keep filtering until we reach the limit
# or we run out of things.
# But only go around 5 times since otherwise synapse will be sad.
while len(room_events) < search_filter.limit() and i < 5:
i += 1
search_result = yield self.store.search_rooms(
room_ids, search_term, keys, search_filter.limit() * 2,
pagination_token=pagination_token,
)
if search_result["highlights"]:
highlights.update(search_result["highlights"])
count = search_result["count"]
results = search_result["results"]
results_map = {r["event"].event_id: r for r in results}
rank_map.update({r["event"].event_id: r["rank"] for r in results})
filtered_events = search_filter.filter([
r["event"] for r in results
])
events = yield self._filter_events_for_client(
user.to_string(), filtered_events
)
room_events.extend(events)
room_events = room_events[:search_filter.limit()]
if len(results) < search_filter.limit() * 2:
pagination_token = None
break
else:
pagination_token = results[-1]["pagination_token"]
for event in room_events:
group = room_groups.setdefault(event.room_id, {
"results": [],
})
group["results"].append(event.event_id)
if room_events and len(room_events) >= search_filter.limit():
last_event_id = room_events[-1].event_id
pagination_token = results_map[last_event_id]["pagination_token"]
# We want to respect the given batch group and group keys so
# that if people blindly use the top level `next_batch` token
# it returns more from the same group (if applicable) rather
# than reverting to searching all results again.
if batch_group and batch_group_key:
global_next_batch = encode_base64("%s\n%s\n%s" % (
batch_group, batch_group_key, pagination_token
))
else:
global_next_batch = encode_base64("%s\n%s\n%s" % (
"all", "", pagination_token
))
for room_id, group in room_groups.items():
group["next_batch"] = encode_base64("%s\n%s\n%s" % (
"room_id", room_id, pagination_token
))
allowed_events.extend(room_events)
else:
# We should never get here due to the guard earlier.
raise NotImplementedError()
# If client has asked for "context" for each event (i.e. some surrounding
# events and state), fetch that
if event_context is not None:
now_token = yield self.hs.get_event_sources().get_current_token()
contexts = {}
for event in allowed_events:
res = yield self.store.get_events_around(
event.room_id, event.event_id, before_limit, after_limit
)
res["events_before"] = yield self._filter_events_for_client(
user.to_string(), res["events_before"]
)
res["events_after"] = yield self._filter_events_for_client(
user.to_string(), res["events_after"]
)
res["start"] = now_token.copy_and_replace(
"room_key", res["start"]
).to_string()
res["end"] = now_token.copy_and_replace(
"room_key", res["end"]
).to_string()
if include_profile:
senders = set(
ev.sender
for ev in itertools.chain(
res["events_before"], [event], res["events_after"]
)
)
if res["events_after"]:
last_event_id = res["events_after"][-1].event_id
else:
last_event_id = event.event_id
state = yield self.store.get_state_for_event(
last_event_id,
types=[(EventTypes.Member, sender) for sender in senders]
)
res["profile_info"] = {
s.state_key: {
"displayname": s.content.get("displayname", None),
"avatar_url": s.content.get("avatar_url", None),
}
for s in state.values()
if s.type == EventTypes.Member and s.state_key in senders
}
contexts[event.event_id] = res
else:
contexts = {}
# TODO: Add a limit
time_now = self.clock.time_msec()
for context in contexts.values():
context["events_before"] = [
serialize_event(e, time_now)
for e in context["events_before"]
]
context["events_after"] = [
serialize_event(e, time_now)
for e in context["events_after"]
]
state_results = {}
if include_state:
rooms = set(e.room_id for e in allowed_events)
for room_id in rooms:
state = yield self.state_handler.get_current_state(room_id)
state_results[room_id] = state.values()
state_results.values()
# We're now about to serialize the events. We should not make any
# blocking calls after this. Otherwise the 'age' will be wrong
results = [
{
"rank": rank_map[e.event_id],
"result": serialize_event(e, time_now),
"context": contexts.get(e.event_id, {}),
}
for e in allowed_events
]
rooms_cat_res = {
"results": results,
"count": count,
"highlights": list(highlights),
}
if state_results:
rooms_cat_res["state"] = {
room_id: [serialize_event(e, time_now) for e in state]
for room_id, state in state_results.items()
}
if room_groups and "room_id" in group_keys:
rooms_cat_res.setdefault("groups", {})["room_id"] = room_groups
if sender_group and "sender" in group_keys:
rooms_cat_res.setdefault("groups", {})["sender"] = sender_group
if global_next_batch:
rooms_cat_res["next_batch"] = global_next_batch
defer.returnValue({
"search_categories": {
"room_events": rooms_cat_res
}
})

File diff suppressed because it is too large Load Diff

View File

@@ -246,17 +246,12 @@ class TypingNotificationEventSource(object):
}, },
} }
@defer.inlineCallbacks def get_new_events(self, from_key, room_ids, **kwargs):
def get_new_events_for_user(self, user, from_key, limit):
from_key = int(from_key) from_key = int(from_key)
handler = self.handler() handler = self.handler()
joined_room_ids = (
yield self.room_member_handler().get_joined_rooms_for_user(user)
)
events = [] events = []
for room_id in joined_room_ids: for room_id in room_ids:
if room_id not in handler._room_serials: if room_id not in handler._room_serials:
continue continue
if handler._room_serials[room_id] <= from_key: if handler._room_serials[room_id] <= from_key:
@@ -264,7 +259,7 @@ class TypingNotificationEventSource(object):
events.append(self._make_event_for(room_id)) events.append(self._make_event_for(room_id))
defer.returnValue((events, handler._latest_room_serial)) return events, handler._latest_room_serial
def get_current_key(self): def get_current_key(self):
return self.handler()._latest_room_serial return self.handler()._latest_room_serial

View File

@@ -12,6 +12,8 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# 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 OpenSSL import SSL
from OpenSSL.SSL import VERIFY_NONE
from synapse.api.errors import CodeMessageException from synapse.api.errors import CodeMessageException
from synapse.util.logcontext import preserve_context_over_fn from synapse.util.logcontext import preserve_context_over_fn
@@ -19,10 +21,9 @@ import synapse.metrics
from canonicaljson import encode_canonical_json from canonicaljson import encode_canonical_json
from twisted.internet import defer, reactor from twisted.internet import defer, reactor, ssl
from twisted.web.client import ( from twisted.web.client import (
Agent, readBody, FileBodyProducer, PartialDownloadError, Agent, readBody, FileBodyProducer, PartialDownloadError,
HTTPConnectionPool,
) )
from twisted.web.http_headers import Headers from twisted.web.http_headers import Headers
@@ -57,10 +58,14 @@ class SimpleHttpClient(object):
# The default context factory in Twisted 14.0.0 (which we require) is # The default context factory in Twisted 14.0.0 (which we require) is
# BrowserLikePolicyForHTTPS which will do regular cert validation # BrowserLikePolicyForHTTPS which will do regular cert validation
# 'like a browser' # 'like a browser'
pool = HTTPConnectionPool(reactor) self.agent = Agent(
pool.maxPersistentPerHost = 10 reactor,
self.agent = Agent(reactor, pool=pool) connectTimeout=15,
self.version_string = hs.version_string contextFactory=hs.get_http_client_context_factory()
)
self.user_agent = hs.version_string
if hs.config.user_agent_suffix:
self.user_agent = "%s %s" % (self.user_agent, hs.config.user_agent_suffix,)
def request(self, method, uri, *args, **kwargs): def request(self, method, uri, *args, **kwargs):
# A small wrapper around self.agent.request() so we can easily attach # A small wrapper around self.agent.request() so we can easily attach
@@ -105,7 +110,7 @@ class SimpleHttpClient(object):
uri.encode("ascii"), uri.encode("ascii"),
headers=Headers({ headers=Headers({
b"Content-Type": [b"application/x-www-form-urlencoded"], b"Content-Type": [b"application/x-www-form-urlencoded"],
b"User-Agent": [self.version_string], b"User-Agent": [self.user_agent],
}), }),
bodyProducer=FileBodyProducer(StringIO(query_bytes)) bodyProducer=FileBodyProducer(StringIO(query_bytes))
) )
@@ -124,7 +129,8 @@ class SimpleHttpClient(object):
"POST", "POST",
uri.encode("ascii"), uri.encode("ascii"),
headers=Headers({ headers=Headers({
"Content-Type": ["application/json"] b"Content-Type": [b"application/json"],
b"User-Agent": [self.user_agent],
}), }),
bodyProducer=FileBodyProducer(StringIO(json_str)) bodyProducer=FileBodyProducer(StringIO(json_str))
) )
@@ -150,27 +156,8 @@ class SimpleHttpClient(object):
On a non-2xx HTTP response. The response body will be used as the On a non-2xx HTTP response. The response body will be used as the
error message. error message.
""" """
if len(args): body = yield self.get_raw(uri, args)
query_bytes = urllib.urlencode(args, True) defer.returnValue(json.loads(body))
uri = "%s?%s" % (uri, query_bytes)
response = yield self.request(
"GET",
uri.encode("ascii"),
headers=Headers({
b"User-Agent": [self.version_string],
})
)
body = yield preserve_context_over_fn(readBody, response)
if 200 <= response.code < 300:
defer.returnValue(json.loads(body))
else:
# NB: This is explicitly not json.loads(body)'d because the contract
# of CodeMessageException is a *string* message. Callers can always
# load it into JSON if they want.
raise CodeMessageException(response.code, body)
@defer.inlineCallbacks @defer.inlineCallbacks
def put_json(self, uri, json_body, args={}): def put_json(self, uri, json_body, args={}):
@@ -199,7 +186,7 @@ class SimpleHttpClient(object):
"PUT", "PUT",
uri.encode("ascii"), uri.encode("ascii"),
headers=Headers({ headers=Headers({
b"User-Agent": [self.version_string], b"User-Agent": [self.user_agent],
"Content-Type": ["application/json"] "Content-Type": ["application/json"]
}), }),
bodyProducer=FileBodyProducer(StringIO(json_str)) bodyProducer=FileBodyProducer(StringIO(json_str))
@@ -215,6 +202,42 @@ class SimpleHttpClient(object):
# load it into JSON if they want. # load it into JSON if they want.
raise CodeMessageException(response.code, body) raise CodeMessageException(response.code, body)
@defer.inlineCallbacks
def get_raw(self, uri, args={}):
""" Gets raw text from the given URI.
Args:
uri (str): The URI to request, not including query parameters
args (dict): A dictionary used to create query strings, defaults to
None.
**Note**: The value of each key is assumed to be an iterable
and *not* a string.
Returns:
Deferred: Succeeds when we get *any* 2xx HTTP response, with the
HTTP body at text.
Raises:
On a non-2xx HTTP response. The response body will be used as the
error message.
"""
if len(args):
query_bytes = urllib.urlencode(args, True)
uri = "%s?%s" % (uri, query_bytes)
response = yield self.request(
"GET",
uri.encode("ascii"),
headers=Headers({
b"User-Agent": [self.user_agent],
})
)
body = yield preserve_context_over_fn(readBody, response)
if 200 <= response.code < 300:
defer.returnValue(body)
else:
raise CodeMessageException(response.code, body)
class CaptchaServerHttpClient(SimpleHttpClient): class CaptchaServerHttpClient(SimpleHttpClient):
""" """
@@ -234,7 +257,7 @@ class CaptchaServerHttpClient(SimpleHttpClient):
bodyProducer=FileBodyProducer(StringIO(query_bytes)), bodyProducer=FileBodyProducer(StringIO(query_bytes)),
headers=Headers({ headers=Headers({
b"Content-Type": [b"application/x-www-form-urlencoded"], b"Content-Type": [b"application/x-www-form-urlencoded"],
b"User-Agent": [self.version_string], b"User-Agent": [self.user_agent],
}) })
) )
@@ -252,3 +275,18 @@ def _print_ex(e):
_print_ex(ex) _print_ex(ex)
else: else:
logger.exception(e) logger.exception(e)
class InsecureInterceptableContextFactory(ssl.ContextFactory):
"""
Factory for PyOpenSSL SSL contexts which accepts any certificate for any domain.
Do not use this since it allows an attacker to intercept your communications.
"""
def __init__(self):
self._context = SSL.Context(SSL.SSLv23_METHOD)
self._context.set_verify(VERIFY_NONE, lambda *_: None)
def getContext(self, hostname, port):
return self._context

View File

@@ -35,6 +35,7 @@ from signedjson.sign import sign_json
import simplejson as json import simplejson as json
import logging import logging
import random
import sys import sys
import urllib import urllib
import urlparse import urlparse
@@ -55,16 +56,20 @@ incoming_responses_counter = metrics.register_counter(
) )
MAX_LONG_RETRIES = 10
MAX_SHORT_RETRIES = 3
class MatrixFederationEndpointFactory(object): class MatrixFederationEndpointFactory(object):
def __init__(self, hs): def __init__(self, hs):
self.tls_context_factory = hs.tls_context_factory self.tls_server_context_factory = hs.tls_server_context_factory
def endpointForURI(self, uri): def endpointForURI(self, uri):
destination = uri.netloc destination = uri.netloc
return matrix_federation_endpoint( return matrix_federation_endpoint(
reactor, destination, timeout=10, reactor, destination, timeout=10,
ssl_context_factory=self.tls_context_factory ssl_context_factory=self.tls_server_context_factory
) )
@@ -99,7 +104,7 @@ class MatrixFederationHttpClient(object):
def _create_request(self, destination, method, path_bytes, def _create_request(self, destination, method, path_bytes,
body_callback, headers_dict={}, param_bytes=b"", body_callback, headers_dict={}, param_bytes=b"",
query_bytes=b"", retry_on_dns_fail=True, query_bytes=b"", retry_on_dns_fail=True,
timeout=None): timeout=None, long_retries=False):
""" Creates and sends a request to the given url """ Creates and sends a request to the given url
""" """
headers_dict[b"User-Agent"] = [self.version_string] headers_dict[b"User-Agent"] = [self.version_string]
@@ -119,7 +124,10 @@ class MatrixFederationHttpClient(object):
# XXX: Would be much nicer to retry only at the transaction-layer # XXX: Would be much nicer to retry only at the transaction-layer
# (once we have reliable transactions in place) # (once we have reliable transactions in place)
retries_left = 5 if long_retries:
retries_left = MAX_LONG_RETRIES
else:
retries_left = MAX_SHORT_RETRIES
http_url_bytes = urlparse.urlunparse( http_url_bytes = urlparse.urlunparse(
("", "", path_bytes, param_bytes, query_bytes, "") ("", "", path_bytes, param_bytes, query_bytes, "")
@@ -180,7 +188,16 @@ class MatrixFederationHttpClient(object):
) )
if retries_left and not timeout: if retries_left and not timeout:
yield sleep(2 ** (5 - retries_left)) if long_retries:
delay = 4 ** (MAX_LONG_RETRIES + 1 - retries_left)
delay = min(delay, 60)
delay *= random.uniform(0.8, 1.4)
else:
delay = 0.5 * 2 ** (MAX_SHORT_RETRIES - retries_left)
delay = min(delay, 2)
delay *= random.uniform(0.8, 1.4)
yield sleep(delay)
retries_left -= 1 retries_left -= 1
else: else:
raise raise
@@ -230,7 +247,8 @@ class MatrixFederationHttpClient(object):
headers_dict[b"Authorization"] = auth_headers headers_dict[b"Authorization"] = auth_headers
@defer.inlineCallbacks @defer.inlineCallbacks
def put_json(self, destination, path, data={}, json_data_callback=None): def put_json(self, destination, path, data={}, json_data_callback=None,
long_retries=False):
""" Sends the specifed json data using PUT """ Sends the specifed json data using PUT
Args: Args:
@@ -241,6 +259,8 @@ class MatrixFederationHttpClient(object):
the request body. This will be encoded as JSON. the request body. This will be encoded as JSON.
json_data_callback (callable): A callable returning the dict to json_data_callback (callable): A callable returning the dict to
use as the request body. use as the request body.
long_retries (bool): A boolean that indicates whether we should
retry for a short or long time.
Returns: Returns:
Deferred: Succeeds when we get a 2xx HTTP response. The result Deferred: Succeeds when we get a 2xx HTTP response. The result
@@ -266,6 +286,7 @@ class MatrixFederationHttpClient(object):
path.encode("ascii"), path.encode("ascii"),
body_callback=body_callback, body_callback=body_callback,
headers_dict={"Content-Type": ["application/json"]}, headers_dict={"Content-Type": ["application/json"]},
long_retries=long_retries,
) )
if 200 <= response.code < 300: if 200 <= response.code < 300:
@@ -281,7 +302,7 @@ class MatrixFederationHttpClient(object):
defer.returnValue(json.loads(body)) defer.returnValue(json.loads(body))
@defer.inlineCallbacks @defer.inlineCallbacks
def post_json(self, destination, path, data={}): def post_json(self, destination, path, data={}, long_retries=True):
""" Sends the specifed json data using POST """ Sends the specifed json data using POST
Args: Args:
@@ -290,6 +311,8 @@ class MatrixFederationHttpClient(object):
path (str): The HTTP path. path (str): The HTTP path.
data (dict): A dict containing the data that will be used as data (dict): A dict containing the data that will be used as
the request body. This will be encoded as JSON. the request body. This will be encoded as JSON.
long_retries (bool): A boolean that indicates whether we should
retry for a short or long time.
Returns: Returns:
Deferred: Succeeds when we get a 2xx HTTP response. The result Deferred: Succeeds when we get a 2xx HTTP response. The result
@@ -309,6 +332,7 @@ class MatrixFederationHttpClient(object):
path.encode("ascii"), path.encode("ascii"),
body_callback=body_callback, body_callback=body_callback,
headers_dict={"Content-Type": ["application/json"]}, headers_dict={"Content-Type": ["application/json"]},
long_retries=True,
) )
if 200 <= response.code < 300: if 200 <= response.code < 300:
@@ -484,6 +508,9 @@ class _JsonProducer(object):
def stopProducing(self): def stopProducing(self):
pass pass
def resumeProducing(self):
pass
def _flatten_response_never_received(e): def _flatten_response_never_received(e):
if hasattr(e, "reasons"): if hasattr(e, "reasons"):

View File

@@ -15,7 +15,7 @@
from synapse.api.errors import ( from synapse.api.errors import (
cs_exception, SynapseError, CodeMessageException, UnrecognizedRequestError cs_exception, SynapseError, CodeMessageException, UnrecognizedRequestError, Codes
) )
from synapse.util.logcontext import LoggingContext, PreserveLoggingContext from synapse.util.logcontext import LoggingContext, PreserveLoggingContext
import synapse.metrics import synapse.metrics
@@ -53,6 +53,23 @@ response_timer = metrics.register_distribution(
labels=["method", "servlet"] labels=["method", "servlet"]
) )
response_ru_utime = metrics.register_distribution(
"response_ru_utime", labels=["method", "servlet"]
)
response_ru_stime = metrics.register_distribution(
"response_ru_stime", labels=["method", "servlet"]
)
response_db_txn_count = metrics.register_distribution(
"response_db_txn_count", labels=["method", "servlet"]
)
response_db_txn_duration = metrics.register_distribution(
"response_db_txn_duration", labels=["method", "servlet"]
)
_next_request_id = 0 _next_request_id = 0
@@ -110,7 +127,10 @@ def request_handler(request_handler):
respond_with_json( respond_with_json(
request, request,
500, 500,
{"error": "Internal server error"}, {
"error": "Internal server error",
"errcode": Codes.UNKNOWN,
},
send_cors=True send_cors=True
) )
return wrapped_request_handler return wrapped_request_handler
@@ -120,7 +140,7 @@ class HttpServer(object):
""" Interface for registering callbacks on a HTTP server """ Interface for registering callbacks on a HTTP server
""" """
def register_path(self, method, path_pattern, callback): def register_paths(self, method, path_patterns, callback):
""" Register a callback that gets fired if we receive a http request """ Register a callback that gets fired if we receive a http request
with the given method for a path that matches the given regex. with the given method for a path that matches the given regex.
@@ -129,7 +149,7 @@ class HttpServer(object):
Args: Args:
method (str): The method to listen to. method (str): The method to listen to.
path_pattern (str): The regex used to match requests. path_patterns (list<SRE_Pattern>): The regex used to match requests.
callback (function): The function to fire if we receive a matched callback (function): The function to fire if we receive a matched
request. The first argument will be the request object and request. The first argument will be the request object and
subsequent arguments will be any matched groups from the regex. subsequent arguments will be any matched groups from the regex.
@@ -165,10 +185,11 @@ class JsonResource(HttpServer, resource.Resource):
self.version_string = hs.version_string self.version_string = hs.version_string
self.hs = hs self.hs = hs
def register_path(self, method, path_pattern, callback): def register_paths(self, method, path_patterns, callback):
self.path_regexs.setdefault(method, []).append( for path_pattern in path_patterns:
self._PathEntry(path_pattern, callback) self.path_regexs.setdefault(method, []).append(
) self._PathEntry(path_pattern, callback)
)
def render(self, request): def render(self, request):
""" This gets called by twisted every time someone sends us a request. """ This gets called by twisted every time someone sends us a request.
@@ -220,6 +241,21 @@ class JsonResource(HttpServer, resource.Resource):
self.clock.time_msec() - start, request.method, servlet_classname self.clock.time_msec() - start, request.method, servlet_classname
) )
try:
context = LoggingContext.current_context()
ru_utime, ru_stime = context.get_resource_usage()
response_ru_utime.inc_by(ru_utime, request.method, servlet_classname)
response_ru_stime.inc_by(ru_stime, request.method, servlet_classname)
response_db_txn_count.inc_by(
context.db_txn_count, request.method, servlet_classname
)
response_db_txn_duration.inc_by(
context.db_txn_duration, request.method, servlet_classname
)
except:
pass
return return
# Huh. No one wanted to handle that? Fiiiiiine. Send 400. # Huh. No one wanted to handle that? Fiiiiiine. Send 400.

View File

@@ -19,7 +19,6 @@ from synapse.api.errors import SynapseError
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -102,12 +101,13 @@ class RestServlet(object):
def register(self, http_server): def register(self, http_server):
""" Register this servlet with the given HTTP server. """ """ Register this servlet with the given HTTP server. """
if hasattr(self, "PATTERN"): if hasattr(self, "PATTERNS"):
pattern = self.PATTERN patterns = self.PATTERNS
for method in ("GET", "PUT", "POST", "OPTIONS", "DELETE"): for method in ("GET", "PUT", "POST", "OPTIONS", "DELETE"):
if hasattr(self, "on_%s" % (method,)): if hasattr(self, "on_%s" % (method,)):
method_handler = getattr(self, "on_%s" % (method,)) method_handler = getattr(self, "on_%s" % (method,))
http_server.register_path(method, pattern, method_handler) http_server.register_paths(method, patterns, method_handler)
else: else:
raise NotImplementedError("RestServlet must register something.") raise NotImplementedError("RestServlet must register something.")

View File

@@ -14,6 +14,8 @@
# limitations under the License. # limitations under the License.
from twisted.internet import defer from twisted.internet import defer
from synapse.api.constants import EventTypes
from synapse.api.errors import AuthError
from synapse.util.logutils import log_function from synapse.util.logutils import log_function
from synapse.util.async import run_on_reactor, ObservableDeferred from synapse.util.async import run_on_reactor, ObservableDeferred
@@ -269,8 +271,8 @@ class Notifier(object):
logger.exception("Failed to notify listener") logger.exception("Failed to notify listener")
@defer.inlineCallbacks @defer.inlineCallbacks
def wait_for_events(self, user, rooms, timeout, callback, def wait_for_events(self, user, timeout, callback, room_ids=None,
from_token=StreamToken("s0", "0", "0", "0")): from_token=StreamToken("s0", "0", "0", "0", "0")):
"""Wait until the callback returns a non empty response or the """Wait until the callback returns a non empty response or the
timeout fires. timeout fires.
""" """
@@ -279,11 +281,12 @@ class Notifier(object):
if user_stream is None: if user_stream is None:
appservice = yield self.store.get_app_service_by_user_id(user) appservice = yield self.store.get_app_service_by_user_id(user)
current_token = yield self.event_sources.get_current_token() current_token = yield self.event_sources.get_current_token()
rooms = yield self.store.get_rooms_for_user(user) if room_ids is None:
rooms = [room.room_id for room in rooms] rooms = yield self.store.get_rooms_for_user(user)
room_ids = [room.room_id for room in rooms]
user_stream = _NotifierUserStream( user_stream = _NotifierUserStream(
user=user, user=user,
rooms=rooms, rooms=room_ids,
appservice=appservice, appservice=appservice,
current_token=current_token, current_token=current_token,
time_now_ms=self.clock.time_msec(), time_now_ms=self.clock.time_msec(),
@@ -328,8 +331,9 @@ class Notifier(object):
defer.returnValue(result) defer.returnValue(result)
@defer.inlineCallbacks @defer.inlineCallbacks
def get_events_for(self, user, rooms, pagination_config, timeout, def get_events_for(self, user, pagination_config, timeout,
only_room_events=False): only_room_events=False,
is_guest=False, guest_room_id=None):
""" For the given user and rooms, return any new events for them. If """ For the given user and rooms, return any new events for them. If
there are no new events wait for up to `timeout` milliseconds for any there are no new events wait for up to `timeout` milliseconds for any
new events to happen before returning. new events to happen before returning.
@@ -342,6 +346,16 @@ class Notifier(object):
limit = pagination_config.limit limit = pagination_config.limit
room_ids = []
if is_guest:
if guest_room_id:
if not (yield self._is_world_readable(guest_room_id)):
raise AuthError(403, "Guest access not allowed")
room_ids = [guest_room_id]
else:
rooms = yield self.store.get_rooms_for_user(user.to_string())
room_ids = [room.room_id for room in rooms]
@defer.inlineCallbacks @defer.inlineCallbacks
def check_for_updates(before_token, after_token): def check_for_updates(before_token, after_token):
if not after_token.is_after(before_token): if not after_token.is_after(before_token):
@@ -349,6 +363,7 @@ class Notifier(object):
events = [] events = []
end_token = from_token end_token = from_token
for name, source in self.event_sources.sources.items(): for name, source in self.event_sources.sources.items():
keyname = "%s_key" % name keyname = "%s_key" % name
before_id = getattr(before_token, keyname) before_id = getattr(before_token, keyname)
@@ -357,9 +372,23 @@ class Notifier(object):
continue continue
if only_room_events and name != "room": if only_room_events and name != "room":
continue continue
new_events, new_key = yield source.get_new_events_for_user( new_events, new_key = yield source.get_new_events(
user, getattr(from_token, keyname), limit, user=user,
from_key=getattr(from_token, keyname),
limit=limit,
is_guest=is_guest,
room_ids=room_ids,
) )
if name == "room":
room_member_handler = self.hs.get_handlers().room_member_handler
new_events = yield room_member_handler._filter_events_for_client(
user.to_string(),
new_events,
is_guest=is_guest,
require_all_visible_for_guests=False
)
events.extend(new_events) events.extend(new_events)
end_token = end_token.copy_and_replace(keyname, new_key) end_token = end_token.copy_and_replace(keyname, new_key)
@@ -369,7 +398,7 @@ class Notifier(object):
defer.returnValue(None) defer.returnValue(None)
result = yield self.wait_for_events( result = yield self.wait_for_events(
user, rooms, timeout, check_for_updates, from_token=from_token user, timeout, check_for_updates, room_ids=room_ids, from_token=from_token
) )
if result is None: if result is None:
@@ -377,6 +406,17 @@ class Notifier(object):
defer.returnValue(result) defer.returnValue(result)
@defer.inlineCallbacks
def _is_world_readable(self, room_id):
state = yield self.hs.get_state_handler().get_current_state(
room_id,
EventTypes.RoomHistoryVisibility
)
if state and "history_visibility" in state.content:
defer.returnValue(state.content["history_visibility"] == "world_readable")
else:
defer.returnValue(False)
@log_function @log_function
def remove_expired_streams(self): def remove_expired_streams(self):
time_now_ms = self.clock.time_msec() time_now_ms = self.clock.time_msec()

View File

@@ -16,14 +16,12 @@
from twisted.internet import defer from twisted.internet import defer
from synapse.streams.config import PaginationConfig from synapse.streams.config import PaginationConfig
from synapse.types import StreamToken, UserID from synapse.types import StreamToken
import synapse.util.async import synapse.util.async
import baserules import push_rule_evaluator as push_rule_evaluator
import logging import logging
import simplejson as json
import re
import random import random
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -33,9 +31,6 @@ class Pusher(object):
INITIAL_BACKOFF = 1000 INITIAL_BACKOFF = 1000
MAX_BACKOFF = 60 * 60 * 1000 MAX_BACKOFF = 60 * 60 * 1000
GIVE_UP_AFTER = 24 * 60 * 60 * 1000 GIVE_UP_AFTER = 24 * 60 * 60 * 1000
DEFAULT_ACTIONS = ['dont_notify']
INEQUALITY_EXPR = re.compile("^([=<>]*)([0-9]*)$")
def __init__(self, _hs, profile_tag, user_name, app_id, def __init__(self, _hs, profile_tag, user_name, app_id,
app_display_name, device_display_name, pushkey, pushkey_ts, app_display_name, device_display_name, pushkey, pushkey_ts,
@@ -62,161 +57,6 @@ class Pusher(object):
self.last_last_active_time = 0 self.last_last_active_time = 0
self.has_unread = True self.has_unread = True
@defer.inlineCallbacks
def _actions_for_event(self, ev):
"""
This should take into account notification settings that the user
has configured both globally and per-room when we have the ability
to do such things.
"""
if ev['user_id'] == self.user_name:
# let's assume you probably know about messages you sent yourself
defer.returnValue(['dont_notify'])
rawrules = yield self.store.get_push_rules_for_user(self.user_name)
rules = []
for rawrule in rawrules:
rule = dict(rawrule)
rule['conditions'] = json.loads(rawrule['conditions'])
rule['actions'] = json.loads(rawrule['actions'])
rules.append(rule)
enabled_map = yield self.store.get_push_rules_enabled_for_user(self.user_name)
user = UserID.from_string(self.user_name)
rules = baserules.list_with_base_rules(rules, user)
room_id = ev['room_id']
# get *our* member event for display name matching
my_display_name = None
our_member_event = yield self.store.get_current_state(
room_id=room_id,
event_type='m.room.member',
state_key=self.user_name,
)
if our_member_event:
my_display_name = our_member_event[0].content.get("displayname")
room_members = yield self.store.get_users_in_room(room_id)
room_member_count = len(room_members)
for r in rules:
if r['rule_id'] in enabled_map:
r['enabled'] = enabled_map[r['rule_id']]
elif 'enabled' not in r:
r['enabled'] = True
if not r['enabled']:
continue
matches = True
conditions = r['conditions']
actions = r['actions']
for c in conditions:
matches &= self._event_fulfills_condition(
ev, c, display_name=my_display_name,
room_member_count=room_member_count
)
logger.debug(
"Rule %s %s",
r['rule_id'], "matches" if matches else "doesn't match"
)
# ignore rules with no actions (we have an explict 'dont_notify')
if len(actions) == 0:
logger.warn(
"Ignoring rule id %s with no actions for user %s",
r['rule_id'], self.user_name
)
continue
if matches:
logger.info(
"%s matches for user %s, event %s",
r['rule_id'], self.user_name, ev['event_id']
)
defer.returnValue(actions)
logger.info(
"No rules match for user %s, event %s",
self.user_name, ev['event_id']
)
defer.returnValue(Pusher.DEFAULT_ACTIONS)
@staticmethod
def _glob_to_regexp(glob):
r = re.escape(glob)
r = re.sub(r'\\\*', r'.*?', r)
r = re.sub(r'\\\?', r'.', r)
# handle [abc], [a-z] and [!a-z] style ranges.
r = re.sub(r'\\\[(\\\!|)(.*)\\\]',
lambda x: ('[%s%s]' % (x.group(1) and '^' or '',
re.sub(r'\\\-', '-', x.group(2)))), r)
return r
def _event_fulfills_condition(self, ev, condition, display_name, room_member_count):
if condition['kind'] == 'event_match':
if 'pattern' not in condition:
logger.warn("event_match condition with no pattern")
return False
# XXX: optimisation: cache our pattern regexps
if condition['key'] == 'content.body':
r = r'\b%s\b' % self._glob_to_regexp(condition['pattern'])
else:
r = r'^%s$' % self._glob_to_regexp(condition['pattern'])
val = _value_for_dotted_key(condition['key'], ev)
if val is None:
return False
return re.search(r, val, flags=re.IGNORECASE) is not None
elif condition['kind'] == 'device':
if 'profile_tag' not in condition:
return True
return condition['profile_tag'] == self.profile_tag
elif condition['kind'] == 'contains_display_name':
# This is special because display names can be different
# between rooms and so you can't really hard code it in a rule.
# Optimisation: we should cache these names and update them from
# the event stream.
if 'content' not in ev or 'body' not in ev['content']:
return False
if not display_name:
return False
return re.search(
"\b%s\b" % re.escape(display_name), ev['content']['body'],
flags=re.IGNORECASE
) is not None
elif condition['kind'] == 'room_member_count':
if 'is' not in condition:
return False
m = Pusher.INEQUALITY_EXPR.match(condition['is'])
if not m:
return False
ineq = m.group(1)
rhs = m.group(2)
if not rhs.isdigit():
return False
rhs = int(rhs)
if ineq == '' or ineq == '==':
return room_member_count == rhs
elif ineq == '<':
return room_member_count < rhs
elif ineq == '>':
return room_member_count > rhs
elif ineq == '>=':
return room_member_count >= rhs
elif ineq == '<=':
return room_member_count <= rhs
else:
return False
else:
return True
@defer.inlineCallbacks @defer.inlineCallbacks
def get_context_for_event(self, ev): def get_context_for_event(self, ev):
name_aliases = yield self.store.get_room_name_and_aliases( name_aliases = yield self.store.get_room_name_and_aliases(
@@ -308,8 +148,14 @@ class Pusher(object):
return return
processed = False processed = False
actions = yield self._actions_for_event(single_event)
tweaks = _tweaks_for_actions(actions) rule_evaluator = yield \
push_rule_evaluator.evaluator_for_user_name_and_profile_tag(
self.user_name, self.profile_tag, single_event['room_id'], self.store
)
actions = yield rule_evaluator.actions_for_event(single_event)
tweaks = rule_evaluator.tweaks_for_actions(actions)
if len(actions) == 0: if len(actions) == 0:
logger.warn("Empty actions! Using default action.") logger.warn("Empty actions! Using default action.")
@@ -448,27 +294,6 @@ class Pusher(object):
self.has_unread = False self.has_unread = False
def _value_for_dotted_key(dotted_key, event):
parts = dotted_key.split(".")
val = event
while len(parts) > 0:
if parts[0] not in val:
return None
val = val[parts[0]]
parts = parts[1:]
return val
def _tweaks_for_actions(actions):
tweaks = {}
for a in actions:
if not isinstance(a, dict):
continue
if 'set_tweak' in a and 'value' in a:
tweaks[a['set_tweak']] = a['value']
return tweaks
class PusherConfigException(Exception): class PusherConfigException(Exception):
def __init__(self, msg): def __init__(self, msg):
super(PusherConfigException, self).__init__(msg) super(PusherConfigException, self).__init__(msg)

View File

@@ -247,6 +247,7 @@ def make_base_append_underride_rules(user):
}, },
{ {
'rule_id': 'global/underride/.m.rule.message', 'rule_id': 'global/underride/.m.rule.message',
'enabled': False,
'conditions': [ 'conditions': [
{ {
'kind': 'event_match', 'kind': 'event_match',

View File

@@ -14,7 +14,6 @@
# limitations under the License. # limitations under the License.
from synapse.push import Pusher, PusherConfigException from synapse.push import Pusher, PusherConfigException
from synapse.http.client import SimpleHttpClient
from twisted.internet import defer from twisted.internet import defer
@@ -46,7 +45,7 @@ class HttpPusher(Pusher):
"'url' required in data for HTTP pusher" "'url' required in data for HTTP pusher"
) )
self.url = data['url'] self.url = data['url']
self.httpCli = SimpleHttpClient(self.hs) self.http_client = _hs.get_simple_http_client()
self.data_minus_url = {} self.data_minus_url = {}
self.data_minus_url.update(self.data) self.data_minus_url.update(self.data)
del self.data_minus_url['url'] del self.data_minus_url['url']
@@ -107,7 +106,7 @@ class HttpPusher(Pusher):
if not notification_dict: if not notification_dict:
defer.returnValue([]) defer.returnValue([])
try: try:
resp = yield self.httpCli.post_json_get_json(self.url, notification_dict) resp = yield self.http_client.post_json_get_json(self.url, notification_dict)
except: except:
logger.warn("Failed to push %s ", self.url) logger.warn("Failed to push %s ", self.url)
defer.returnValue(False) defer.returnValue(False)
@@ -138,7 +137,7 @@ class HttpPusher(Pusher):
} }
} }
try: try:
resp = yield self.httpCli.post_json_get_json(self.url, d) resp = yield self.http_client.post_json_get_json(self.url, d)
except: except:
logger.exception("Failed to push %s ", self.url) logger.exception("Failed to push %s ", self.url)
defer.returnValue(False) defer.returnValue(False)

View File

@@ -0,0 +1,224 @@
# -*- coding: utf-8 -*-
# Copyright 2015 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 twisted.internet import defer
from synapse.types import UserID
import baserules
import logging
import simplejson as json
import re
logger = logging.getLogger(__name__)
@defer.inlineCallbacks
def evaluator_for_user_name_and_profile_tag(user_name, profile_tag, room_id, store):
rawrules = yield store.get_push_rules_for_user(user_name)
enabled_map = yield store.get_push_rules_enabled_for_user(user_name)
our_member_event = yield store.get_current_state(
room_id=room_id,
event_type='m.room.member',
state_key=user_name,
)
defer.returnValue(PushRuleEvaluator(
user_name, profile_tag, rawrules, enabled_map,
room_id, our_member_event, store
))
class PushRuleEvaluator:
DEFAULT_ACTIONS = ['dont_notify']
INEQUALITY_EXPR = re.compile("^([=<>]*)([0-9]*)$")
def __init__(self, user_name, profile_tag, raw_rules, enabled_map, room_id,
our_member_event, store):
self.user_name = user_name
self.profile_tag = profile_tag
self.room_id = room_id
self.our_member_event = our_member_event
self.store = store
rules = []
for raw_rule in raw_rules:
rule = dict(raw_rule)
rule['conditions'] = json.loads(raw_rule['conditions'])
rule['actions'] = json.loads(raw_rule['actions'])
rules.append(rule)
user = UserID.from_string(self.user_name)
self.rules = baserules.list_with_base_rules(rules, user)
self.enabled_map = enabled_map
@staticmethod
def tweaks_for_actions(actions):
tweaks = {}
for a in actions:
if not isinstance(a, dict):
continue
if 'set_tweak' in a and 'value' in a:
tweaks[a['set_tweak']] = a['value']
return tweaks
@defer.inlineCallbacks
def actions_for_event(self, ev):
"""
This should take into account notification settings that the user
has configured both globally and per-room when we have the ability
to do such things.
"""
if ev['user_id'] == self.user_name:
# let's assume you probably know about messages you sent yourself
defer.returnValue(['dont_notify'])
room_id = ev['room_id']
# get *our* member event for display name matching
my_display_name = None
if self.our_member_event:
my_display_name = self.our_member_event[0].content.get("displayname")
room_members = yield self.store.get_users_in_room(room_id)
room_member_count = len(room_members)
for r in self.rules:
if r['rule_id'] in self.enabled_map:
r['enabled'] = self.enabled_map[r['rule_id']]
elif 'enabled' not in r:
r['enabled'] = True
if not r['enabled']:
continue
matches = True
conditions = r['conditions']
actions = r['actions']
for c in conditions:
matches &= self._event_fulfills_condition(
ev, c, display_name=my_display_name,
room_member_count=room_member_count
)
logger.debug(
"Rule %s %s",
r['rule_id'], "matches" if matches else "doesn't match"
)
# ignore rules with no actions (we have an explict 'dont_notify')
if len(actions) == 0:
logger.warn(
"Ignoring rule id %s with no actions for user %s",
r['rule_id'], self.user_name
)
continue
if matches:
logger.info(
"%s matches for user %s, event %s",
r['rule_id'], self.user_name, ev['event_id']
)
defer.returnValue(actions)
logger.info(
"No rules match for user %s, event %s",
self.user_name, ev['event_id']
)
defer.returnValue(PushRuleEvaluator.DEFAULT_ACTIONS)
@staticmethod
def _glob_to_regexp(glob):
r = re.escape(glob)
r = re.sub(r'\\\*', r'.*?', r)
r = re.sub(r'\\\?', r'.', r)
# handle [abc], [a-z] and [!a-z] style ranges.
r = re.sub(r'\\\[(\\\!|)(.*)\\\]',
lambda x: ('[%s%s]' % (x.group(1) and '^' or '',
re.sub(r'\\\-', '-', x.group(2)))), r)
return r
def _event_fulfills_condition(self, ev, condition, display_name, room_member_count):
if condition['kind'] == 'event_match':
if 'pattern' not in condition:
logger.warn("event_match condition with no pattern")
return False
# XXX: optimisation: cache our pattern regexps
if condition['key'] == 'content.body':
r = r'\b%s\b' % self._glob_to_regexp(condition['pattern'])
else:
r = r'^%s$' % self._glob_to_regexp(condition['pattern'])
val = _value_for_dotted_key(condition['key'], ev)
if val is None:
return False
return re.search(r, val, flags=re.IGNORECASE) is not None
elif condition['kind'] == 'device':
if 'profile_tag' not in condition:
return True
return condition['profile_tag'] == self.profile_tag
elif condition['kind'] == 'contains_display_name':
# This is special because display names can be different
# between rooms and so you can't really hard code it in a rule.
# Optimisation: we should cache these names and update them from
# the event stream.
if 'content' not in ev or 'body' not in ev['content']:
return False
if not display_name:
return False
return re.search(
r"\b%s\b" % re.escape(display_name), ev['content']['body'],
flags=re.IGNORECASE
) is not None
elif condition['kind'] == 'room_member_count':
if 'is' not in condition:
return False
m = PushRuleEvaluator.INEQUALITY_EXPR.match(condition['is'])
if not m:
return False
ineq = m.group(1)
rhs = m.group(2)
if not rhs.isdigit():
return False
rhs = int(rhs)
if ineq == '' or ineq == '==':
return room_member_count == rhs
elif ineq == '<':
return room_member_count < rhs
elif ineq == '>':
return room_member_count > rhs
elif ineq == '>=':
return room_member_count >= rhs
elif ineq == '<=':
return room_member_count <= rhs
else:
return False
else:
return True
def _value_for_dotted_key(dotted_key, event):
parts = dotted_key.split(".")
val = event
while len(parts) > 0:
if parts[0] not in val:
return None
val = val[parts[0]]
parts = parts[1:]
return val

View File

@@ -18,18 +18,18 @@ from distutils.version import LooseVersion
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
REQUIREMENTS = { REQUIREMENTS = {
"frozendict>=0.4": ["frozendict"],
"unpaddedbase64>=1.0.1": ["unpaddedbase64>=1.0.1"], "unpaddedbase64>=1.0.1": ["unpaddedbase64>=1.0.1"],
"canonicaljson>=1.0.0": ["canonicaljson>=1.0.0"], "canonicaljson>=1.0.0": ["canonicaljson>=1.0.0"],
"signedjson>=1.0.0": ["signedjson>=1.0.0"], "signedjson>=1.0.0": ["signedjson>=1.0.0"],
"Twisted>=15.1.0": ["twisted>=15.1.0"], "pynacl>=0.3.0": ["nacl>=0.3.0", "nacl.bindings"],
"service_identity>=1.0.0": ["service_identity>=1.0.0"], "service_identity>=1.0.0": ["service_identity>=1.0.0"],
"Twisted>=15.1.0": ["twisted>=15.1.0"],
"pyopenssl>=0.14": ["OpenSSL>=0.14"], "pyopenssl>=0.14": ["OpenSSL>=0.14"],
"pyyaml": ["yaml"], "pyyaml": ["yaml"],
"pyasn1": ["pyasn1"], "pyasn1": ["pyasn1"],
"pynacl>=0.3.0": ["nacl>=0.3.0"],
"daemonize": ["daemonize"], "daemonize": ["daemonize"],
"py-bcrypt": ["bcrypt"], "py-bcrypt": ["bcrypt"],
"frozendict>=0.4": ["frozendict"],
"pillow": ["PIL"], "pillow": ["PIL"],
"pydenticon": ["pydenticon"], "pydenticon": ["pydenticon"],
"ujson": ["ujson"], "ujson": ["ujson"],
@@ -60,7 +60,10 @@ DEPENDENCY_LINKS = {
class MissingRequirementError(Exception): class MissingRequirementError(Exception):
pass def __init__(self, message, module_name, dependency):
super(MissingRequirementError, self).__init__(message)
self.module_name = module_name
self.dependency = dependency
def check_requirements(config=None): def check_requirements(config=None):
@@ -88,7 +91,7 @@ def check_requirements(config=None):
) )
raise MissingRequirementError( raise MissingRequirementError(
"Can't import %r which is part of %r" "Can't import %r which is part of %r"
% (module_name, dependency) % (module_name, dependency), module_name, dependency
) )
version = getattr(module, "__version__", None) version = getattr(module, "__version__", None)
file_path = getattr(module, "__file__", None) file_path = getattr(module, "__file__", None)
@@ -101,23 +104,25 @@ def check_requirements(config=None):
if version is None: if version is None:
raise MissingRequirementError( raise MissingRequirementError(
"Version of %r isn't set as __version__ of module %r" "Version of %r isn't set as __version__ of module %r"
% (dependency, module_name) % (dependency, module_name), module_name, dependency
) )
if LooseVersion(version) < LooseVersion(required_version): if LooseVersion(version) < LooseVersion(required_version):
raise MissingRequirementError( raise MissingRequirementError(
"Version of %r in %r is too old. %r < %r" "Version of %r in %r is too old. %r < %r"
% (dependency, file_path, version, required_version) % (dependency, file_path, version, required_version),
module_name, dependency
) )
elif version_test == "==": elif version_test == "==":
if version is None: if version is None:
raise MissingRequirementError( raise MissingRequirementError(
"Version of %r isn't set as __version__ of module %r" "Version of %r isn't set as __version__ of module %r"
% (dependency, module_name) % (dependency, module_name), module_name, dependency
) )
if LooseVersion(version) != LooseVersion(required_version): if LooseVersion(version) != LooseVersion(required_version):
raise MissingRequirementError( raise MissingRequirementError(
"Unexpected version of %r in %r. %r != %r" "Unexpected version of %r in %r. %r != %r"
% (dependency, file_path, version, required_version) % (dependency, file_path, version, required_version),
module_name, dependency
) )

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2015 OpenMarket Ltd # Copyright 2014, 2015 OpenMarket Ltd
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -12,3 +12,69 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# 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.rest.client.v1 import (
room,
events,
profile,
presence,
initial_sync,
directory,
voip,
admin,
pusher,
push_rule,
register as v1_register,
login as v1_login,
)
from synapse.rest.client.v2_alpha import (
sync,
filter,
account,
register,
auth,
receipts,
keys,
tokenrefresh,
tags,
account_data,
)
from synapse.http.server import JsonResource
class ClientRestResource(JsonResource):
"""A resource for version 1 of the matrix client API."""
def __init__(self, hs):
JsonResource.__init__(self, hs, canonical_json=False)
self.register_servlets(self, hs)
@staticmethod
def register_servlets(client_resource, hs):
# "v1"
room.register_servlets(hs, client_resource)
events.register_servlets(hs, client_resource)
v1_register.register_servlets(hs, client_resource)
v1_login.register_servlets(hs, client_resource)
profile.register_servlets(hs, client_resource)
presence.register_servlets(hs, client_resource)
initial_sync.register_servlets(hs, client_resource)
directory.register_servlets(hs, client_resource)
voip.register_servlets(hs, client_resource)
admin.register_servlets(hs, client_resource)
pusher.register_servlets(hs, client_resource)
push_rule.register_servlets(hs, client_resource)
# "v2"
sync.register_servlets(hs, client_resource)
filter.register_servlets(hs, client_resource)
account.register_servlets(hs, client_resource)
register.register_servlets(hs, client_resource)
auth.register_servlets(hs, client_resource)
receipts.register_servlets(hs, client_resource)
keys.register_servlets(hs, client_resource)
tokenrefresh.register_servlets(hs, client_resource)
tags.register_servlets(hs, client_resource)
account_data.register_servlets(hs, client_resource)

View File

@@ -12,33 +12,3 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# 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 (
room, events, register, login, profile, presence, initial_sync, directory,
voip, admin, pusher, push_rule
)
from synapse.http.server import JsonResource
class ClientV1RestResource(JsonResource):
"""A resource for version 1 of the matrix client API."""
def __init__(self, hs):
JsonResource.__init__(self, hs, canonical_json=False)
self.register_servlets(self, hs)
@staticmethod
def register_servlets(client_resource, hs):
room.register_servlets(hs, client_resource)
events.register_servlets(hs, client_resource)
register.register_servlets(hs, client_resource)
login.register_servlets(hs, client_resource)
profile.register_servlets(hs, client_resource)
presence.register_servlets(hs, client_resource)
initial_sync.register_servlets(hs, client_resource)
directory.register_servlets(hs, client_resource)
voip.register_servlets(hs, client_resource)
admin.register_servlets(hs, client_resource)
pusher.register_servlets(hs, client_resource)
push_rule.register_servlets(hs, client_resource)

View File

@@ -18,7 +18,7 @@ from twisted.internet import defer
from synapse.api.errors import AuthError, SynapseError from synapse.api.errors import AuthError, SynapseError
from synapse.types import UserID from synapse.types import UserID
from base import ClientV1RestServlet, client_path_pattern from base import ClientV1RestServlet, client_path_patterns
import logging import logging
@@ -26,12 +26,12 @@ logger = logging.getLogger(__name__)
class WhoisRestServlet(ClientV1RestServlet): class WhoisRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/admin/whois/(?P<user_id>[^/]*)") PATTERNS = client_path_patterns("/admin/whois/(?P<user_id>[^/]*)")
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, user_id): def on_GET(self, request, user_id):
target_user = UserID.from_string(user_id) target_user = UserID.from_string(user_id)
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
is_admin = yield self.auth.is_server_admin(auth_user) is_admin = yield self.auth.is_server_admin(auth_user)
if not is_admin and target_user != auth_user: if not is_admin and target_user != auth_user:

View File

@@ -27,7 +27,7 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def client_path_pattern(path_regex): def client_path_patterns(path_regex, releases=(0,), include_in_unstable=True):
"""Creates a regex compiled client path with the correct client path """Creates a regex compiled client path with the correct client path
prefix. prefix.
@@ -37,7 +37,14 @@ def client_path_pattern(path_regex):
Returns: Returns:
SRE_Pattern SRE_Pattern
""" """
return re.compile("^" + CLIENT_PREFIX + path_regex) patterns = [re.compile("^" + CLIENT_PREFIX + path_regex)]
if include_in_unstable:
unstable_prefix = CLIENT_PREFIX.replace("/api/v1", "/unstable")
patterns.append(re.compile("^" + unstable_prefix + path_regex))
for release in releases:
new_prefix = CLIENT_PREFIX.replace("/api/v1", "/r%d" % release)
patterns.append(re.compile("^" + new_prefix + path_regex))
return patterns
class ClientV1RestServlet(RestServlet): class ClientV1RestServlet(RestServlet):

View File

@@ -18,7 +18,7 @@ from twisted.internet import defer
from synapse.api.errors import AuthError, SynapseError, Codes from synapse.api.errors import AuthError, SynapseError, Codes
from synapse.types import RoomAlias from synapse.types import RoomAlias
from .base import ClientV1RestServlet, client_path_pattern from .base import ClientV1RestServlet, client_path_patterns
import simplejson as json import simplejson as json
import logging import logging
@@ -32,7 +32,7 @@ def register_servlets(hs, http_server):
class ClientDirectoryServer(ClientV1RestServlet): class ClientDirectoryServer(ClientV1RestServlet):
PATTERN = client_path_pattern("/directory/room/(?P<room_alias>[^/]*)$") PATTERNS = client_path_patterns("/directory/room/(?P<room_alias>[^/]*)$")
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, room_alias): def on_GET(self, request, room_alias):
@@ -69,7 +69,7 @@ class ClientDirectoryServer(ClientV1RestServlet):
try: try:
# try to auth as a user # try to auth as a user
user, _ = yield self.auth.get_user_by_req(request) user, _, _ = yield self.auth.get_user_by_req(request)
try: try:
user_id = user.to_string() user_id = user.to_string()
yield dir_handler.create_association( yield dir_handler.create_association(
@@ -116,7 +116,7 @@ class ClientDirectoryServer(ClientV1RestServlet):
# fallback to default user behaviour if they aren't an AS # fallback to default user behaviour if they aren't an AS
pass pass
user, _ = yield self.auth.get_user_by_req(request) user, _, _ = yield self.auth.get_user_by_req(request)
is_admin = yield self.auth.is_server_admin(user) is_admin = yield self.auth.is_server_admin(user)
if not is_admin: if not is_admin:

View File

@@ -18,7 +18,7 @@ from twisted.internet import defer
from synapse.api.errors import SynapseError from synapse.api.errors import SynapseError
from synapse.streams.config import PaginationConfig from synapse.streams.config import PaginationConfig
from .base import ClientV1RestServlet, client_path_pattern from .base import ClientV1RestServlet, client_path_patterns
from synapse.events.utils import serialize_event from synapse.events.utils import serialize_event
import logging import logging
@@ -28,13 +28,21 @@ logger = logging.getLogger(__name__)
class EventStreamRestServlet(ClientV1RestServlet): class EventStreamRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/events$") PATTERNS = client_path_patterns("/events$")
DEFAULT_LONGPOLL_TIME_MS = 30000 DEFAULT_LONGPOLL_TIME_MS = 30000
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request): def on_GET(self, request):
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, is_guest = yield self.auth.get_user_by_req(
request,
allow_guest=True
)
room_id = None
if is_guest:
if "room_id" not in request.args:
raise SynapseError(400, "Guest users must specify room_id param")
room_id = request.args["room_id"][0]
try: try:
handler = self.handlers.event_stream_handler handler = self.handlers.event_stream_handler
pagin_config = PaginationConfig.from_request(request) pagin_config = PaginationConfig.from_request(request)
@@ -49,7 +57,8 @@ class EventStreamRestServlet(ClientV1RestServlet):
chunk = yield handler.get_stream( chunk = yield handler.get_stream(
auth_user.to_string(), pagin_config, timeout=timeout, auth_user.to_string(), pagin_config, timeout=timeout,
as_client_event=as_client_event as_client_event=as_client_event, affect_presence=(not is_guest),
room_id=room_id, is_guest=is_guest
) )
except: except:
logger.exception("Event stream failed") logger.exception("Event stream failed")
@@ -63,7 +72,7 @@ class EventStreamRestServlet(ClientV1RestServlet):
# TODO: Unit test gets, with and without auth, with different kinds of events. # TODO: Unit test gets, with and without auth, with different kinds of events.
class EventRestServlet(ClientV1RestServlet): class EventRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/events/(?P<event_id>[^/]*)$") PATTERNS = client_path_patterns("/events/(?P<event_id>[^/]*)$")
def __init__(self, hs): def __init__(self, hs):
super(EventRestServlet, self).__init__(hs) super(EventRestServlet, self).__init__(hs)
@@ -71,7 +80,7 @@ class EventRestServlet(ClientV1RestServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, event_id): def on_GET(self, request, event_id):
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
handler = self.handlers.event_handler handler = self.handlers.event_handler
event = yield handler.get_event(auth_user, event_id) event = yield handler.get_event(auth_user, event_id)

View File

@@ -16,25 +16,25 @@
from twisted.internet import defer from twisted.internet import defer
from synapse.streams.config import PaginationConfig from synapse.streams.config import PaginationConfig
from base import ClientV1RestServlet, client_path_pattern from base import ClientV1RestServlet, client_path_patterns
# TODO: Needs unit testing # TODO: Needs unit testing
class InitialSyncRestServlet(ClientV1RestServlet): class InitialSyncRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/initialSync$") PATTERNS = client_path_patterns("/initialSync$")
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request): def on_GET(self, request):
user, _ = yield self.auth.get_user_by_req(request) user, _, _ = yield self.auth.get_user_by_req(request)
with_feedback = "feedback" in request.args
as_client_event = "raw" not in request.args as_client_event = "raw" not in request.args
pagination_config = PaginationConfig.from_request(request) pagination_config = PaginationConfig.from_request(request)
handler = self.handlers.message_handler handler = self.handlers.message_handler
include_archived = request.args.get("archived", None) == ["true"]
content = yield handler.snapshot_all_rooms( content = yield handler.snapshot_all_rooms(
user_id=user.to_string(), user_id=user.to_string(),
pagin_config=pagination_config, pagin_config=pagination_config,
feedback=with_feedback, as_client_event=as_client_event,
as_client_event=as_client_event include_archived=include_archived,
) )
defer.returnValue((200, content)) defer.returnValue((200, content))

View File

@@ -15,36 +15,61 @@
from twisted.internet import defer from twisted.internet import defer
from synapse.api.errors import SynapseError from synapse.api.errors import SynapseError, LoginError, Codes
from synapse.types import UserID from synapse.types import UserID
from base import ClientV1RestServlet, client_path_pattern from base import ClientV1RestServlet, client_path_patterns
import simplejson as json import simplejson as json
import urllib import urllib
import urlparse
import logging import logging
from saml2 import BINDING_HTTP_POST from saml2 import BINDING_HTTP_POST
from saml2 import config from saml2 import config
from saml2.client import Saml2Client from saml2.client import Saml2Client
import xml.etree.ElementTree as ET
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class LoginRestServlet(ClientV1RestServlet): class LoginRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/login$") PATTERNS = client_path_patterns("/login$")
PASS_TYPE = "m.login.password" PASS_TYPE = "m.login.password"
SAML2_TYPE = "m.login.saml2" SAML2_TYPE = "m.login.saml2"
CAS_TYPE = "m.login.cas"
TOKEN_TYPE = "m.login.token"
def __init__(self, hs): def __init__(self, hs):
super(LoginRestServlet, self).__init__(hs) super(LoginRestServlet, self).__init__(hs)
self.idp_redirect_url = hs.config.saml2_idp_redirect_url self.idp_redirect_url = hs.config.saml2_idp_redirect_url
self.password_enabled = hs.config.password_enabled
self.saml2_enabled = hs.config.saml2_enabled self.saml2_enabled = hs.config.saml2_enabled
self.cas_enabled = hs.config.cas_enabled
self.cas_server_url = hs.config.cas_server_url
self.cas_required_attributes = hs.config.cas_required_attributes
self.servername = hs.config.server_name
self.http_client = hs.get_simple_http_client()
def on_GET(self, request): def on_GET(self, request):
flows = [{"type": LoginRestServlet.PASS_TYPE}] flows = []
if self.saml2_enabled: if self.saml2_enabled:
flows.append({"type": LoginRestServlet.SAML2_TYPE}) flows.append({"type": LoginRestServlet.SAML2_TYPE})
if self.cas_enabled:
flows.append({"type": LoginRestServlet.CAS_TYPE})
# While its valid for us to advertise this login type generally,
# synapse currently only gives out these tokens as part of the
# CAS login flow.
# Generally we don't want to advertise login flows that clients
# don't know how to implement, since they (currently) will always
# fall back to the fallback API if they don't understand one of the
# login flow types returned.
flows.append({"type": LoginRestServlet.TOKEN_TYPE})
if self.password_enabled:
flows.append({"type": LoginRestServlet.PASS_TYPE})
return (200, {"flows": flows}) return (200, {"flows": flows})
def on_OPTIONS(self, request): def on_OPTIONS(self, request):
@@ -55,6 +80,9 @@ class LoginRestServlet(ClientV1RestServlet):
login_submission = _parse_json(request) login_submission = _parse_json(request)
try: try:
if login_submission["type"] == LoginRestServlet.PASS_TYPE: if login_submission["type"] == LoginRestServlet.PASS_TYPE:
if not self.password_enabled:
raise SynapseError(400, "Password login has been disabled.")
result = yield self.do_password_login(login_submission) result = yield self.do_password_login(login_submission)
defer.returnValue(result) defer.returnValue(result)
elif self.saml2_enabled and (login_submission["type"] == elif self.saml2_enabled and (login_submission["type"] ==
@@ -67,6 +95,20 @@ class LoginRestServlet(ClientV1RestServlet):
"uri": "%s%s" % (self.idp_redirect_url, relay_state) "uri": "%s%s" % (self.idp_redirect_url, relay_state)
} }
defer.returnValue((200, result)) defer.returnValue((200, result))
# TODO Delete this after all CAS clients switch to token login instead
elif self.cas_enabled and (login_submission["type"] ==
LoginRestServlet.CAS_TYPE):
uri = "%s/proxyValidate" % (self.cas_server_url,)
args = {
"ticket": login_submission["ticket"],
"service": login_submission["service"]
}
body = yield self.http_client.get_raw(uri, args)
result = yield self.do_cas_login(body)
defer.returnValue(result)
elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
result = yield self.do_token_login(login_submission)
defer.returnValue(result)
else: else:
raise SynapseError(400, "Bad login type.") raise SynapseError(400, "Bad login type.")
except KeyError: except KeyError:
@@ -78,6 +120,8 @@ class LoginRestServlet(ClientV1RestServlet):
user_id = yield self.hs.get_datastore().get_user_id_by_threepid( user_id = yield self.hs.get_datastore().get_user_id_by_threepid(
login_submission['medium'], login_submission['address'] login_submission['medium'], login_submission['address']
) )
if not user_id:
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
else: else:
user_id = login_submission['user'] user_id = login_submission['user']
@@ -100,39 +144,98 @@ class LoginRestServlet(ClientV1RestServlet):
defer.returnValue((200, result)) defer.returnValue((200, result))
class LoginFallbackRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/login/fallback$")
def on_GET(self, request):
# TODO(kegan): This should be returning some HTML which is capable of
# hitting LoginRestServlet
return (200, {})
class PasswordResetRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/login/reset")
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request): def do_token_login(self, login_submission):
reset_info = _parse_json(request) token = login_submission['token']
try: auth_handler = self.handlers.auth_handler
email = reset_info["email"] user_id = (
user_id = reset_info["user_id"] yield auth_handler.validate_short_term_login_token_and_get_user_id(token)
handler = self.handlers.login_handler )
yield handler.reset_password(user_id, email) user_id, access_token, refresh_token = (
# purposefully give no feedback to avoid people hammering different yield auth_handler.get_login_tuple_for_user_id(user_id)
# combinations. )
defer.returnValue((200, {})) result = {
except KeyError: "user_id": user_id, # may have changed
raise SynapseError( "access_token": access_token,
400, "refresh_token": refresh_token,
"Missing keys. Requires 'email' and 'user_id'." "home_server": self.hs.hostname,
}
defer.returnValue((200, result))
# TODO Delete this after all CAS clients switch to token login instead
@defer.inlineCallbacks
def do_cas_login(self, cas_response_body):
user, attributes = self.parse_cas_response(cas_response_body)
for required_attribute, required_value in self.cas_required_attributes.items():
# If required attribute was not in CAS Response - Forbidden
if required_attribute not in attributes:
raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED)
# Also need to check value
if required_value is not None:
actual_value = attributes[required_attribute]
# If required attribute value does not match expected - Forbidden
if required_value != actual_value:
raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED)
user_id = UserID.create(user, self.hs.hostname).to_string()
auth_handler = self.handlers.auth_handler
user_exists = yield auth_handler.does_user_exist(user_id)
if user_exists:
user_id, access_token, refresh_token = (
yield auth_handler.get_login_tuple_for_user_id(user_id)
) )
result = {
"user_id": user_id, # may have changed
"access_token": access_token,
"refresh_token": refresh_token,
"home_server": self.hs.hostname,
}
else:
user_id, access_token = (
yield self.handlers.registration_handler.register(localpart=user)
)
result = {
"user_id": user_id, # may have changed
"access_token": access_token,
"home_server": self.hs.hostname,
}
defer.returnValue((200, result))
# TODO Delete this after all CAS clients switch to token login instead
def parse_cas_response(self, cas_response_body):
root = ET.fromstring(cas_response_body)
if not root.tag.endswith("serviceResponse"):
raise LoginError(401, "Invalid CAS response", errcode=Codes.UNAUTHORIZED)
if not root[0].tag.endswith("authenticationSuccess"):
raise LoginError(401, "Unsuccessful CAS response", errcode=Codes.UNAUTHORIZED)
for child in root[0]:
if child.tag.endswith("user"):
user = child.text
if child.tag.endswith("attributes"):
attributes = {}
for attribute in child:
# ElementTree library expands the namespace in attribute tags
# to the full URL of the namespace.
# See (https://docs.python.org/2/library/xml.etree.elementtree.html)
# We don't care about namespace here and it will always be encased in
# curly braces, so we remove them.
if "}" in attribute.tag:
attributes[attribute.tag.split("}")[1]] = attribute.text
else:
attributes[attribute.tag] = attribute.text
if user is None or attributes is None:
raise LoginError(401, "Invalid CAS response", errcode=Codes.UNAUTHORIZED)
return (user, attributes)
class SAML2RestServlet(ClientV1RestServlet): class SAML2RestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/login/saml2") PATTERNS = client_path_patterns("/login/saml2", releases=())
def __init__(self, hs): def __init__(self, hs):
super(SAML2RestServlet, self).__init__(hs) super(SAML2RestServlet, self).__init__(hs)
@@ -174,6 +277,127 @@ class SAML2RestServlet(ClientV1RestServlet):
defer.returnValue((200, {"status": "not_authenticated"})) defer.returnValue((200, {"status": "not_authenticated"}))
# TODO Delete this after all CAS clients switch to token login instead
class CasRestServlet(ClientV1RestServlet):
PATTERNS = client_path_patterns("/login/cas", releases=())
def __init__(self, hs):
super(CasRestServlet, self).__init__(hs)
self.cas_server_url = hs.config.cas_server_url
def on_GET(self, request):
return (200, {"serverUrl": self.cas_server_url})
class CasRedirectServlet(ClientV1RestServlet):
PATTERNS = client_path_patterns("/login/cas/redirect", releases=())
def __init__(self, hs):
super(CasRedirectServlet, self).__init__(hs)
self.cas_server_url = hs.config.cas_server_url
self.cas_service_url = hs.config.cas_service_url
def on_GET(self, request):
args = request.args
if "redirectUrl" not in args:
return (400, "Redirect URL not specified for CAS auth")
client_redirect_url_param = urllib.urlencode({
"redirectUrl": args["redirectUrl"][0]
})
hs_redirect_url = self.cas_service_url + "/_matrix/client/api/v1/login/cas/ticket"
service_param = urllib.urlencode({
"service": "%s?%s" % (hs_redirect_url, client_redirect_url_param)
})
request.redirect("%s?%s" % (self.cas_server_url, service_param))
request.finish()
class CasTicketServlet(ClientV1RestServlet):
PATTERNS = client_path_patterns("/login/cas/ticket", releases=())
def __init__(self, hs):
super(CasTicketServlet, self).__init__(hs)
self.cas_server_url = hs.config.cas_server_url
self.cas_service_url = hs.config.cas_service_url
self.cas_required_attributes = hs.config.cas_required_attributes
@defer.inlineCallbacks
def on_GET(self, request):
client_redirect_url = request.args["redirectUrl"][0]
http_client = self.hs.get_simple_http_client()
uri = self.cas_server_url + "/proxyValidate"
args = {
"ticket": request.args["ticket"],
"service": self.cas_service_url
}
body = yield http_client.get_raw(uri, args)
result = yield self.handle_cas_response(request, body, client_redirect_url)
defer.returnValue(result)
@defer.inlineCallbacks
def handle_cas_response(self, request, cas_response_body, client_redirect_url):
user, attributes = self.parse_cas_response(cas_response_body)
for required_attribute, required_value in self.cas_required_attributes.items():
# If required attribute was not in CAS Response - Forbidden
if required_attribute not in attributes:
raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED)
# Also need to check value
if required_value is not None:
actual_value = attributes[required_attribute]
# If required attribute value does not match expected - Forbidden
if required_value != actual_value:
raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED)
user_id = UserID.create(user, self.hs.hostname).to_string()
auth_handler = self.handlers.auth_handler
user_exists = yield auth_handler.does_user_exist(user_id)
if not user_exists:
user_id, _ = (
yield self.handlers.registration_handler.register(localpart=user)
)
login_token = auth_handler.generate_short_term_login_token(user_id)
redirect_url = self.add_login_token_to_redirect_url(client_redirect_url,
login_token)
request.redirect(redirect_url)
request.finish()
def add_login_token_to_redirect_url(self, url, token):
url_parts = list(urlparse.urlparse(url))
query = dict(urlparse.parse_qsl(url_parts[4]))
query.update({"loginToken": token})
url_parts[4] = urllib.urlencode(query)
return urlparse.urlunparse(url_parts)
def parse_cas_response(self, cas_response_body):
root = ET.fromstring(cas_response_body)
if not root.tag.endswith("serviceResponse"):
raise LoginError(401, "Invalid CAS response", errcode=Codes.UNAUTHORIZED)
if not root[0].tag.endswith("authenticationSuccess"):
raise LoginError(401, "Unsuccessful CAS response", errcode=Codes.UNAUTHORIZED)
for child in root[0]:
if child.tag.endswith("user"):
user = child.text
if child.tag.endswith("attributes"):
attributes = {}
for attribute in child:
# ElementTree library expands the namespace in attribute tags
# to the full URL of the namespace.
# See (https://docs.python.org/2/library/xml.etree.elementtree.html)
# We don't care about namespace here and it will always be encased in
# curly braces, so we remove them.
if "}" in attribute.tag:
attributes[attribute.tag.split("}")[1]] = attribute.text
else:
attributes[attribute.tag] = attribute.text
if user is None or attributes is None:
raise LoginError(401, "Invalid CAS response", errcode=Codes.UNAUTHORIZED)
return (user, attributes)
def _parse_json(request): def _parse_json(request):
try: try:
content = json.loads(request.content.read()) content = json.loads(request.content.read())
@@ -188,4 +412,8 @@ def register_servlets(hs, http_server):
LoginRestServlet(hs).register(http_server) LoginRestServlet(hs).register(http_server)
if hs.config.saml2_enabled: if hs.config.saml2_enabled:
SAML2RestServlet(hs).register(http_server) SAML2RestServlet(hs).register(http_server)
if hs.config.cas_enabled:
CasRedirectServlet(hs).register(http_server)
CasTicketServlet(hs).register(http_server)
CasRestServlet(hs).register(http_server)
# TODO PasswordResetRestServlet(hs).register(http_server) # TODO PasswordResetRestServlet(hs).register(http_server)

View File

@@ -19,7 +19,7 @@ from twisted.internet import defer
from synapse.api.errors import SynapseError from synapse.api.errors import SynapseError
from synapse.types import UserID from synapse.types import UserID
from .base import ClientV1RestServlet, client_path_pattern from .base import ClientV1RestServlet, client_path_patterns
import simplejson as json import simplejson as json
import logging import logging
@@ -28,11 +28,11 @@ logger = logging.getLogger(__name__)
class PresenceStatusRestServlet(ClientV1RestServlet): class PresenceStatusRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/presence/(?P<user_id>[^/]*)/status") PATTERNS = client_path_patterns("/presence/(?P<user_id>[^/]*)/status")
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, user_id): def on_GET(self, request, user_id):
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
user = UserID.from_string(user_id) user = UserID.from_string(user_id)
state = yield self.handlers.presence_handler.get_state( state = yield self.handlers.presence_handler.get_state(
@@ -42,7 +42,7 @@ class PresenceStatusRestServlet(ClientV1RestServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_PUT(self, request, user_id): def on_PUT(self, request, user_id):
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
user = UserID.from_string(user_id) user = UserID.from_string(user_id)
state = {} state = {}
@@ -73,11 +73,11 @@ class PresenceStatusRestServlet(ClientV1RestServlet):
class PresenceListRestServlet(ClientV1RestServlet): class PresenceListRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/presence/list/(?P<user_id>[^/]*)") PATTERNS = client_path_patterns("/presence/list/(?P<user_id>[^/]*)")
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, user_id): def on_GET(self, request, user_id):
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
user = UserID.from_string(user_id) user = UserID.from_string(user_id)
if not self.hs.is_mine(user): if not self.hs.is_mine(user):
@@ -97,7 +97,7 @@ class PresenceListRestServlet(ClientV1RestServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request, user_id): def on_POST(self, request, user_id):
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
user = UserID.from_string(user_id) user = UserID.from_string(user_id)
if not self.hs.is_mine(user): if not self.hs.is_mine(user):
@@ -120,7 +120,7 @@ class PresenceListRestServlet(ClientV1RestServlet):
if len(u) == 0: if len(u) == 0:
continue continue
invited_user = UserID.from_string(u) invited_user = UserID.from_string(u)
yield self.handlers.presence_handler.send_invite( yield self.handlers.presence_handler.send_presence_invite(
observer_user=user, observed_user=invited_user observer_user=user, observed_user=invited_user
) )

View File

@@ -16,14 +16,14 @@
""" This module contains REST servlets to do with profile: /profile/<paths> """ """ This module contains REST servlets to do with profile: /profile/<paths> """
from twisted.internet import defer from twisted.internet import defer
from .base import ClientV1RestServlet, client_path_pattern from .base import ClientV1RestServlet, client_path_patterns
from synapse.types import UserID from synapse.types import UserID
import simplejson as json import simplejson as json
class ProfileDisplaynameRestServlet(ClientV1RestServlet): class ProfileDisplaynameRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/profile/(?P<user_id>[^/]*)/displayname") PATTERNS = client_path_patterns("/profile/(?P<user_id>[^/]*)/displayname")
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, user_id): def on_GET(self, request, user_id):
@@ -37,7 +37,7 @@ class ProfileDisplaynameRestServlet(ClientV1RestServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_PUT(self, request, user_id): def on_PUT(self, request, user_id):
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request, allow_guest=True)
user = UserID.from_string(user_id) user = UserID.from_string(user_id)
try: try:
@@ -56,7 +56,7 @@ class ProfileDisplaynameRestServlet(ClientV1RestServlet):
class ProfileAvatarURLRestServlet(ClientV1RestServlet): class ProfileAvatarURLRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/profile/(?P<user_id>[^/]*)/avatar_url") PATTERNS = client_path_patterns("/profile/(?P<user_id>[^/]*)/avatar_url")
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, user_id): def on_GET(self, request, user_id):
@@ -70,7 +70,7 @@ class ProfileAvatarURLRestServlet(ClientV1RestServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_PUT(self, request, user_id): def on_PUT(self, request, user_id):
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
user = UserID.from_string(user_id) user = UserID.from_string(user_id)
try: try:
@@ -89,7 +89,7 @@ class ProfileAvatarURLRestServlet(ClientV1RestServlet):
class ProfileRestServlet(ClientV1RestServlet): class ProfileRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/profile/(?P<user_id>[^/]*)") PATTERNS = client_path_patterns("/profile/(?P<user_id>[^/]*)")
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, user_id): def on_GET(self, request, user_id):

View File

@@ -18,7 +18,7 @@ from twisted.internet import defer
from synapse.api.errors import ( from synapse.api.errors import (
SynapseError, Codes, UnrecognizedRequestError, NotFoundError, StoreError SynapseError, Codes, UnrecognizedRequestError, NotFoundError, StoreError
) )
from .base import ClientV1RestServlet, client_path_pattern from .base import ClientV1RestServlet, client_path_patterns
from synapse.storage.push_rule import ( from synapse.storage.push_rule import (
InconsistentRuleException, RuleNotFoundException InconsistentRuleException, RuleNotFoundException
) )
@@ -31,7 +31,7 @@ import simplejson as json
class PushRuleRestServlet(ClientV1RestServlet): class PushRuleRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/pushrules/.*$") PATTERNS = client_path_patterns("/pushrules/.*$")
SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR = ( SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR = (
"Unrecognised request: You probably wanted a trailing slash") "Unrecognised request: You probably wanted a trailing slash")
@@ -43,7 +43,7 @@ class PushRuleRestServlet(ClientV1RestServlet):
except InvalidRuleException as e: except InvalidRuleException as e:
raise SynapseError(400, e.message) raise SynapseError(400, e.message)
user, _ = yield self.auth.get_user_by_req(request) user, _, _ = yield self.auth.get_user_by_req(request)
if '/' in spec['rule_id'] or '\\' in spec['rule_id']: if '/' in spec['rule_id'] or '\\' in spec['rule_id']:
raise SynapseError(400, "rule_id may not contain slashes") raise SynapseError(400, "rule_id may not contain slashes")
@@ -92,7 +92,7 @@ class PushRuleRestServlet(ClientV1RestServlet):
def on_DELETE(self, request): def on_DELETE(self, request):
spec = _rule_spec_from_path(request.postpath) spec = _rule_spec_from_path(request.postpath)
user, _ = yield self.auth.get_user_by_req(request) user, _, _ = yield self.auth.get_user_by_req(request)
namespaced_rule_id = _namespaced_rule_id_from_spec(spec) namespaced_rule_id = _namespaced_rule_id_from_spec(spec)
@@ -109,7 +109,7 @@ class PushRuleRestServlet(ClientV1RestServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request): def on_GET(self, request):
user, _ = yield self.auth.get_user_by_req(request) user, _, _ = yield self.auth.get_user_by_req(request)
# we build up the full structure and then decide which bits of it # we build up the full structure and then decide which bits of it
# to send which means doing unnecessary work sometimes but is # to send which means doing unnecessary work sometimes but is
@@ -207,7 +207,12 @@ class PushRuleRestServlet(ClientV1RestServlet):
def set_rule_attr(self, user_name, spec, val): def set_rule_attr(self, user_name, spec, val):
if spec['attr'] == 'enabled': if spec['attr'] == 'enabled':
if isinstance(val, dict) and "enabled" in val:
val = val["enabled"]
if not isinstance(val, bool): if not isinstance(val, bool):
# Legacy fallback
# This should *actually* take a dict, but many clients pass
# bools directly, so let's not break them.
raise SynapseError(400, "Value for 'enabled' must be boolean") raise SynapseError(400, "Value for 'enabled' must be boolean")
namespaced_rule_id = _namespaced_rule_id_from_spec(spec) namespaced_rule_id = _namespaced_rule_id_from_spec(spec)
self.hs.get_datastore().set_push_rule_enabled( self.hs.get_datastore().set_push_rule_enabled(

View File

@@ -17,17 +17,20 @@ from twisted.internet import defer
from synapse.api.errors import SynapseError, Codes from synapse.api.errors import SynapseError, Codes
from synapse.push import PusherConfigException from synapse.push import PusherConfigException
from .base import ClientV1RestServlet, client_path_pattern from .base import ClientV1RestServlet, client_path_patterns
import simplejson as json import simplejson as json
import logging
logger = logging.getLogger(__name__)
class PusherRestServlet(ClientV1RestServlet): class PusherRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/pushers/set$") PATTERNS = client_path_patterns("/pushers/set$")
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request): def on_POST(self, request):
user, token_id = yield self.auth.get_user_by_req(request) user, token_id, _ = yield self.auth.get_user_by_req(request)
content = _parse_json(request) content = _parse_json(request)
@@ -51,6 +54,9 @@ class PusherRestServlet(ClientV1RestServlet):
raise SynapseError(400, "Missing parameters: "+','.join(missing), raise SynapseError(400, "Missing parameters: "+','.join(missing),
errcode=Codes.MISSING_PARAM) errcode=Codes.MISSING_PARAM)
logger.debug("set pushkey %s to kind %s", content['pushkey'], content['kind'])
logger.debug("Got pushers request with body: %r", content)
append = False append = False
if 'append' in content: if 'append' in content:
append = content['append'] append = content['append']

View File

@@ -18,7 +18,7 @@ from twisted.internet import defer
from synapse.api.errors import SynapseError, Codes from synapse.api.errors import SynapseError, Codes
from synapse.api.constants import LoginType from synapse.api.constants import LoginType
from base import ClientV1RestServlet, client_path_pattern from base import ClientV1RestServlet, client_path_patterns
import synapse.util.stringutils as stringutils import synapse.util.stringutils as stringutils
from synapse.util.async import run_on_reactor from synapse.util.async import run_on_reactor
@@ -48,7 +48,7 @@ class RegisterRestServlet(ClientV1RestServlet):
handler doesn't have a concept of multi-stages or sessions. handler doesn't have a concept of multi-stages or sessions.
""" """
PATTERN = client_path_pattern("/register$") PATTERNS = client_path_patterns("/register$", releases=(), include_in_unstable=False)
def __init__(self, hs): def __init__(self, hs):
super(RegisterRestServlet, self).__init__(hs) super(RegisterRestServlet, self).__init__(hs)

View File

@@ -16,8 +16,8 @@
""" This module contains REST servlets to do with rooms: /rooms/<paths> """ """ This module contains REST servlets to do with rooms: /rooms/<paths> """
from twisted.internet import defer from twisted.internet import defer
from base import ClientV1RestServlet, client_path_pattern from base import ClientV1RestServlet, client_path_patterns
from synapse.api.errors import SynapseError, Codes from synapse.api.errors import SynapseError, Codes, AuthError
from synapse.streams.config import PaginationConfig from synapse.streams.config import PaginationConfig
from synapse.api.constants import EventTypes, Membership from synapse.api.constants import EventTypes, Membership
from synapse.types import UserID, RoomID, RoomAlias from synapse.types import UserID, RoomID, RoomAlias
@@ -27,7 +27,6 @@ import simplejson as json
import logging import logging
import urllib import urllib
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -35,16 +34,16 @@ class RoomCreateRestServlet(ClientV1RestServlet):
# No PATTERN; we have custom dispatch rules here # No PATTERN; we have custom dispatch rules here
def register(self, http_server): def register(self, http_server):
PATTERN = "/createRoom" PATTERNS = "/createRoom"
register_txn_path(self, PATTERN, http_server) register_txn_path(self, PATTERNS, http_server)
# define CORS for all of /rooms in RoomCreateRestServlet for simplicity # define CORS for all of /rooms in RoomCreateRestServlet for simplicity
http_server.register_path("OPTIONS", http_server.register_paths("OPTIONS",
client_path_pattern("/rooms(?:/.*)?$"), client_path_patterns("/rooms(?:/.*)?$"),
self.on_OPTIONS) self.on_OPTIONS)
# define CORS for /createRoom[/txnid] # define CORS for /createRoom[/txnid]
http_server.register_path("OPTIONS", http_server.register_paths("OPTIONS",
client_path_pattern("/createRoom(?:/.*)?$"), client_path_patterns("/createRoom(?:/.*)?$"),
self.on_OPTIONS) self.on_OPTIONS)
@defer.inlineCallbacks @defer.inlineCallbacks
def on_PUT(self, request, txn_id): def on_PUT(self, request, txn_id):
@@ -62,7 +61,7 @@ class RoomCreateRestServlet(ClientV1RestServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request): def on_POST(self, request):
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
room_config = self.get_room_config(request) room_config = self.get_room_config(request)
info = yield self.make_room(room_config, auth_user, None) info = yield self.make_room(room_config, auth_user, None)
@@ -104,18 +103,18 @@ class RoomStateEventRestServlet(ClientV1RestServlet):
state_key = ("/rooms/(?P<room_id>[^/]*)/state/" state_key = ("/rooms/(?P<room_id>[^/]*)/state/"
"(?P<event_type>[^/]*)/(?P<state_key>[^/]*)$") "(?P<event_type>[^/]*)/(?P<state_key>[^/]*)$")
http_server.register_path("GET", http_server.register_paths("GET",
client_path_pattern(state_key), client_path_patterns(state_key),
self.on_GET) self.on_GET)
http_server.register_path("PUT", http_server.register_paths("PUT",
client_path_pattern(state_key), client_path_patterns(state_key),
self.on_PUT) self.on_PUT)
http_server.register_path("GET", http_server.register_paths("GET",
client_path_pattern(no_state_key), client_path_patterns(no_state_key),
self.on_GET_no_state_key) self.on_GET_no_state_key)
http_server.register_path("PUT", http_server.register_paths("PUT",
client_path_pattern(no_state_key), client_path_patterns(no_state_key),
self.on_PUT_no_state_key) self.on_PUT_no_state_key)
def on_GET_no_state_key(self, request, room_id, event_type): def on_GET_no_state_key(self, request, room_id, event_type):
return self.on_GET(request, room_id, event_type, "") return self.on_GET(request, room_id, event_type, "")
@@ -125,7 +124,7 @@ class RoomStateEventRestServlet(ClientV1RestServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, room_id, event_type, state_key): def on_GET(self, request, room_id, event_type, state_key):
user, _ = yield self.auth.get_user_by_req(request) user, _, is_guest = yield self.auth.get_user_by_req(request, allow_guest=True)
msg_handler = self.handlers.message_handler msg_handler = self.handlers.message_handler
data = yield msg_handler.get_room_data( data = yield msg_handler.get_room_data(
@@ -133,6 +132,7 @@ class RoomStateEventRestServlet(ClientV1RestServlet):
room_id=room_id, room_id=room_id,
event_type=event_type, event_type=event_type,
state_key=state_key, state_key=state_key,
is_guest=is_guest,
) )
if not data: if not data:
@@ -143,7 +143,7 @@ class RoomStateEventRestServlet(ClientV1RestServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_PUT(self, request, room_id, event_type, state_key, txn_id=None): def on_PUT(self, request, room_id, event_type, state_key, txn_id=None):
user, token_id = yield self.auth.get_user_by_req(request) user, token_id, _ = yield self.auth.get_user_by_req(request)
content = _parse_json(request) content = _parse_json(request)
@@ -170,12 +170,12 @@ class RoomSendEventRestServlet(ClientV1RestServlet):
def register(self, http_server): def register(self, http_server):
# /rooms/$roomid/send/$event_type[/$txn_id] # /rooms/$roomid/send/$event_type[/$txn_id]
PATTERN = ("/rooms/(?P<room_id>[^/]*)/send/(?P<event_type>[^/]*)") PATTERNS = ("/rooms/(?P<room_id>[^/]*)/send/(?P<event_type>[^/]*)")
register_txn_path(self, PATTERN, http_server, with_get=True) register_txn_path(self, PATTERNS, http_server, with_get=True)
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request, room_id, event_type, txn_id=None): def on_POST(self, request, room_id, event_type, txn_id=None):
user, token_id = yield self.auth.get_user_by_req(request) user, token_id, _ = yield self.auth.get_user_by_req(request, allow_guest=True)
content = _parse_json(request) content = _parse_json(request)
msg_handler = self.handlers.message_handler msg_handler = self.handlers.message_handler
@@ -215,12 +215,15 @@ class JoinRoomAliasServlet(ClientV1RestServlet):
def register(self, http_server): def register(self, http_server):
# /join/$room_identifier[/$txn_id] # /join/$room_identifier[/$txn_id]
PATTERN = ("/join/(?P<room_identifier>[^/]*)") PATTERNS = ("/join/(?P<room_identifier>[^/]*)")
register_txn_path(self, PATTERN, http_server) register_txn_path(self, PATTERNS, http_server)
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request, room_identifier, txn_id=None): def on_POST(self, request, room_identifier, txn_id=None):
user, token_id = yield self.auth.get_user_by_req(request) user, token_id, is_guest = yield self.auth.get_user_by_req(
request,
allow_guest=True
)
# the identifier could be a room alias or a room id. Try one then the # the identifier could be a room alias or a room id. Try one then the
# other if it fails to parse, without swallowing other valid # other if it fails to parse, without swallowing other valid
@@ -242,16 +245,20 @@ class JoinRoomAliasServlet(ClientV1RestServlet):
defer.returnValue((200, ret_dict)) defer.returnValue((200, ret_dict))
else: # room id else: # room id
msg_handler = self.handlers.message_handler msg_handler = self.handlers.message_handler
content = {"membership": Membership.JOIN}
if is_guest:
content["kind"] = "guest"
yield msg_handler.create_and_send_event( yield msg_handler.create_and_send_event(
{ {
"type": EventTypes.Member, "type": EventTypes.Member,
"content": {"membership": Membership.JOIN}, "content": content,
"room_id": identifier.to_string(), "room_id": identifier.to_string(),
"sender": user.to_string(), "sender": user.to_string(),
"state_key": user.to_string(), "state_key": user.to_string(),
}, },
token_id=token_id, token_id=token_id,
txn_id=txn_id, txn_id=txn_id,
is_guest=is_guest,
) )
defer.returnValue((200, {"room_id": identifier.to_string()})) defer.returnValue((200, {"room_id": identifier.to_string()}))
@@ -273,7 +280,7 @@ class JoinRoomAliasServlet(ClientV1RestServlet):
# TODO: Needs unit testing # TODO: Needs unit testing
class PublicRoomListRestServlet(ClientV1RestServlet): class PublicRoomListRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/publicRooms$") PATTERNS = client_path_patterns("/publicRooms$")
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request): def on_GET(self, request):
@@ -284,18 +291,24 @@ class PublicRoomListRestServlet(ClientV1RestServlet):
# TODO: Needs unit testing # TODO: Needs unit testing
class RoomMemberListRestServlet(ClientV1RestServlet): class RoomMemberListRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/members$") PATTERNS = client_path_patterns("/rooms/(?P<room_id>[^/]*)/members$")
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, room_id): def on_GET(self, request, room_id):
# TODO support Pagination stream API (limit/tokens) # TODO support Pagination stream API (limit/tokens)
user, _ = yield self.auth.get_user_by_req(request) user, _, _ = yield self.auth.get_user_by_req(request)
handler = self.handlers.room_member_handler handler = self.handlers.message_handler
members = yield handler.get_room_members_as_pagination_chunk( events = yield handler.get_state_events(
room_id=room_id, room_id=room_id,
user_id=user.to_string()) user_id=user.to_string(),
)
for event in members["chunk"]: chunk = []
for event in events:
if event["type"] != EventTypes.Member:
continue
chunk.append(event)
# FIXME: should probably be state_key here, not user_id # FIXME: should probably be state_key here, not user_id
target_user = UserID.from_string(event["user_id"]) target_user = UserID.from_string(event["user_id"])
# Presence is an optional cache; don't fail if we can't fetch it # Presence is an optional cache; don't fail if we can't fetch it
@@ -308,27 +321,28 @@ class RoomMemberListRestServlet(ClientV1RestServlet):
except: except:
pass pass
defer.returnValue((200, members)) defer.returnValue((200, {
"chunk": chunk
}))
# TODO: Needs unit testing # TODO: Needs better unit testing
class RoomMessageListRestServlet(ClientV1RestServlet): class RoomMessageListRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/messages$") PATTERNS = client_path_patterns("/rooms/(?P<room_id>[^/]*)/messages$")
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, room_id): def on_GET(self, request, room_id):
user, _ = yield self.auth.get_user_by_req(request) user, _, is_guest = yield self.auth.get_user_by_req(request, allow_guest=True)
pagination_config = PaginationConfig.from_request( pagination_config = PaginationConfig.from_request(
request, default_limit=10, request, default_limit=10,
) )
with_feedback = "feedback" in request.args
as_client_event = "raw" not in request.args as_client_event = "raw" not in request.args
handler = self.handlers.message_handler handler = self.handlers.message_handler
msgs = yield handler.get_messages( msgs = yield handler.get_messages(
room_id=room_id, room_id=room_id,
user_id=user.to_string(), user_id=user.to_string(),
is_guest=is_guest,
pagin_config=pagination_config, pagin_config=pagination_config,
feedback=with_feedback,
as_client_event=as_client_event as_client_event=as_client_event
) )
@@ -337,58 +351,71 @@ class RoomMessageListRestServlet(ClientV1RestServlet):
# TODO: Needs unit testing # TODO: Needs unit testing
class RoomStateRestServlet(ClientV1RestServlet): class RoomStateRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/state$") PATTERNS = client_path_patterns("/rooms/(?P<room_id>[^/]*)/state$")
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, room_id): def on_GET(self, request, room_id):
user, _ = yield self.auth.get_user_by_req(request) user, _, is_guest = yield self.auth.get_user_by_req(request, allow_guest=True)
handler = self.handlers.message_handler handler = self.handlers.message_handler
# Get all the current state for this room # Get all the current state for this room
events = yield handler.get_state_events( events = yield handler.get_state_events(
room_id=room_id, room_id=room_id,
user_id=user.to_string(), user_id=user.to_string(),
is_guest=is_guest,
) )
defer.returnValue((200, events)) defer.returnValue((200, events))
# TODO: Needs unit testing # TODO: Needs unit testing
class RoomInitialSyncRestServlet(ClientV1RestServlet): class RoomInitialSyncRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/initialSync$") PATTERNS = client_path_patterns("/rooms/(?P<room_id>[^/]*)/initialSync$")
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, room_id): def on_GET(self, request, room_id):
user, _ = yield self.auth.get_user_by_req(request) user, _, is_guest = yield self.auth.get_user_by_req(request, allow_guest=True)
pagination_config = PaginationConfig.from_request(request) pagination_config = PaginationConfig.from_request(request)
content = yield self.handlers.message_handler.room_initial_sync( content = yield self.handlers.message_handler.room_initial_sync(
room_id=room_id, room_id=room_id,
user_id=user.to_string(), user_id=user.to_string(),
pagin_config=pagination_config, pagin_config=pagination_config,
is_guest=is_guest,
) )
defer.returnValue((200, content)) defer.returnValue((200, content))
class RoomTriggerBackfill(ClientV1RestServlet): class RoomEventContext(ClientV1RestServlet):
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/backfill$") PATTERNS = client_path_patterns(
"/rooms/(?P<room_id>[^/]*)/context/(?P<event_id>[^/]*)$"
)
def __init__(self, hs): def __init__(self, hs):
super(RoomTriggerBackfill, self).__init__(hs) super(RoomEventContext, self).__init__(hs)
self.clock = hs.get_clock() self.clock = hs.get_clock()
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, room_id): def on_GET(self, request, room_id, event_id):
remote_server = urllib.unquote( user, _, is_guest = yield self.auth.get_user_by_req(request, allow_guest=True)
request.args["remote"][0]
).decode("UTF-8")
limit = int(request.args["limit"][0]) limit = int(request.args.get("limit", [10])[0])
handler = self.handlers.federation_handler results = yield self.handlers.room_context_handler.get_event_context(
events = yield handler.backfill(remote_server, room_id, limit) user, room_id, event_id, limit, is_guest
)
time_now = self.clock.time_msec() time_now = self.clock.time_msec()
results["events_before"] = [
serialize_event(event, time_now) for event in results["events_before"]
]
results["events_after"] = [
serialize_event(event, time_now) for event in results["events_after"]
]
results["state"] = [
serialize_event(event, time_now) for event in results["state"]
]
res = [serialize_event(event, time_now) for event in events] logger.info("Responding with %r", results)
defer.returnValue((200, res))
defer.returnValue((200, results))
# TODO: Needs unit testing # TODO: Needs unit testing
@@ -396,43 +423,83 @@ class RoomMembershipRestServlet(ClientV1RestServlet):
def register(self, http_server): def register(self, http_server):
# /rooms/$roomid/[invite|join|leave] # /rooms/$roomid/[invite|join|leave]
PATTERN = ("/rooms/(?P<room_id>[^/]*)/" PATTERNS = ("/rooms/(?P<room_id>[^/]*)/"
"(?P<membership_action>join|invite|leave|ban|kick)") "(?P<membership_action>join|invite|leave|ban|kick|forget)")
register_txn_path(self, PATTERN, http_server) register_txn_path(self, PATTERNS, http_server)
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request, room_id, membership_action, txn_id=None): def on_POST(self, request, room_id, membership_action, txn_id=None):
user, token_id = yield self.auth.get_user_by_req(request) user, token_id, is_guest = yield self.auth.get_user_by_req(
request,
allow_guest=True
)
effective_membership_action = membership_action
if is_guest and membership_action not in {Membership.JOIN, Membership.LEAVE}:
raise AuthError(403, "Guest access not allowed")
content = _parse_json(request) content = _parse_json(request)
# target user is you unless it is an invite # target user is you unless it is an invite
state_key = user.to_string() state_key = user.to_string()
if membership_action in ["invite", "ban", "kick"]:
if "user_id" not in content: if membership_action == "invite" and self._has_3pid_invite_keys(content):
yield self.handlers.room_member_handler.do_3pid_invite(
room_id,
user,
content["medium"],
content["address"],
content["id_server"],
token_id,
txn_id
)
defer.returnValue((200, {}))
return
elif membership_action in ["invite", "ban", "kick"]:
if "user_id" in content:
state_key = content["user_id"]
else:
raise SynapseError(400, "Missing user_id key.") raise SynapseError(400, "Missing user_id key.")
state_key = content["user_id"]
# make sure it looks like a user ID; it'll throw if it's invalid. # make sure it looks like a user ID; it'll throw if it's invalid.
UserID.from_string(state_key) UserID.from_string(state_key)
if membership_action == "kick": if membership_action == "kick":
membership_action = "leave" effective_membership_action = "leave"
elif membership_action == "forget":
effective_membership_action = "leave"
msg_handler = self.handlers.message_handler msg_handler = self.handlers.message_handler
content = {"membership": unicode(effective_membership_action)}
if is_guest:
content["kind"] = "guest"
yield msg_handler.create_and_send_event( yield msg_handler.create_and_send_event(
{ {
"type": EventTypes.Member, "type": EventTypes.Member,
"content": {"membership": unicode(membership_action)}, "content": content,
"room_id": room_id, "room_id": room_id,
"sender": user.to_string(), "sender": user.to_string(),
"state_key": state_key, "state_key": state_key,
}, },
token_id=token_id, token_id=token_id,
txn_id=txn_id, txn_id=txn_id,
is_guest=is_guest,
) )
if membership_action == "forget":
yield self.handlers.room_member_handler.forget(user, room_id)
defer.returnValue((200, {})) defer.returnValue((200, {}))
def _has_3pid_invite_keys(self, content):
for key in {"id_server", "medium", "address"}:
if key not in content:
return False
return True
@defer.inlineCallbacks @defer.inlineCallbacks
def on_PUT(self, request, room_id, membership_action, txn_id): def on_PUT(self, request, room_id, membership_action, txn_id):
try: try:
@@ -452,12 +519,12 @@ class RoomMembershipRestServlet(ClientV1RestServlet):
class RoomRedactEventRestServlet(ClientV1RestServlet): class RoomRedactEventRestServlet(ClientV1RestServlet):
def register(self, http_server): def register(self, http_server):
PATTERN = ("/rooms/(?P<room_id>[^/]*)/redact/(?P<event_id>[^/]*)") PATTERNS = ("/rooms/(?P<room_id>[^/]*)/redact/(?P<event_id>[^/]*)")
register_txn_path(self, PATTERN, http_server) register_txn_path(self, PATTERNS, http_server)
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request, room_id, event_id, txn_id=None): def on_POST(self, request, room_id, event_id, txn_id=None):
user, token_id = yield self.auth.get_user_by_req(request) user, token_id, _ = yield self.auth.get_user_by_req(request)
content = _parse_json(request) content = _parse_json(request)
msg_handler = self.handlers.message_handler msg_handler = self.handlers.message_handler
@@ -491,13 +558,13 @@ class RoomRedactEventRestServlet(ClientV1RestServlet):
class RoomTypingRestServlet(ClientV1RestServlet): class RoomTypingRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern( PATTERNS = client_path_patterns(
"/rooms/(?P<room_id>[^/]*)/typing/(?P<user_id>[^/]*)$" "/rooms/(?P<room_id>[^/]*)/typing/(?P<user_id>[^/]*)$"
) )
@defer.inlineCallbacks @defer.inlineCallbacks
def on_PUT(self, request, room_id, user_id): def on_PUT(self, request, room_id, user_id):
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
room_id = urllib.unquote(room_id) room_id = urllib.unquote(room_id)
target_user = UserID.from_string(urllib.unquote(user_id)) target_user = UserID.from_string(urllib.unquote(user_id))
@@ -523,6 +590,23 @@ class RoomTypingRestServlet(ClientV1RestServlet):
defer.returnValue((200, {})) defer.returnValue((200, {}))
class SearchRestServlet(ClientV1RestServlet):
PATTERNS = client_path_patterns(
"/search$"
)
@defer.inlineCallbacks
def on_POST(self, request):
auth_user, _, _ = yield self.auth.get_user_by_req(request)
content = _parse_json(request)
batch = request.args.get("next_batch", [None])[0]
results = yield self.handlers.search_handler.search(auth_user, content, batch)
defer.returnValue((200, results))
def _parse_json(request): def _parse_json(request):
try: try:
content = json.loads(request.content.read()) content = json.loads(request.content.read())
@@ -547,20 +631,20 @@ def register_txn_path(servlet, regex_string, http_server, with_get=False):
http_server : The http_server to register paths with. http_server : The http_server to register paths with.
with_get: True to also register respective GET paths for the PUTs. with_get: True to also register respective GET paths for the PUTs.
""" """
http_server.register_path( http_server.register_paths(
"POST", "POST",
client_path_pattern(regex_string + "$"), client_path_patterns(regex_string + "$"),
servlet.on_POST servlet.on_POST
) )
http_server.register_path( http_server.register_paths(
"PUT", "PUT",
client_path_pattern(regex_string + "/(?P<txn_id>[^/]*)$"), client_path_patterns(regex_string + "/(?P<txn_id>[^/]*)$"),
servlet.on_PUT servlet.on_PUT
) )
if with_get: if with_get:
http_server.register_path( http_server.register_paths(
"GET", "GET",
client_path_pattern(regex_string + "/(?P<txn_id>[^/]*)$"), client_path_patterns(regex_string + "/(?P<txn_id>[^/]*)$"),
servlet.on_GET servlet.on_GET
) )
@@ -571,7 +655,6 @@ def register_servlets(hs, http_server):
RoomMemberListRestServlet(hs).register(http_server) RoomMemberListRestServlet(hs).register(http_server)
RoomMessageListRestServlet(hs).register(http_server) RoomMessageListRestServlet(hs).register(http_server)
JoinRoomAliasServlet(hs).register(http_server) JoinRoomAliasServlet(hs).register(http_server)
RoomTriggerBackfill(hs).register(http_server)
RoomMembershipRestServlet(hs).register(http_server) RoomMembershipRestServlet(hs).register(http_server)
RoomSendEventRestServlet(hs).register(http_server) RoomSendEventRestServlet(hs).register(http_server)
PublicRoomListRestServlet(hs).register(http_server) PublicRoomListRestServlet(hs).register(http_server)
@@ -579,3 +662,5 @@ def register_servlets(hs, http_server):
RoomInitialSyncRestServlet(hs).register(http_server) RoomInitialSyncRestServlet(hs).register(http_server)
RoomRedactEventRestServlet(hs).register(http_server) RoomRedactEventRestServlet(hs).register(http_server)
RoomTypingRestServlet(hs).register(http_server) RoomTypingRestServlet(hs).register(http_server)
SearchRestServlet(hs).register(http_server)
RoomEventContext(hs).register(http_server)

View File

@@ -15,7 +15,7 @@
from twisted.internet import defer from twisted.internet import defer
from base import ClientV1RestServlet, client_path_pattern from base import ClientV1RestServlet, client_path_patterns
import hmac import hmac
@@ -24,11 +24,11 @@ import base64
class VoipRestServlet(ClientV1RestServlet): class VoipRestServlet(ClientV1RestServlet):
PATTERN = client_path_pattern("/voip/turnServer$") PATTERNS = client_path_patterns("/voip/turnServer$")
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request): def on_GET(self, request):
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
turnUris = self.hs.config.turn_uris turnUris = self.hs.config.turn_uris
turnSecret = self.hs.config.turn_shared_secret turnSecret = self.hs.config.turn_shared_secret

View File

@@ -12,35 +12,3 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# 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 (
sync,
filter,
account,
register,
auth,
receipts,
keys,
tokenrefresh,
)
from synapse.http.server import JsonResource
class ClientV2AlphaRestResource(JsonResource):
"""A resource for version 2 alpha of the matrix client API."""
def __init__(self, hs):
JsonResource.__init__(self, hs, canonical_json=False)
self.register_servlets(self, hs)
@staticmethod
def register_servlets(client_resource, hs):
sync.register_servlets(hs, client_resource)
filter.register_servlets(hs, client_resource)
account.register_servlets(hs, client_resource)
register.register_servlets(hs, client_resource)
auth.register_servlets(hs, client_resource)
receipts.register_servlets(hs, client_resource)
keys.register_servlets(hs, client_resource)
tokenrefresh.register_servlets(hs, client_resource)

View File

@@ -27,7 +27,7 @@ import simplejson
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def client_v2_pattern(path_regex): def client_v2_patterns(path_regex, releases=(0,)):
"""Creates a regex compiled client path with the correct client path """Creates a regex compiled client path with the correct client path
prefix. prefix.
@@ -37,7 +37,13 @@ def client_v2_pattern(path_regex):
Returns: Returns:
SRE_Pattern SRE_Pattern
""" """
return re.compile("^" + CLIENT_V2_ALPHA_PREFIX + path_regex) patterns = [re.compile("^" + CLIENT_V2_ALPHA_PREFIX + path_regex)]
unstable_prefix = CLIENT_V2_ALPHA_PREFIX.replace("/v2_alpha", "/unstable")
patterns.append(re.compile("^" + unstable_prefix + path_regex))
for release in releases:
new_prefix = CLIENT_V2_ALPHA_PREFIX.replace("/v2_alpha", "/r%d" % release)
patterns.append(re.compile("^" + new_prefix + path_regex))
return patterns
def parse_request_allow_empty(request): def parse_request_allow_empty(request):

View File

@@ -20,7 +20,7 @@ from synapse.api.errors import LoginError, SynapseError, Codes
from synapse.http.servlet import RestServlet from synapse.http.servlet import RestServlet
from synapse.util.async import run_on_reactor from synapse.util.async import run_on_reactor
from ._base import client_v2_pattern, parse_json_dict_from_request from ._base import client_v2_patterns, parse_json_dict_from_request
import logging import logging
@@ -29,7 +29,7 @@ logger = logging.getLogger(__name__)
class PasswordRestServlet(RestServlet): class PasswordRestServlet(RestServlet):
PATTERN = client_v2_pattern("/account/password") PATTERNS = client_v2_patterns("/account/password")
def __init__(self, hs): def __init__(self, hs):
super(PasswordRestServlet, self).__init__() super(PasswordRestServlet, self).__init__()
@@ -55,7 +55,7 @@ class PasswordRestServlet(RestServlet):
if LoginType.PASSWORD in result: if LoginType.PASSWORD in result:
# if using password, they should also be logged in # if using password, they should also be logged in
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
if auth_user.to_string() != result[LoginType.PASSWORD]: if auth_user.to_string() != result[LoginType.PASSWORD]:
raise LoginError(400, "", Codes.UNKNOWN) raise LoginError(400, "", Codes.UNKNOWN)
user_id = auth_user.to_string() user_id = auth_user.to_string()
@@ -89,7 +89,7 @@ class PasswordRestServlet(RestServlet):
class ThreepidRestServlet(RestServlet): class ThreepidRestServlet(RestServlet):
PATTERN = client_v2_pattern("/account/3pid") PATTERNS = client_v2_patterns("/account/3pid")
def __init__(self, hs): def __init__(self, hs):
super(ThreepidRestServlet, self).__init__() super(ThreepidRestServlet, self).__init__()
@@ -102,7 +102,7 @@ class ThreepidRestServlet(RestServlet):
def on_GET(self, request): def on_GET(self, request):
yield run_on_reactor() yield run_on_reactor()
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
threepids = yield self.hs.get_datastore().user_get_threepids( threepids = yield self.hs.get_datastore().user_get_threepids(
auth_user.to_string() auth_user.to_string()
@@ -120,7 +120,7 @@ class ThreepidRestServlet(RestServlet):
raise SynapseError(400, "Missing param", Codes.MISSING_PARAM) raise SynapseError(400, "Missing param", Codes.MISSING_PARAM)
threePidCreds = body['threePidCreds'] threePidCreds = body['threePidCreds']
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
threepid = yield self.identity_handler.threepid_from_creds(threePidCreds) threepid = yield self.identity_handler.threepid_from_creds(threePidCreds)

View File

@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
# Copyright 2015 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 ._base import client_v2_patterns
from synapse.http.servlet import RestServlet
from synapse.api.errors import AuthError, SynapseError
from twisted.internet import defer
import logging
import simplejson as json
logger = logging.getLogger(__name__)
class AccountDataServlet(RestServlet):
"""
PUT /user/{user_id}/account_data/{account_dataType} HTTP/1.1
"""
PATTERNS = client_v2_patterns(
"/user/(?P<user_id>[^/]*)/account_data/(?P<account_data_type>[^/]*)"
)
def __init__(self, hs):
super(AccountDataServlet, self).__init__()
self.auth = hs.get_auth()
self.store = hs.get_datastore()
self.notifier = hs.get_notifier()
@defer.inlineCallbacks
def on_PUT(self, request, user_id, account_data_type):
auth_user, _, _ = yield self.auth.get_user_by_req(request)
if user_id != auth_user.to_string():
raise AuthError(403, "Cannot add account data for other users.")
try:
content_bytes = request.content.read()
body = json.loads(content_bytes)
except:
raise SynapseError(400, "Invalid JSON")
max_id = yield self.store.add_account_data_for_user(
user_id, account_data_type, body
)
yield self.notifier.on_new_event(
"account_data_key", max_id, users=[user_id]
)
defer.returnValue((200, {}))
class RoomAccountDataServlet(RestServlet):
"""
PUT /user/{user_id}/rooms/{room_id}/account_data/{account_dataType} HTTP/1.1
"""
PATTERNS = client_v2_patterns(
"/user/(?P<user_id>[^/]*)"
"/rooms/(?P<room_id>[^/]*)"
"/account_data/(?P<account_data_type>[^/]*)"
)
def __init__(self, hs):
super(RoomAccountDataServlet, self).__init__()
self.auth = hs.get_auth()
self.store = hs.get_datastore()
self.notifier = hs.get_notifier()
@defer.inlineCallbacks
def on_PUT(self, request, user_id, room_id, account_data_type):
auth_user, _, _ = yield self.auth.get_user_by_req(request)
if user_id != auth_user.to_string():
raise AuthError(403, "Cannot add account data for other users.")
try:
content_bytes = request.content.read()
body = json.loads(content_bytes)
except:
raise SynapseError(400, "Invalid JSON")
if not isinstance(body, dict):
raise ValueError("Expected a JSON object")
max_id = yield self.store.add_account_data_to_room(
user_id, room_id, account_data_type, body
)
yield self.notifier.on_new_event(
"account_data_key", max_id, users=[user_id]
)
defer.returnValue((200, {}))
def register_servlets(hs, http_server):
AccountDataServlet(hs).register(http_server)
RoomAccountDataServlet(hs).register(http_server)

View File

@@ -20,7 +20,7 @@ from synapse.api.errors import SynapseError
from synapse.api.urls import CLIENT_V2_ALPHA_PREFIX from synapse.api.urls import CLIENT_V2_ALPHA_PREFIX
from synapse.http.servlet import RestServlet from synapse.http.servlet import RestServlet
from ._base import client_v2_pattern from ._base import client_v2_patterns
import logging import logging
@@ -97,7 +97,7 @@ class AuthRestServlet(RestServlet):
cannot be handled in the normal flow (with requests to the same endpoint). cannot be handled in the normal flow (with requests to the same endpoint).
Current use is for web fallback auth. Current use is for web fallback auth.
""" """
PATTERN = client_v2_pattern("/auth/(?P<stagetype>[\w\.]*)/fallback/web") PATTERNS = client_v2_patterns("/auth/(?P<stagetype>[\w\.]*)/fallback/web")
def __init__(self, hs): def __init__(self, hs):
super(AuthRestServlet, self).__init__() super(AuthRestServlet, self).__init__()

View File

@@ -19,7 +19,7 @@ from synapse.api.errors import AuthError, SynapseError
from synapse.http.servlet import RestServlet from synapse.http.servlet import RestServlet
from synapse.types import UserID from synapse.types import UserID
from ._base import client_v2_pattern from ._base import client_v2_patterns
import simplejson as json import simplejson as json
import logging import logging
@@ -29,7 +29,7 @@ logger = logging.getLogger(__name__)
class GetFilterRestServlet(RestServlet): class GetFilterRestServlet(RestServlet):
PATTERN = client_v2_pattern("/user/(?P<user_id>[^/]*)/filter/(?P<filter_id>[^/]*)") PATTERNS = client_v2_patterns("/user/(?P<user_id>[^/]*)/filter/(?P<filter_id>[^/]*)")
def __init__(self, hs): def __init__(self, hs):
super(GetFilterRestServlet, self).__init__() super(GetFilterRestServlet, self).__init__()
@@ -40,7 +40,7 @@ class GetFilterRestServlet(RestServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, user_id, filter_id): def on_GET(self, request, user_id, filter_id):
target_user = UserID.from_string(user_id) target_user = UserID.from_string(user_id)
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
if target_user != auth_user: if target_user != auth_user:
raise AuthError(403, "Cannot get filters for other users") raise AuthError(403, "Cannot get filters for other users")
@@ -65,7 +65,7 @@ class GetFilterRestServlet(RestServlet):
class CreateFilterRestServlet(RestServlet): class CreateFilterRestServlet(RestServlet):
PATTERN = client_v2_pattern("/user/(?P<user_id>[^/]*)/filter") PATTERNS = client_v2_patterns("/user/(?P<user_id>[^/]*)/filter")
def __init__(self, hs): def __init__(self, hs):
super(CreateFilterRestServlet, self).__init__() super(CreateFilterRestServlet, self).__init__()
@@ -76,7 +76,7 @@ class CreateFilterRestServlet(RestServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request, user_id): def on_POST(self, request, user_id):
target_user = UserID.from_string(user_id) target_user = UserID.from_string(user_id)
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
if target_user != auth_user: if target_user != auth_user:
raise AuthError(403, "Cannot create filters for other users") raise AuthError(403, "Cannot create filters for other users")

View File

@@ -21,7 +21,7 @@ from synapse.types import UserID
from canonicaljson import encode_canonical_json from canonicaljson import encode_canonical_json
from ._base import client_v2_pattern from ._base import client_v2_patterns
import simplejson as json import simplejson as json
import logging import logging
@@ -54,7 +54,7 @@ class KeyUploadServlet(RestServlet):
}, },
} }
""" """
PATTERN = client_v2_pattern("/keys/upload/(?P<device_id>[^/]*)") PATTERNS = client_v2_patterns("/keys/upload/(?P<device_id>[^/]*)", releases=())
def __init__(self, hs): def __init__(self, hs):
super(KeyUploadServlet, self).__init__() super(KeyUploadServlet, self).__init__()
@@ -64,7 +64,7 @@ class KeyUploadServlet(RestServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request, device_id): def on_POST(self, request, device_id):
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
user_id = auth_user.to_string() user_id = auth_user.to_string()
# TODO: Check that the device_id matches that in the authentication # TODO: Check that the device_id matches that in the authentication
# or derive the device_id from the authentication instead. # or derive the device_id from the authentication instead.
@@ -109,7 +109,7 @@ class KeyUploadServlet(RestServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, device_id): def on_GET(self, request, device_id):
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
user_id = auth_user.to_string() user_id = auth_user.to_string()
result = yield self.store.count_e2e_one_time_keys(user_id, device_id) result = yield self.store.count_e2e_one_time_keys(user_id, device_id)
@@ -154,12 +154,13 @@ class KeyQueryServlet(RestServlet):
} } } } } } } } } } } }
""" """
PATTERN = client_v2_pattern( PATTERNS = client_v2_patterns(
"/keys/query(?:" "/keys/query(?:"
"/(?P<user_id>[^/]*)(?:" "/(?P<user_id>[^/]*)(?:"
"/(?P<device_id>[^/]*)" "/(?P<device_id>[^/]*)"
")?" ")?"
")?" ")?",
releases=()
) )
def __init__(self, hs): def __init__(self, hs):
@@ -181,7 +182,7 @@ class KeyQueryServlet(RestServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request, user_id, device_id): def on_GET(self, request, user_id, device_id):
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
auth_user_id = auth_user.to_string() auth_user_id = auth_user.to_string()
user_id = user_id if user_id else auth_user_id user_id = user_id if user_id else auth_user_id
device_ids = [device_id] if device_id else [] device_ids = [device_id] if device_id else []
@@ -245,10 +246,11 @@ class OneTimeKeyServlet(RestServlet):
} } } } } } } }
""" """
PATTERN = client_v2_pattern( PATTERNS = client_v2_patterns(
"/keys/claim(?:/?|(?:/" "/keys/claim(?:/?|(?:/"
"(?P<user_id>[^/]*)/(?P<device_id>[^/]*)/(?P<algorithm>[^/]*)" "(?P<user_id>[^/]*)/(?P<device_id>[^/]*)/(?P<algorithm>[^/]*)"
")?)" ")?)",
releases=()
) )
def __init__(self, hs): def __init__(self, hs):

View File

@@ -15,8 +15,9 @@
from twisted.internet import defer from twisted.internet import defer
from synapse.api.errors import SynapseError
from synapse.http.servlet import RestServlet from synapse.http.servlet import RestServlet
from ._base import client_v2_pattern from ._base import client_v2_patterns
import logging import logging
@@ -25,7 +26,7 @@ logger = logging.getLogger(__name__)
class ReceiptRestServlet(RestServlet): class ReceiptRestServlet(RestServlet):
PATTERN = client_v2_pattern( PATTERNS = client_v2_patterns(
"/rooms/(?P<room_id>[^/]*)" "/rooms/(?P<room_id>[^/]*)"
"/receipt/(?P<receipt_type>[^/]*)" "/receipt/(?P<receipt_type>[^/]*)"
"/(?P<event_id>[^/]*)$" "/(?P<event_id>[^/]*)$"
@@ -39,7 +40,10 @@ class ReceiptRestServlet(RestServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request, room_id, receipt_type, event_id): def on_POST(self, request, room_id, receipt_type, event_id):
user, _ = yield self.auth.get_user_by_req(request) user, _, _ = yield self.auth.get_user_by_req(request)
if receipt_type != "m.read":
raise SynapseError(400, "Receipt type must be 'm.read'")
yield self.receipts_handler.received_client_receipt( yield self.receipts_handler.received_client_receipt(
room_id, room_id,

View File

@@ -16,10 +16,10 @@
from twisted.internet import defer from twisted.internet import defer
from synapse.api.constants import LoginType from synapse.api.constants import LoginType
from synapse.api.errors import SynapseError, Codes from synapse.api.errors import SynapseError, Codes, UnrecognizedRequestError
from synapse.http.servlet import RestServlet from synapse.http.servlet import RestServlet
from ._base import client_v2_pattern, parse_json_dict_from_request from ._base import client_v2_patterns, parse_json_dict_from_request
import logging import logging
import hmac import hmac
@@ -41,7 +41,7 @@ logger = logging.getLogger(__name__)
class RegisterRestServlet(RestServlet): class RegisterRestServlet(RestServlet):
PATTERN = client_v2_pattern("/register") PATTERNS = client_v2_patterns("/register")
def __init__(self, hs): def __init__(self, hs):
super(RegisterRestServlet, self).__init__() super(RegisterRestServlet, self).__init__()
@@ -55,6 +55,19 @@ class RegisterRestServlet(RestServlet):
def on_POST(self, request): def on_POST(self, request):
yield run_on_reactor() yield run_on_reactor()
kind = "user"
if "kind" in request.args:
kind = request.args["kind"][0]
if kind == "guest":
ret = yield self._do_guest_registration()
defer.returnValue(ret)
return
elif kind != "user":
raise UnrecognizedRequestError(
"Do not understand membership kind: %s" % (kind,)
)
if '/register/email/requestToken' in request.path: if '/register/email/requestToken' in request.path:
ret = yield self.onEmailTokenRequest(request) ret = yield self.onEmailTokenRequest(request)
defer.returnValue(ret) defer.returnValue(ret)
@@ -236,6 +249,18 @@ class RegisterRestServlet(RestServlet):
ret = yield self.identity_handler.requestEmailToken(**body) ret = yield self.identity_handler.requestEmailToken(**body)
defer.returnValue((200, ret)) defer.returnValue((200, ret))
@defer.inlineCallbacks
def _do_guest_registration(self):
if not self.hs.config.allow_guest_access:
defer.returnValue((403, "Guest access is disabled"))
user_id, _ = yield self.registration_handler.register(generate_token=False)
access_token = self.auth_handler.generate_access_token(user_id, ["guest = true"])
defer.returnValue((200, {
"user_id": user_id,
"access_token": access_token,
"home_server": self.hs.hostname,
}))
def register_servlets(hs, http_server): def register_servlets(hs, http_server):
RegisterRestServlet(hs).register(http_server) RegisterRestServlet(hs).register(http_server)

View File

@@ -20,14 +20,19 @@ from synapse.http.servlet import (
) )
from synapse.handlers.sync import SyncConfig from synapse.handlers.sync import SyncConfig
from synapse.types import StreamToken from synapse.types import StreamToken
from synapse.events import FrozenEvent
from synapse.events.utils import ( from synapse.events.utils import (
serialize_event, format_event_for_client_v2_without_event_id, serialize_event, format_event_for_client_v2_without_room_id,
) )
from synapse.api.filtering import Filter from synapse.api.filtering import FilterCollection
from ._base import client_v2_pattern from synapse.api.errors import SynapseError
from ._base import client_v2_patterns
import copy
import logging import logging
import ujson as json
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -36,99 +41,93 @@ class SyncRestServlet(RestServlet):
GET parameters:: GET parameters::
timeout(int): How long to wait for new events in milliseconds. timeout(int): How long to wait for new events in milliseconds.
limit(int): Maxiumum number of events per room to return.
gap(bool): Create gaps the message history if limit is exceeded to
ensure that the client has the most recent messages. Defaults to
"true".
sort(str,str): tuple of sort key (e.g. "timeline") and direction
(e.g. "asc", "desc"). Defaults to "timeline,asc".
since(batch_token): Batch token when asking for incremental deltas. since(batch_token): Batch token when asking for incremental deltas.
set_presence(str): What state the device presence should be set to. set_presence(str): What state the device presence should be set to.
default is "online". default is "online".
backfill(bool): Should the HS request message history from other
servers. This may take a long time making it unsuitable for clients
expecting a prompt response. Defaults to "true".
filter(filter_id): A filter to apply to the events returned. filter(filter_id): A filter to apply to the events returned.
filter_*: Filter override parameters.
Response JSON:: Response JSON::
{ {
"next_batch": // batch token for the next /sync "next_batch": // batch token for the next /sync
"private_user_data": // private events for this user. "presence": // presence data for the user.
"public_user_data": // public events for all users including the "rooms": {
// public events for this user. "join": { // Joined rooms being updated.
"rooms": [{ // List of rooms with updates. "${room_id}": { // Id of the room being updated
"room_id": // Id of the room being updated
"limited": // Was the per-room event limit exceeded?
"published": // Is the room published by our HS?
"event_map": // Map of EventID -> event JSON. "event_map": // Map of EventID -> event JSON.
"events": { // The recent events in the room if gap is "true" "timeline": { // The recent events in the room if gap is "true"
// otherwise the next events in the room. "limited": // Was the per-room event limit exceeded?
"batch": [] // list of EventIDs in the "event_map". // otherwise the next events in the room.
"prev_batch": // back token for getting previous events. "events": [] // list of EventIDs in the "event_map".
"prev_batch": // back token for getting previous events.
} }
"state": [] // list of EventIDs updating the current state to "state": {"events": []} // list of EventIDs updating the
// be what it should be at the end of the batch. // current state to be what it should
"ephemeral": [] // be at the end of the batch.
}] "ephemeral": {"events": []} // list of event objects
}
},
"invite": {}, // Invited rooms being updated.
"leave": {} // Archived rooms being updated.
}
} }
""" """
PATTERN = client_v2_pattern("/sync$") PATTERNS = client_v2_patterns("/sync$")
ALLOWED_SORT = set(["timeline,asc", "timeline,desc"]) ALLOWED_PRESENCE = set(["online", "offline"])
ALLOWED_PRESENCE = set(["online", "offline", "idle"])
def __init__(self, hs): def __init__(self, hs):
super(SyncRestServlet, self).__init__() super(SyncRestServlet, self).__init__()
self.auth = hs.get_auth() self.auth = hs.get_auth()
self.event_stream_handler = hs.get_handlers().event_stream_handler
self.sync_handler = hs.get_handlers().sync_handler self.sync_handler = hs.get_handlers().sync_handler
self.clock = hs.get_clock() self.clock = hs.get_clock()
self.filtering = hs.get_filtering() self.filtering = hs.get_filtering()
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request): def on_GET(self, request):
user, token_id = yield self.auth.get_user_by_req(request) user, token_id, is_guest = yield self.auth.get_user_by_req(
request, allow_guest=True
)
timeout = parse_integer(request, "timeout", default=0) timeout = parse_integer(request, "timeout", default=0)
limit = parse_integer(request, "limit", required=True)
gap = parse_boolean(request, "gap", default=True)
sort = parse_string(
request, "sort", default="timeline,asc",
allowed_values=self.ALLOWED_SORT
)
since = parse_string(request, "since") since = parse_string(request, "since")
set_presence = parse_string( set_presence = parse_string(
request, "set_presence", default="online", request, "set_presence", default="online",
allowed_values=self.ALLOWED_PRESENCE allowed_values=self.ALLOWED_PRESENCE
) )
backfill = parse_boolean(request, "backfill", default=False)
filter_id = parse_string(request, "filter", default=None) filter_id = parse_string(request, "filter", default=None)
full_state = parse_boolean(request, "full_state", default=False)
logger.info( logger.info(
"/sync: user=%r, timeout=%r, limit=%r, gap=%r, sort=%r, since=%r," "/sync: user=%r, timeout=%r, since=%r,"
" set_presence=%r, backfill=%r, filter_id=%r" % ( " set_presence=%r, filter_id=%r" % (
user, timeout, limit, gap, sort, since, set_presence, user, timeout, since, set_presence, filter_id
backfill, filter_id
) )
) )
# TODO(mjark): Load filter and apply overrides. if filter_id and filter_id.startswith('{'):
try: try:
filter = yield self.filtering.get_user_filter( filter_object = json.loads(filter_id)
user.localpart, filter_id except:
raise SynapseError(400, "Invalid filter JSON")
self.filtering._check_valid_filter(filter_object)
filter = FilterCollection(filter_object)
else:
try:
filter = yield self.filtering.get_user_filter(
user.localpart, filter_id
)
except:
filter = FilterCollection({})
if is_guest and filter.list_rooms() is None:
raise SynapseError(
400, "Guest users must provide a list of rooms in the filter"
) )
except:
filter = Filter({})
# filter = filter.apply_overrides(http_request)
# if filter.matches(event):
# # stuff
sync_config = SyncConfig( sync_config = SyncConfig(
user=user, user=user,
gap=gap, is_guest=is_guest,
limit=limit,
sort=sort,
backfill=backfill,
filter=filter, filter=filter,
) )
@@ -137,70 +136,260 @@ class SyncRestServlet(RestServlet):
else: else:
since_token = None since_token = None
sync_result = yield self.sync_handler.wait_for_sync_for_user( if set_presence == "online":
sync_config, since_token=since_token, timeout=timeout yield self.event_stream_handler.started_stream(user)
)
try:
sync_result = yield self.sync_handler.wait_for_sync_for_user(
sync_config, since_token=since_token, timeout=timeout,
full_state=full_state
)
finally:
if set_presence == "online":
self.event_stream_handler.stopped_stream(user)
time_now = self.clock.time_msec() time_now = self.clock.time_msec()
joined = self.encode_joined(
sync_result.joined, filter, time_now, token_id
)
invited = self.encode_invited(
sync_result.invited, filter, time_now, token_id
)
archived = self.encode_archived(
sync_result.archived, filter, time_now, token_id
)
response_content = { response_content = {
"public_user_data": self.encode_user_data( "account_data": self.encode_account_data(
sync_result.public_user_data, filter, time_now sync_result.account_data, filter, time_now
), ),
"private_user_data": self.encode_user_data( "presence": self.encode_presence(
sync_result.private_user_data, filter, time_now sync_result.presence, filter, time_now
),
"rooms": self.encode_rooms(
sync_result.rooms, filter, time_now, token_id
), ),
"rooms": {
"join": joined,
"invite": invited,
"leave": archived,
},
"next_batch": sync_result.next_batch.to_string(), "next_batch": sync_result.next_batch.to_string(),
} }
defer.returnValue((200, response_content)) defer.returnValue((200, response_content))
def encode_user_data(self, events, filter, time_now): def encode_presence(self, events, filter, time_now):
return events formatted = []
for event in events:
event = copy.deepcopy(event)
event['sender'] = event['content'].pop('user_id')
formatted.append(event)
return {"events": filter.filter_presence(formatted)}
def encode_rooms(self, rooms, filter, time_now, token_id): def encode_account_data(self, events, filter, time_now):
return [ return {"events": filter.filter_account_data(events)}
self.encode_room(room, filter, time_now, token_id)
for room in rooms def encode_joined(self, rooms, filter, time_now, token_id):
] """
Encode the joined rooms in a sync result
:param list[synapse.handlers.sync.JoinedSyncResult] rooms: list of sync
results for rooms this user is joined to
:param FilterCollection filter: filters to apply to the results
:param int time_now: current time - used as a baseline for age
calculations
:param int token_id: ID of the user's auth token - used for namespacing
of transaction IDs
:return: the joined rooms list, in our response format
:rtype: dict[str, dict[str, object]]
"""
joined = {}
for room in rooms:
joined[room.room_id] = self.encode_room(
room, filter, time_now, token_id
)
return joined
def encode_invited(self, rooms, filter, time_now, token_id):
"""
Encode the invited rooms in a sync result
:param list[synapse.handlers.sync.InvitedSyncResult] rooms: list of
sync results for rooms this user is joined to
:param FilterCollection filter: filters to apply to the results
:param int time_now: current time - used as a baseline for age
calculations
:param int token_id: ID of the user's auth token - used for namespacing
of transaction IDs
:return: the invited rooms list, in our response format
:rtype: dict[str, dict[str, object]]
"""
invited = {}
for room in rooms:
invite = serialize_event(
room.invite, time_now, token_id=token_id,
event_format=format_event_for_client_v2_without_room_id,
)
invited_state = invite.get("unsigned", {}).pop("invite_room_state", [])
invited_state.append(invite)
invited[room.room_id] = {
"invite_state": {"events": invited_state}
}
return invited
def encode_archived(self, rooms, filter, time_now, token_id):
"""
Encode the archived rooms in a sync result
:param list[synapse.handlers.sync.ArchivedSyncResult] rooms: list of
sync results for rooms this user is joined to
:param FilterCollection filter: filters to apply to the results
:param int time_now: current time - used as a baseline for age
calculations
:param int token_id: ID of the user's auth token - used for namespacing
of transaction IDs
:return: the invited rooms list, in our response format
:rtype: dict[str, dict[str, object]]
"""
joined = {}
for room in rooms:
joined[room.room_id] = self.encode_room(
room, filter, time_now, token_id, joined=False
)
return joined
@staticmethod @staticmethod
def encode_room(room, filter, time_now, token_id): def encode_room(room, filter, time_now, token_id, joined=True):
event_map = {} """
state_events = filter.filter_room_state(room.state) :param JoinedSyncResult|ArchivedSyncResult room: sync result for a
recent_events = filter.filter_room_events(room.events) single room
state_event_ids = [] :param FilterCollection filter: filters to apply to the results
recent_event_ids = [] :param int time_now: current time - used as a baseline for age
for event in state_events: calculations
# TODO(mjark): Respect formatting requirements in the filter. :param int token_id: ID of the user's auth token - used for namespacing
event_map[event.event_id] = serialize_event( of transaction IDs
event, time_now, token_id=token_id, :param joined: True if the user is joined to this room - will mean
event_format=format_event_for_client_v2_without_event_id, we handle ephemeral events
)
state_event_ids.append(event.event_id)
for event in recent_events: :return: the room, encoded in our response format
:rtype: dict[str, object]
"""
def serialize(event):
# TODO(mjark): Respect formatting requirements in the filter. # TODO(mjark): Respect formatting requirements in the filter.
event_map[event.event_id] = serialize_event( return serialize_event(
event, time_now, token_id=token_id, event, time_now, token_id=token_id,
event_format=format_event_for_client_v2_without_event_id, event_format=format_event_for_client_v2_without_room_id,
) )
recent_event_ids.append(event.event_id)
state_dict = room.state
timeline_events = filter.filter_room_timeline(room.timeline.events)
state_dict = SyncRestServlet._rollback_state_for_timeline(
state_dict, timeline_events)
state_events = filter.filter_room_state(state_dict.values())
serialized_state = [serialize(e) for e in state_events]
serialized_timeline = [serialize(e) for e in timeline_events]
account_data = filter.filter_room_account_data(
room.account_data
)
result = { result = {
"room_id": room.room_id, "timeline": {
"event_map": event_map, "events": serialized_timeline,
"events": { "prev_batch": room.timeline.prev_batch.to_string(),
"batch": recent_event_ids, "limited": room.timeline.limited,
"prev_batch": room.prev_batch.to_string(),
}, },
"state": state_event_ids, "state": {"events": serialized_state},
"limited": room.limited, "account_data": {"events": account_data},
"published": room.published,
"ephemeral": room.ephemeral,
} }
if joined:
ephemeral_events = filter.filter_room_ephemeral(room.ephemeral)
result["ephemeral"] = {"events": ephemeral_events}
return result
@staticmethod
def _rollback_state_for_timeline(state, timeline):
"""
Wind the state dictionary backwards, so that it represents the
state at the start of the timeline, rather than at the end.
:param dict[(str, str), synapse.events.EventBase] state: the
state dictionary. Will be updated to the state before the timeline.
:param list[synapse.events.EventBase] timeline: the event timeline
:return: updated state dictionary
"""
logger.debug("Processing state dict %r; timeline %r", state,
[e.get_dict() for e in timeline])
result = state.copy()
for timeline_event in reversed(timeline):
if not timeline_event.is_state():
continue
event_key = (timeline_event.type, timeline_event.state_key)
logger.debug("Considering %s for removal", event_key)
state_event = result.get(event_key)
if (state_event is None or
state_event.event_id != timeline_event.event_id):
# the event in the timeline isn't present in the state
# dictionary.
#
# the most likely cause for this is that there was a fork in
# the event graph, and the state is no longer valid. Really,
# the event shouldn't be in the timeline. We're going to ignore
# it for now, however.
logger.warn("Found state event %r in timeline which doesn't "
"match state dictionary", timeline_event)
continue
prev_event_id = timeline_event.unsigned.get("replaces_state", None)
prev_content = timeline_event.unsigned.get('prev_content')
prev_sender = timeline_event.unsigned.get('prev_sender')
# Empircally it seems possible for the event to have a
# "replaces_state" key but not a prev_content or prev_sender
# markjh conjectures that it could be due to the server not
# having a copy of that event.
# If this is the case the we ignore the previous event. This will
# cause the displayname calculations on the client to be incorrect
if prev_event_id is None or not prev_content or not prev_sender:
logger.debug(
"Removing %r from the state dict, as it is missing"
" prev_content (prev_event_id=%r)",
timeline_event.event_id, prev_event_id
)
del result[event_key]
else:
logger.debug(
"Replacing %r with %r in state dict",
timeline_event.event_id, prev_event_id
)
result[event_key] = FrozenEvent({
"type": timeline_event.type,
"state_key": timeline_event.state_key,
"content": prev_content,
"sender": prev_sender,
"event_id": prev_event_id,
"room_id": timeline_event.room_id,
})
logger.debug("New value: %r", result.get(event_key))
return result return result

View File

@@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
# Copyright 2015 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 ._base import client_v2_patterns
from synapse.http.servlet import RestServlet
from synapse.api.errors import AuthError, SynapseError
from twisted.internet import defer
import logging
import simplejson as json
logger = logging.getLogger(__name__)
class TagListServlet(RestServlet):
"""
GET /user/{user_id}/rooms/{room_id}/tags HTTP/1.1
"""
PATTERNS = client_v2_patterns(
"/user/(?P<user_id>[^/]*)/rooms/(?P<room_id>[^/]*)/tags"
)
def __init__(self, hs):
super(TagListServlet, self).__init__()
self.auth = hs.get_auth()
self.store = hs.get_datastore()
@defer.inlineCallbacks
def on_GET(self, request, user_id, room_id):
auth_user, _, _ = yield self.auth.get_user_by_req(request)
if user_id != auth_user.to_string():
raise AuthError(403, "Cannot get tags for other users.")
tags = yield self.store.get_tags_for_room(user_id, room_id)
defer.returnValue((200, {"tags": tags}))
class TagServlet(RestServlet):
"""
PUT /user/{user_id}/rooms/{room_id}/tags/{tag} HTTP/1.1
DELETE /user/{user_id}/rooms/{room_id}/tags/{tag} HTTP/1.1
"""
PATTERNS = client_v2_patterns(
"/user/(?P<user_id>[^/]*)/rooms/(?P<room_id>[^/]*)/tags/(?P<tag>[^/]*)"
)
def __init__(self, hs):
super(TagServlet, self).__init__()
self.auth = hs.get_auth()
self.store = hs.get_datastore()
self.notifier = hs.get_notifier()
@defer.inlineCallbacks
def on_PUT(self, request, user_id, room_id, tag):
auth_user, _, _ = yield self.auth.get_user_by_req(request)
if user_id != auth_user.to_string():
raise AuthError(403, "Cannot add tags for other users.")
try:
content_bytes = request.content.read()
body = json.loads(content_bytes)
except:
raise SynapseError(400, "Invalid tag JSON")
max_id = yield self.store.add_tag_to_room(user_id, room_id, tag, body)
yield self.notifier.on_new_event(
"account_data_key", max_id, users=[user_id]
)
defer.returnValue((200, {}))
@defer.inlineCallbacks
def on_DELETE(self, request, user_id, room_id, tag):
auth_user, _, _ = yield self.auth.get_user_by_req(request)
if user_id != auth_user.to_string():
raise AuthError(403, "Cannot add tags for other users.")
max_id = yield self.store.remove_tag_from_room(user_id, room_id, tag)
yield self.notifier.on_new_event(
"account_data_key", max_id, users=[user_id]
)
defer.returnValue((200, {}))
def register_servlets(hs, http_server):
TagListServlet(hs).register(http_server)
TagServlet(hs).register(http_server)

View File

@@ -18,7 +18,7 @@ from twisted.internet import defer
from synapse.api.errors import AuthError, StoreError, SynapseError from synapse.api.errors import AuthError, StoreError, SynapseError
from synapse.http.servlet import RestServlet from synapse.http.servlet import RestServlet
from ._base import client_v2_pattern, parse_json_dict_from_request from ._base import client_v2_patterns, parse_json_dict_from_request
class TokenRefreshRestServlet(RestServlet): class TokenRefreshRestServlet(RestServlet):
@@ -26,7 +26,7 @@ class TokenRefreshRestServlet(RestServlet):
Exchanges refresh tokens for a pair of an access token and a new refresh Exchanges refresh tokens for a pair of an access token and a new refresh
token. token.
""" """
PATTERN = client_v2_pattern("/tokenrefresh") PATTERNS = client_v2_patterns("/tokenrefresh")
def __init__(self, hs): def __init__(self, hs):
super(TokenRefreshRestServlet, self).__init__() super(TokenRefreshRestServlet, self).__init__()

View File

@@ -66,7 +66,7 @@ class ContentRepoResource(resource.Resource):
@defer.inlineCallbacks @defer.inlineCallbacks
def map_request_to_name(self, request): def map_request_to_name(self, request):
# auth the user # auth the user
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
# namespace all file uploads on the user # namespace all file uploads on the user
prefix = base64.urlsafe_b64encode( prefix = base64.urlsafe_b64encode(

View File

@@ -70,7 +70,7 @@ class UploadResource(BaseMediaResource):
@request_handler @request_handler
@defer.inlineCallbacks @defer.inlineCallbacks
def _async_render_POST(self, request): def _async_render_POST(self, request):
auth_user, _ = yield self.auth.get_user_by_req(request) auth_user, _, _ = yield self.auth.get_user_by_req(request)
# TODO: The checks here are a bit late. The content will have # TODO: The checks here are a bit late. The content will have
# already been uploaded to a tmp file at this point # already been uploaded to a tmp file at this point
content_length = request.getHeader("Content-Length") content_length = request.getHeader("Content-Length")

View File

@@ -19,7 +19,9 @@
# partial one for unit test mocking. # partial one for unit test mocking.
# Imports required for the default HomeServer() implementation # Imports required for the default HomeServer() implementation
from twisted.web.client import BrowserLikePolicyForHTTPS
from synapse.federation import initialize_http_replication from synapse.federation import initialize_http_replication
from synapse.http.client import SimpleHttpClient, InsecureInterceptableContextFactory
from synapse.notifier import Notifier from synapse.notifier import Notifier
from synapse.api.auth import Auth from synapse.api.auth import Auth
from synapse.handlers import Handlers from synapse.handlers import Handlers
@@ -27,7 +29,6 @@ from synapse.state import StateHandler
from synapse.storage import DataStore from synapse.storage import DataStore
from synapse.util import Clock from synapse.util import Clock
from synapse.util.distributor import Distributor from synapse.util.distributor import Distributor
from synapse.util.lockutils import LockManager
from synapse.streams.events import EventSources from synapse.streams.events import EventSources
from synapse.api.ratelimiting import Ratelimiter from synapse.api.ratelimiting import Ratelimiter
from synapse.crypto.keyring import Keyring from synapse.crypto.keyring import Keyring
@@ -68,11 +69,9 @@ class BaseHomeServer(object):
'auth', 'auth',
'rest_servlet_factory', 'rest_servlet_factory',
'state_handler', 'state_handler',
'room_lock_manager',
'notifier', 'notifier',
'distributor', 'distributor',
'resource_for_client', 'client_resource',
'resource_for_client_v2_alpha',
'resource_for_federation', 'resource_for_federation',
'resource_for_static_content', 'resource_for_static_content',
'resource_for_web_client', 'resource_for_web_client',
@@ -87,6 +86,8 @@ class BaseHomeServer(object):
'pusherpool', 'pusherpool',
'event_builder_factory', 'event_builder_factory',
'filtering', 'filtering',
'http_client_context_factory',
'simple_http_client',
] ]
def __init__(self, hostname, **kwargs): def __init__(self, hostname, **kwargs):
@@ -174,6 +175,17 @@ class HomeServer(BaseHomeServer):
def build_auth(self): def build_auth(self):
return Auth(self) return Auth(self)
def build_http_client_context_factory(self):
config = self.get_config()
return (
InsecureInterceptableContextFactory()
if config.use_insecure_ssl_client_just_for_testing_do_not_use
else BrowserLikePolicyForHTTPS()
)
def build_simple_http_client(self):
return SimpleHttpClient(self)
def build_v1auth(self): def build_v1auth(self):
orf = Auth(self) orf = Auth(self)
# Matrix spec makes no reference to what HTTP status code is returned, # Matrix spec makes no reference to what HTTP status code is returned,
@@ -186,9 +198,6 @@ class HomeServer(BaseHomeServer):
def build_state_handler(self): def build_state_handler(self):
return StateHandler(self) return StateHandler(self)
def build_room_lock_manager(self):
return LockManager()
def build_distributor(self): def build_distributor(self):
return Distributor() return Distributor()

Some files were not shown because too many files have changed in this diff Show More