Compare commits

..
146 Commits
Author SHA1 Message Date
Jason Volk e106e50ed0 Update the since token to skip empty range from prior iteration.
Log and discard presence ping errors without preventing sync.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-06 08:06:39 +00:00
Jason Volk 00f11a9e8f Additional instrumentation around global counter events. 2025-08-06 06:45:43 +00:00
Jason Volk bf48c7b1b4 Move active and finished request counters out of span field.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-06 00:11:26 +00:00
Jason Volk 8a8a5d598e Fix sync loop watcher ABA.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-05 10:51:56 +00:00
Jason Volk fa84a7e6c2 Bump 1.3.0
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-05 07:02:31 +00:00
Jason Volk 2bdeca0ed3 ci: Fix missing condition for Push Branch. (#103)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-05 07:02:31 +00:00
Jason Volk c337ea1864 Fixes for sync v3 protocol compliance.
Add sync v3 filter support.

Fix events duplicated between state and timeline; cleanup some lets.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-04 18:55:37 +00:00
Jason Volk b00361d274 Add specific shorteventid to shortstatehash query function.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-04 18:55:37 +00:00
Jason Volk fb17aa19bf Add configs for creating the admin room and granting first user.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-04 18:55:37 +00:00
Jason Volk fc28e8e1dd Update flakes list; unflake several packages as fail.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-04 18:55:37 +00:00
Jason Volk cea25c6580 docker: Make the complement defaults more accessible.
commit newly uncovered tests.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-04 05:13:06 +00:00
Jason Volk a0dc37e024 Fetch prev_events concurrently.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-04 05:13:06 +00:00
Jason Volk 003257693b Fetch outlier and auth chains concurrently.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-04 00:48:30 +00:00
Jason Volk bc898efcce Uncomment generated-config's global section header, comment all others. (#116)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-03 21:55:36 +00:00
Jason Volk fa8ad1d629 Update the router log truncator for new axum syntax.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-03 00:53:53 +00:00
Jason Volk 44b60050b3 Reorder auth match cases; add todo for AppserviceTokenOptional.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-02 22:02:26 +00:00
Jason Volk 4b84e7c31a Flatten and dedup append_incoming branch in upgrade_outlier.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-02 18:00:20 +00:00
Jason Volk 4237f21903 Renames for several event_handler service files, fn's and args.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-02 18:00:20 +00:00
Jason Volk 4ac61fd87b Remove federation_handletime for now.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-02 18:00:20 +00:00
Jason Volk d217927000 Split fetch_outlier; abstract backoff stanzas into fn.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-02 18:00:20 +00:00
Jason Volk 4b9b85f671 ABI encapsulations; generate leaf code in pub fn's rather than inling them.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-02 18:00:20 +00:00
Jason Volk fa3b72947a Eliminate rooms outlier service, duplicate subset of timeline service.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-02 18:00:20 +00:00
Jason Volk 5d963abda6 Combine rooms timeline service data unit into mod; eliminate wrappers.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-02 18:00:20 +00:00
ObiomaAtWanaguandJason Volk a91380ac17 Change from the error 'No more rooms.' to the output 'No rooms are published.' if there are no rooms.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-02 18:00:20 +00:00
Jason Volk 1664a2c225 Implement refresh-tokens. (resolves #50)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-01 09:14:58 +00:00
Jason Volk 0d7eeb7766 Extend the append count guards over subsequent operations.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-01 09:14:58 +00:00
Jason Volk 16447f7383 Handle defaulting trailing elements in truncated database records.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-01 05:30:40 +00:00
Jason Volk 3612b04890 Add timepoint_from_epoch to time utils.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-01 03:40:27 +00:00
Jason Volk e6f1b98d6a Improve tracing of request error responses.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-31 14:31:28 +00:00
Jason Volk 6742e1d206 Sort misc cargo list entries, database maps.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-31 07:02:59 +00:00
Jason Volk dc0976a2e5 Respond with 403 instead of 404 for non-existing user.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-31 07:02:59 +00:00
Jason Volk f3f23ee7ee Fix missing counter retirements (51931de94c).
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-30 23:34:21 +00:00
Jason Volk 0b15ab2006 Ensure unwind safety in dispatch sequence.
Ensure await safety/efficiency in retirement sequence.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-30 23:34:21 +00:00
Jason Volk bd0c3e33e2 Loop syncv3 for robust response without re-request overhead.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-30 23:34:21 +00:00
Jason Volk 1bb16c8b73 Additional span logging of counter state; trace logging of contents.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-30 23:34:21 +00:00
Jason Volk 59b62b1453 Fix partial v3 syncs on post-timeout pass; fix partial state on room join.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-30 23:34:21 +00:00
Jason Volk 2e0b156de0 Fix missing user account data sync watcher.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-30 19:28:16 +00:00
Jason Volk 3593b8ebf7 Implement Debug for RawPduId.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-30 19:09:46 +00:00
Jason Volk 80015a6a30 Fix missed rustfmt let due to comment in chain.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-30 19:09:46 +00:00
Jason Volk 95e49d3cb8 Fix map_or calling function.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-30 19:09:46 +00:00
Jason Volk 25164e030d Hide own user from user directory search results.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-30 19:09:46 +00:00
Jason Volk f04924d6be docker: Extract complement logs on failure.
docker: Sort config options used during complement.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-29 14:34:42 +00:00
Jason Volk df7cd58618 Support matching room_id as publicrooms search term.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-29 04:18:21 +00:00
Jason Volk bf12e05dae Add meta iteration for public rooms.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-29 04:18:21 +00:00
Jason Volk e53366a492 Fix missing sync watcher not added for knocking feature.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-29 04:18:21 +00:00
Jason Volk ce25d79e66 Flatten authorised-join check queries.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-29 04:18:21 +00:00
Jason Volk 71ef093edc Unbox and pin various either-or futures.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-29 04:18:21 +00:00
Jason Volk 066097a83d Optimize user and appservice token queries.
Split/cleanup api::router auth delegator.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-29 04:18:20 +00:00
Jason Volk 1c24ed09fa Fix misuse of try_select in logical-or future util.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-29 04:18:20 +00:00
Jason Volk d3dbe09380 Add SystemTimeError to core error enum.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-27 13:09:30 +00:00
Jason Volk e9d0baa32e Add granular timeout configs for sync longpolling.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-27 11:34:15 +00:00
Jason Volk 7dd10ebc6c docs: Add note to configuration doc.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-27 11:03:12 +00:00
Jason Volk 29ac62ec27 Fix zero since-token value not used as initial sync condition.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-27 11:03:12 +00:00
Jason Volk 8c8985e8f2 Add user_id to v3 sync span; add span for sync token association.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-27 11:03:12 +00:00
Jason Volk 730920b571 Collect v5 typing events concurrent with other extensions.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-27 07:04:10 +00:00
Jason Volk 66334bdd53 Move syncv3 typings event for user.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-27 07:04:10 +00:00
Jason Volk 6b7bf80de5 Disable legacy media request fallbacks by default.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-27 07:04:09 +00:00
Jason Volk 4c35bfbda4 Fix M_UNKNOWN error which should be M_BAD_ALIAS.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-26 07:29:30 +00:00
Jason Volk ba51748ad3 Add missing upper-bounded calls; improve snake-sync windowing.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-26 05:52:30 +00:00
Jason Volk 63dfe8f7e3 Add upper-bound for presence_since().
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-26 05:36:19 +00:00
Jason Volk c6836e51b2 Add upper-bound to readreceipts_since() and callsites.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-26 04:44:09 +00:00
Jason Volk 33a9fce828 dedup room directory banned server check.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-26 02:26:09 +00:00
Jason Volk 267fd4de5b Enable additional complement parallel tests.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-25 22:52:29 +00:00
Jason Volk ce30f83052 Optimize sync watchers and key serializations.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-25 21:55:37 +00:00
Jason Volk e09a2c0e0f Window sync ranges below any partially completed writes.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-25 21:55:37 +00:00
Jason Volk 992abfab8f Separate cached body from request body in snake-sync; cleanup.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-25 21:55:37 +00:00
Jason Volk 51931de94c Add release-sequence for counter retirements.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-25 21:55:37 +00:00
Jason Volk 85a84f93c7 Fix misinterpretation of filter.room.include_leave in sync v3.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-25 21:55:37 +00:00
Jason Volk e58ef326d4 Refactor legacy database watcher system.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-25 20:07:21 +00:00
Jason Volk 0d439188e8 Fix sequence windowing for sync v3 invite/leave/knock rooms.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-25 20:07:21 +00:00
Jason Volk 22ef704ac6 Fix bug from roomuserid_knockedcount vs. roomid_knockedcount.
The use of "count" instead of "sequence" leads to confusion with
i.e. counting the elements of a set e.g. Iterator::count().

Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-25 20:07:21 +00:00
Jason Volk 7bdd4065c8 Split database::Map put/del from insert.rs and remove.rs
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-25 20:07:18 +00:00
Jason Volk 0fcb072239 Refactor counter increment sites for TwoPhaseCounter.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-25 10:32:52 +00:00
Jason Volk 05bb1f4ac7 Add TwoPhaseCounter to core utils.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-25 10:32:52 +00:00
Jason Volk 89e3a8c182 Add common editor temps to gitignore.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-25 10:32:52 +00:00
Jason Volk f0576f8d7d Remove unused deprecated sliding sync service code.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 23:13:01 +00:00
Jason Volk 11f77defc7 Handle AuthData::FallbackAcknowledgement; mute error for now.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 23:13:01 +00:00
Jason Volk 23c75855ba Split api/client/register from api/client/account suite.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 23:13:01 +00:00
Jason Volk 5253a8d36a Inline and apply must_use to remaining services globals wrappers.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 22:31:11 +00:00
Jason Volk c9adee86f5 Unify calculate_state_changes in syncv3
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 22:17:30 +00:00
Jason Volk 3c47516c85 Pipeline private read receipt fetch; cleanup tuples syncv3.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 22:17:30 +00:00
Jason Volk 299d3230a1 Optimize typing event collection.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 22:17:30 +00:00
9ea8fbc482 Add typing indicators to sync v5.
Co-authored-by: Jade Ellis <jade@ellis.link>
2025-07-24 22:17:30 +00:00
Jason Volk f7877d122b List #tuwunel:matrix.org; simplify readme.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:28 +00:00
Jason Volk 17119670c0 Add admin debug command to manually resync with primary. (#35)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:28 +00:00
Jason Volk 535fcab4b5 ci: Create multi-arch manifest for each release tag (fixes #103).
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:28 +00:00
Jason Volk cbe34affa3 Add unique id counter to request tracing span. (fixes #112)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:28 +00:00
Jason Volk ed0b3c764e Parallelize state_at_incoming sstatehash/prev_event fetches.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:28 +00:00
Jason Volk 190269f616 Simplify Pdu Hashes struct.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:28 +00:00
Jason Volk 3ea8c82ef0 Follow-up to admin channel change (7d2d42542c).
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:28 +00:00
Jason Volk 2a627baa96 Fix excessive nesting.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:27 +00:00
Jason Volk 50de6277b8 Split/cleanup backfill stream stanza.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:27 +00:00
Jason Volk 4429323e11 Flatten threads_until().
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:27 +00:00
Jason Volk 18f8d6c65c Rebox future segmentations.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:27 +00:00
Jason Volk aac49b09c7 Improve mutable state in sender closure captures.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:27 +00:00
Jason Volk c8d35cca57 Refactor to async closures.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:27 +00:00
Jason Volk cca0f20148 Replace several expects with error coercions.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:27 +00:00
Jason Volk 23d188f612 Spacing/formatting adjustments.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:27 +00:00
Jason Volk ae707ab465 Simplify default Result generics.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:27 +00:00
Jason Volk 8244d78cb2 Make Event trait Send+Sync.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:27 +00:00
Jason Volk b0315da3d7 Fix branches sharing code.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:27 +00:00
Jason Volk eca01cfb3f Fix useless if let seq.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:27 +00:00
Jason Volk cf13f5afc1 Allow clippy::map_unwrap_or.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:27 +00:00
Jason Volk c944f1124a Additional BoolExt utils.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:27 +00:00
Jason Volk 70fc95e800 Fix same generic used for separate closures in BoolExt.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:27 +00:00
Jason Volk 507bd1edc7 Update complement; mask new tests.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-24 04:07:27 +00:00
coolGiandJason Volk ee0e594aef Fixed nix cached shasum for rust-toolchain.toml 2025-07-21 21:05:36 -07:00
ObiomaAtWanaguandJason Volk fd080914c2 Enhanced docu for configuration
Include that more than one config file can be named.
Describe the relevance of ordering for setting the value.
2025-07-09 20:38:27 +00:00
dasha_uwuandJason Volk 9dd2b28605 Add base64 output for admin query command.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-08 23:49:18 +00:00
Jade EllisandJason Volk 34abe1cce4 fix: Allow joining via invite for knock_restricted rooms 2025-07-08 23:38:18 +00:00
Tyler MayoffandJason Volk a340e6786d use room names first and only fallback to heroes if not available 2025-07-07 07:36:51 +00:00
June StrawberryandJason Volk c37ee97351 allow nightly unstable_name_collisions lint due to BoolExt conflict
Signed-off-by: June Strawberry <june@basednetworking.com>
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-07 07:36:51 +00:00
Jason Volk 7d2d42542c Simplify admin channel type.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-07 07:09:49 +00:00
Jason Volk fd0082fd2c Add ready_try_skip_while() to stream utils.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-07 07:09:49 +00:00
Jason Volk ca8f3ae057 Add gitignores for -Zself-profile output.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-07 07:09:49 +00:00
Jason Volk 8a748e02df Fix into_recv_seek lifetime flow to match into_recv_get.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-07 07:09:49 +00:00
Jason Volk 79ee7838e7 Rename elided_named_lifetimes to mismatched_lifetime_syntaxes.
Reapply is_multiple_of().

Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-07 07:09:49 +00:00
Jason Volk e9a441fb6e Bump tokio to 1.46
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-07 02:44:18 +00:00
Jason Volk 93f5557232 Bump rust to 1.88.0
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-07 02:23:25 +00:00
Jason Volk 3d88767977 Bump 1.2.0
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-04 02:00:07 +00:00
Jason Volk b68cff3b2f Fix clippy::manual-is-multiple-of.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-04 01:49:57 +00:00
Jason Volk 36c3845ff6 Comment section headers in generated example config. (fixes #96)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-04 00:31:09 +00:00
Jason Volk 7f1c7b964e Add missing display/default attributes to appservice example config.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-04 00:13:30 +00:00
Jason Volk 1cbcd3fc88 Add Result traits map_ref()/and_then_ref().
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-03 22:18:44 +00:00
Jason Volk ad9ddd91cd Add broad_find_map() to stream utils.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-03 22:18:44 +00:00
Jason Volk a2b4c07cf7 Additional internal pdu getters.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-03 22:18:44 +00:00
orhtej2andJason Volk 2ead282bec Dedup entries returned by LDAP 2025-06-29 04:02:41 +00:00
Jason Volk 9496d05cd2 ci: Pass package build condition as workflow arg.
ci: Add defaults for init matrix; branch conditions for pulls.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-29 04:02:41 +00:00
Jason Volk c8ab1713c3 Fix errant ldap3 dependency in src/core.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-29 04:02:41 +00:00
orhtej2 84acdccb93 Other typo 2025-06-29 01:20:02 +02:00
orhtej2 6164f62395 Fix optional admin config 2025-06-29 01:16:51 +02:00
Jason Volk b36ad9cfef docker: use OCI media types for registry images. (fixes #79)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-26 01:01:56 +00:00
Jason Volk d557f0f456 README: soften warning to tip. [ci skip]
Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-26 01:01:56 +00:00
Jason Volk d858c5c1f4 docker: Add Instruction Set Architectures to build matrix.
ci: Publish multi-arch images for both branch and tag refs.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-25 17:21:55 +00:00
Jason Volk 972201da60 Add warning when listening=false. (fixes #81)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-25 01:04:18 +00:00
Jason Volk 56d9c9a8b5 Handle empty std::env::args() for FreeBSD. (fixes #75)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-25 01:04:18 +00:00
Jason Volk 84ce3e0d0e Change default emoji due to inconsistent support. (closes #80)
Move config item to more prominent location.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-25 01:04:18 +00:00
Jason Volk 578cf9d2d7 Fix missing lz4 and bz2; tweak feature projection. (closes #78)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-25 01:04:18 +00:00
Jason Volk ca69af97c1 docker: Re-relax cargo cache expectations. (#53)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-24 23:45:08 +00:00
Jason Volk ac26193ffe Check appservice registrations for unique as_token. (#72)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-20 18:08:19 +00:00
Jason Volk a84e559640 Implement declarative appservices. (closes #67)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-20 18:08:19 +00:00
Jason Volk 2e559a0d3e docker: Use zstd for all compressions.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-20 13:51:35 +00:00
Jason Volk 2634fed2d4 Tweak readme wording.
Fix actual typo.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-20 13:04:02 +00:00
Jason Volk 31a3676cd8 ci: Publish multi-arch manifest. (fixes #74)
ci: Toggle package install checks via vars.

docker: Disable provenance attestations which auto-generate manifests.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-20 10:52:35 +00:00
Jason Volk aa1609d611 Fix RELEASE notes formatting for GH.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-20 01:21:07 +00:00
Jason Volk 8e13f07d2a ci: Fix distro package generation for tags.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-19 13:05:49 +00:00
214 changed files with 5892 additions and 4184 deletions
+15 -15
View File
@@ -43,7 +43,7 @@ jobs:
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
&& contains(fromJSON(inputs.machines), fromJSON('["X64"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
name: Format
uses: ./.github/workflows/bake.yml
@@ -54,9 +54,9 @@ jobs:
rust_toolchains: '["nightly"]'
rust_targets: ${{inputs.rust_targets}}
sys_names: ${{inputs.sys_names}}
sys_targets: ${{inputs.sys_targets}}
sys_versions: ${{inputs.sys_versions}}
machines: '["X64"]'
sys_targets: '["x86_64-v1-linux-gnu"]'
machines: ${{inputs.machines}}
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
@@ -66,7 +66,7 @@ jobs:
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
&& contains(fromJSON(inputs.machines), fromJSON('["X64"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
name: Typos
uses: ./.github/workflows/bake.yml
@@ -78,8 +78,8 @@ jobs:
sys_names: ${{inputs.sys_names}}
sys_versions: ${{inputs.sys_versions}}
rust_targets: ${{inputs.rust_targets}}
sys_targets: ${{inputs.sys_targets}}
machines: '["X64"]'
sys_targets: '["x86_64-v1-linux-gnu"]'
machines: ${{inputs.machines}}
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
@@ -89,7 +89,7 @@ jobs:
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
&& contains(fromJSON(inputs.machines), fromJSON('["X64"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
name: Audit
uses: ./.github/workflows/bake.yml
@@ -101,8 +101,8 @@ jobs:
sys_names: ${{inputs.sys_names}}
sys_versions: ${{inputs.sys_versions}}
rust_targets: ${{inputs.rust_targets}}
sys_targets: ${{inputs.sys_targets}}
machines: '["X64"]'
sys_targets: '["x86_64-v1-linux-gnu"]'
machines: ${{inputs.machines}}
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
@@ -112,7 +112,7 @@ jobs:
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
&& contains(fromJSON(inputs.machines), fromJSON('["X64"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
name: Lychee
uses: ./.github/workflows/bake.yml
@@ -124,8 +124,8 @@ jobs:
sys_names: ${{inputs.sys_names}}
sys_versions: ${{inputs.sys_versions}}
rust_targets: ${{inputs.rust_targets}}
sys_targets: ${{inputs.sys_targets}}
machines: '["X64"]'
sys_targets: '["x86_64-v1-linux-gnu"]'
machines: ${{inputs.machines}}
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
@@ -135,7 +135,7 @@ jobs:
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
&& contains(fromJSON(inputs.machines), fromJSON('["X64"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
name: Check
uses: ./.github/workflows/bake.yml
@@ -147,8 +147,8 @@ jobs:
sys_names: ${{inputs.sys_names}}
sys_versions: ${{inputs.sys_versions}}
rust_targets: ${{inputs.rust_targets}}
sys_targets: ${{inputs.sys_targets}}
machines: '["X64"]'
sys_targets: '["x86_64-v1-linux-gnu"]'
machines: ${{inputs.machines}}
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
+76 -31
View File
@@ -26,7 +26,7 @@ jobs:
default_sys_names: '["debian"]'
default_sys_versions: '["testing-slim"]'
default_rust_targets: '["x86_64-unknown-linux-gnu"]'
default_sys_targets: '["x86_64-linux-gnu"]'
default_sys_targets: '["x86_64-v1-linux-gnu"]'
default_machines: '["X64"]'
outputs:
@@ -38,21 +38,28 @@ jobs:
sys_targets: ${{vars.SYS_TARGETS || env.default_sys_targets}}
sys_versions: ${{vars.SYS_VERSIONS || env.default_sys_versions}}
machines: ${{vars.MACHINES || env.default_machines}}
package: ${{vars.PACKAGE || !contains(github.ref, 'refs/pull/')}}
publish: ${{vars.PUBLISH || !contains(github.ref, 'refs/pull/')}}
build_pkgs: ${{vars.BUILD_PKGS || github.ref == 'refs/heads/main' || contains(github.ref, 'tags/v')}}
check_pkgs: ${{vars.CHECK_PKGS || 'false'}}
complement: ${{vars.COMPLEMENT || 'true'}}
complement_runner: 'het'
docker_repo: ${{vars.DOCKER_REPO}}
release_url: ${{steps.release.outputs.upload_url}}
strategy:
fail-fast: true
matrix:
runner: ${{fromJSON(vars.RUNNERS)}}
machine: ${{fromJSON(vars.MACHINES)}}
exclude: ${{fromJSON(vars.RUNNERS_EXCLUDES)}}
runner: ${{fromJSON(vars.RUNNERS || '["het"]')}}
machine: ${{fromJSON(vars.MACHINES || '["X64"]')}}
exclude: ${{fromJSON(vars.RUNNERS_EXCLUDES || '[]')}}
steps:
- name: Initialize Builder
env:
runner: ${{matrix.runner}}
reserved_space: '{"het": "256GB", "aws": "48GB", "gcp": "160GB"}'
max_used_space: '{"het": "448GB", "aws": "64GB", "gcp": "192GB"}'
reserved_space: '{"het": "384GB", "aws": "48GB", "gcp": "160GB"}'
max_used_space: '{"het": "512GB", "aws": "64GB", "gcp": "192GB"}'
run: |
set +e
docker buildx inspect "${GITHUB_ACTOR}"
@@ -134,10 +141,16 @@ jobs:
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "none"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "default"},
{"rust_target": "aarch64-unknown-linux-gnu", "rust_toolchain": "nightly"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-linux-gnu"},
{"rust_target": "x86_64-unknown-linux-gnu", "sys_target": "aarch64-linux-gnu"},
{"sys_target": "x86_64-linux-gnu", "machine": "ARM64"},
{"sys_target": "aarch64-linux-gnu", "machine": "X64"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v1-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v2-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v3-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v4-linux-gnu"},
{"rust_target": "x86_64-unknown-linux-gnu", "sys_target": "aarch64-v8-linux-gnu"},
{"sys_target": "aarch64-v8-linux-gnu", "machine": "X64"},
{"sys_target": "x86_64-v1-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v2-linux-gnu"},
{"sys_target": "x86_64-v3-linux-gnu"},
{"sys_target": "x86_64-v4-linux-gnu"},
]
test:
@@ -149,7 +162,8 @@ jobs:
needs: [init, lint]
uses: ./.github/workflows/test.yml
with:
complement: ${{fromJSON(vars.COMPLEMENT || 'true')}}
complement: ${{fromJSON(needs.init.outputs.complement)}}
complement_runner: ${{needs.init.outputs.complement_runner}}
cargo_profiles: ${{needs.init.outputs.cargo_profiles}}
feat_sets: ${{needs.init.outputs.feat_sets}}
rust_toolchains: ${{needs.init.outputs.rust_toolchains}}
@@ -158,7 +172,6 @@ jobs:
sys_targets: ${{needs.init.outputs.sys_targets}}
sys_versions: ${{needs.init.outputs.sys_versions}}
machines: ${{needs.init.outputs.machines}}
complement_runner: 'het'
excludes: >
[
{"cargo_profile": "test", "rust_toolchain": "stable", "feat_set": "none"},
@@ -171,21 +184,36 @@ jobs:
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "none"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "default"},
{"rust_target": "aarch64-unknown-linux-gnu", "rust_toolchain": "nightly"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-linux-gnu"},
{"rust_target": "x86_64-unknown-linux-gnu", "sys_target": "aarch64-linux-gnu"},
{"sys_target": "x86_64-linux-gnu", "machine": "ARM64"},
{"sys_target": "aarch64-linux-gnu", "machine": "X64"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v1-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v2-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v3-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v4-linux-gnu"},
{"rust_target": "x86_64-unknown-linux-gnu", "sys_target": "aarch64-v8-linux-gnu"},
{"sys_target": "aarch64-v8-linux-gnu", "machine": "X64"},
{"sys_target": "x86_64-v1-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v2-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v2-linux-gnu", "feat_set": "none"},
{"sys_target": "x86_64-v2-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v3-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v3-linux-gnu", "feat_set": "none"},
{"sys_target": "x86_64-v3-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v4-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v4-linux-gnu", "feat_set": "none"},
{"sys_target": "x86_64-v4-linux-gnu", "feat_set": "default"},
]
package:
if: >
!failure() && !cancelled()
&& needs.init.outputs.package
name: Package
needs: [init, lint]
uses: ./.github/workflows/package.yml
with:
release_url: ${{needs.init.outputs.release_url}}
check_pkgs: ${{needs.init.outputs.check_pkgs}}
build_pkgs: ${{needs.init.outputs.build_pkgs}}
cargo_profiles: ${{needs.init.outputs.cargo_profiles}}
feat_sets: ${{needs.init.outputs.feat_sets}}
rust_toolchains: ${{needs.init.outputs.rust_toolchains}}
@@ -198,33 +226,41 @@ jobs:
[
{"feat_set": "none"},
{"cargo_profile": "test"},
{"cargo_profile": "release-native"},
{"cargo_profile": "release-debuginfo", "feat_set": "default"},
{"cargo_profile": "release-debuginfo", "rust_toolchain": "nightly"},
{"cargo_profile": "release-debuginfo", "bake_target": "oci"},
{"cargo_profile": "release-debuginfo", "rust_target": "aarch64-unknown-linux-gnu"},
{"cargo_profile": "release-native", "feat_set": "default"},
{"cargo_profile": "release-native", "rust_toolchain": "stable"},
{"cargo_profile": "release-native", "feat_set": "default"},
{"cargo_profile": "release-native", "bake_target": "docker"},
{"cargo_profile": "release-native", "bake_target": "oci"},
{"cargo_profile": "release-native", "rust_target": "aarch64-unknown-linux-gnu"},
{"cargo_profile": "release-debuginfo", "sys_target": "x86_64-v2-linux-gnu"},
{"cargo_profile": "release-debuginfo", "sys_target": "x86_64-v3-linux-gnu"},
{"cargo_profile": "release-debuginfo", "sys_target": "x86_64-v4-linux-gnu"},
{"cargo_profile": "release", "rust_toolchain": "nightly"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "default"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-linux-gnu"},
{"rust_target": "x86_64-unknown-linux-gnu", "sys_target": "aarch64-linux-gnu"},
{"sys_target": "x86_64-linux-gnu", "machine": "ARM64"},
{"sys_target": "aarch64-linux-gnu", "machine": "X64"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v1-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v2-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v3-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v4-linux-gnu"},
{"rust_target": "x86_64-unknown-linux-gnu", "sys_target": "aarch64-v8-linux-gnu"},
{"sys_target": "aarch64-v8-linux-gnu", "machine": "X64"},
{"sys_target": "x86_64-v1-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v2-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v2-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v3-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v3-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v4-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v4-linux-gnu", "feat_set": "default"},
]
publish:
if: >
!failure() && !cancelled()
&& needs.init.outputs.publish
name: Publish
needs: [init, test, package]
uses: ./.github/workflows/publish.yml
with:
docker_repo: ${{vars.DOCKER_REPO}}
docker_repo: ${{needs.init.outputs.docker_repo}}
release_url: ${{needs.init.outputs.release_url}}
cargo_profiles: ${{needs.init.outputs.cargo_profiles}}
feat_sets: ${{needs.init.outputs.feat_sets}}
@@ -242,10 +278,19 @@ jobs:
{"cargo_profile": "release-native"},
{"cargo_profile": "release", "rust_toolchain": "nightly"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "default"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-linux-gnu"},
{"rust_target": "x86_64-unknown-linux-gnu", "sys_target": "aarch64-linux-gnu"},
{"sys_target": "x86_64-linux-gnu", "machine": "ARM64"},
{"sys_target": "aarch64-linux-gnu", "machine": "X64"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v1-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v2-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v3-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v4-linux-gnu"},
{"rust_target": "x86_64-unknown-linux-gnu", "sys_target": "aarch64-v8-linux-gnu"},
{"sys_target": "aarch64-v8-linux-gnu", "machine": "X64"},
{"sys_target": "x86_64-v1-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v2-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v2-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v3-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v3-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v4-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v4-linux-gnu", "feat_set": "default"},
]
secrets:
+11 -5
View File
@@ -38,6 +38,12 @@ on:
release_url:
type: string
description: For release assets
build_pkgs:
type: string
default: 'true'
check_pkgs:
type: string
default: 'false'
jobs:
book:
@@ -46,7 +52,7 @@ jobs:
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["release"]')[0])
&& contains(fromJSON(inputs.feat_sets), fromJSON('["default"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["stable"]')[0])
&& contains(fromJSON(inputs.machines), fromJSON('["X64"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
name: Book
uses: ./.github/workflows/bake.yml
@@ -57,9 +63,9 @@ jobs:
rust_toolchains: '["stable"]'
rust_targets: ${{inputs.rust_targets}}
sys_names: ${{inputs.sys_names}}
sys_targets: ${{inputs.sys_targets}}
sys_versions: ${{inputs.sys_versions}}
machines: '["X64"]'
sys_targets: '["x86_64-v1-linux-gnu"]'
machines: ${{inputs.machines}}
runner: 'het'
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
@@ -144,7 +150,7 @@ jobs:
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["release"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["stable"]')[0])
&& github.ref == 'refs/heads/main'
&& fromJSON(inputs.build_pkgs)
name: Distro Packages
uses: ./.github/workflows/bake.yml
@@ -179,7 +185,7 @@ jobs:
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["release"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["stable"]')[0])
&& github.ref == 'refs/heads/main'
&& fromJSON(inputs.check_pkgs)
name: Check Packages
needs: [pkgs]
+200 -6
View File
@@ -49,7 +49,7 @@ jobs:
documents:
if: >
!failure() && !cancelled()
&& contains(fromJSON(inputs.machines), fromJSON('["X64"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["X64"]')[0])
&& (github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/v'))
&& !contains(github.ref, '-draft')
@@ -70,16 +70,14 @@ jobs:
&& inputs.machines
&& inputs.docker_repo != ''
&& (github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/v'))
&& !contains(github.ref, '-draft')
name: Containers
runs-on: ${{matrix.machine}}
permissions: write-all
continue-on-error: false
strategy:
fail-fast: true
matrix:
bake_target: ${{fromJSON('["github", "dockerhub"]')}}
bake_target: ${{fromJSON('["docker_io", "ghcr_io"]')}}
cargo_profile: ${{fromJSON(inputs.cargo_profiles)}}
rust_toolchain: ${{fromJSON(inputs.rust_toolchains)}}
feat_set: ${{fromJSON(inputs.feat_sets)}}
@@ -92,6 +90,10 @@ jobs:
include: ${{fromJSON(inputs.includes)}}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: GitHub Login
uses: docker/login-action@v3
with:
@@ -120,8 +122,200 @@ jobs:
acct: ${{github.actor}}
repo: ${{github.repository}}
docker_repo: ${{inputs.docker_repo}}
docker_tag_preview: ${{ matrix.cargo_profile == 'release' && matrix.feat_set == 'all' && matrix.sys_target == 'x86_64-linux-gnu' && contains(github.ref, 'refs/tags/v') }}
docker_tag_latest: ${{ matrix.cargo_profile == 'release' && matrix.feat_set == 'all' && matrix.sys_target == 'x86_64-linux-gnu' && contains(github.ref, 'refs/tags/v') && !contains(github.ref, '-rc') }}
run: |
docker/bake.sh "${{matrix.bake_target}}"
bundles:
if: >
!failure() && !cancelled()
&& inputs.machines
&& inputs.docker_repo != ''
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["release"]')[0])
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["stable"]')[0])
&& (github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/v'))
name: Bundles
needs: [containers]
runs-on: 'het'
permissions: write-all
strategy:
fail-fast: true
matrix:
bake_target: ${{fromJSON('["docker_io", "ghcr_io"]')}}
cargo_profile: ${{fromJSON('["release"]')}}
feat_set: ${{fromJSON('["all"]')}}
rust_toolchain: ${{fromJSON('["stable"]')}}
sys_name: ${{fromJSON(inputs.sys_names)}}
sys_version: ${{fromJSON(inputs.sys_versions)}}
rust_target: ${{fromJSON(inputs.rust_targets)}}
sys_target: ${{fromJSON(inputs.sys_targets)}}
machine: ${{fromJSON(inputs.machines)}}
exclude: ${{fromJSON(inputs.excludes)}}
include: ${{fromJSON(inputs.includes)}}
steps:
- name: GitHub Login
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.ghcr_token }}
- name: DockerHub Login
uses: docker/login-action@v3
with:
registry: docker.io
username: ${{ github.actor }}
password: ${{ secrets.dockerhub_token }}
- name: Release Branch
if: >
!failure() && !cancelled()
&& (github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/v'))
env:
acct: ${{github.actor}}
ghcr_repo: ${{github.repository}}
docker_repo: ${{inputs.docker_repo}}
registry: ${{matrix.bake_target}}
cargo_profile: ${{matrix.cargo_profile}}
feat_set: ${{matrix.feat_set}}
sys_target: ${{matrix.sys_target}}
ref_name: ${{github.ref_name}}
run: |
registry="$(echo -n $registry | sed s/_/./)"
if test "$registry" = "ghcr.io"; then
repo="$ghcr_repo"
else
repo="$docker_repo"
fi
docker manifest create "${registry}/${repo}:${ref_name}" \
--amend "${registry}/${repo}:${ref_name}-${cargo_profile}-${feat_set}-${sys_target}"
- name: Release Preview
if: >
!failure() && !cancelled()
&& contains(github.ref, 'refs/tags/v')
&& !contains(github.ref, '-draft')
env:
acct: ${{github.actor}}
ghcr_repo: ${{github.repository}}
docker_repo: ${{inputs.docker_repo}}
registry: ${{matrix.bake_target}}
cargo_profile: ${{matrix.cargo_profile}}
feat_set: ${{matrix.feat_set}}
sys_target: ${{matrix.sys_target}}
ref_name: ${{github.ref_name}}
run: |
registry="$(echo -n $registry | sed s/_/./)"
if test "$registry" = "ghcr.io"; then
repo="$ghcr_repo"
else
repo="$docker_repo"
fi
docker manifest create "${registry}/${repo}:preview" \
--amend "${registry}/${repo}:${ref_name}-${cargo_profile}-${feat_set}-${sys_target}"
- name: Release Latest
if: >
!failure() && !cancelled()
&& contains(github.ref, 'refs/tags/v')
&& !contains(github.ref, '-draft')
&& !contains(github.ref, '-rc')
env:
acct: ${{github.actor}}
ghcr_repo: ${{github.repository}}
docker_repo: ${{inputs.docker_repo}}
registry: ${{matrix.bake_target}}
cargo_profile: ${{matrix.cargo_profile}}
feat_set: ${{matrix.feat_set}}
sys_target: ${{matrix.sys_target}}
ref_name: ${{github.ref_name}}
run: |
registry="$(echo -n $registry | sed s/_/./)"
if test "$registry" = "ghcr.io"; then
repo="$ghcr_repo"
else
repo="$docker_repo"
fi
docker manifest create "${registry}/${repo}:latest" \
--amend "${registry}/${repo}:${ref_name}-${cargo_profile}-${feat_set}-${sys_target}"
delivery:
if: >
!failure() && !cancelled()
&& inputs.machines
&& inputs.docker_repo != ''
&& (github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/v'))
name: Delivery
needs: [bundles, documents]
runs-on: 'het'
permissions: write-all
steps:
- name: GitHub Login
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.ghcr_token }}
- name: DockerHub Login
uses: docker/login-action@v3
with:
registry: docker.io
username: ${{ github.actor }}
password: ${{ secrets.dockerhub_token }}
- name: Push Branch
if: >
!failure() && !cancelled()
&& (github.ref == 'refs/heads/main' || contains(github.ref, 'refs/tags/v'))
env:
ref_name: ${{github.ref_name}}
ghcr_repo: ${{github.repository}}
docker_repo: ${{inputs.docker_repo}}
run: |
docker manifest push -p "ghcr.io/${ghcr_repo}:${ref_name}"
docker manifest push -p "docker.io/${docker_repo}:${ref_name}"
- name: Push Preview
if: >
!failure() && !cancelled()
&& contains(github.ref, 'refs/tags/v')
&& !contains(github.ref, '-draft')
env:
ghcr_repo: ${{github.repository}}
docker_repo: ${{inputs.docker_repo}}
run: |
docker manifest push -p "ghcr.io/${ghcr_repo}:preview"
docker manifest push -p "docker.io/${docker_repo}:preview"
- name: Push Latest
if: >
!failure() && !cancelled()
&& contains(github.ref, 'refs/tags/v')
&& !contains(github.ref, '-draft')
&& !contains(github.ref, '-rc')
env:
ghcr_repo: ${{github.repository}}
docker_repo: ${{inputs.docker_repo}}
run: |
docker manifest push -p "ghcr.io/${ghcr_repo}:latest"
docker manifest push -p "docker.io/${docker_repo}:latest"
+46 -17
View File
@@ -50,7 +50,7 @@ jobs:
contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
&& contains(fromJSON(inputs.machines), fromJSON('["X64"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
name: Unit
uses: ./.github/workflows/bake.yml
@@ -62,8 +62,8 @@ jobs:
sys_names: ${{inputs.sys_names}}
sys_versions: ${{inputs.sys_versions}}
rust_targets: ${{inputs.rust_targets}}
sys_targets: ${{inputs.sys_targets}}
machines: '["X64"]'
sys_targets: '["x86_64-v1-linux-gnu"]'
machines: ${{inputs.machines}}
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
@@ -92,6 +92,7 @@ jobs:
{"cargo_profile": "test", "feat_set": "none", "bake_target": "smoke-valgrind"},
{"cargo_profile": "test", "rust_toolchain": "stable"},
{"cargo_profile": "test", "rust_target": "aarch64-unknown-linux-gnu"},
{"cargo_profile": "test", "sys_target": "x86_64-v2-linux-gnu", "bake_target": "smoke-valgrind"},
{"cargo_profile": "release", "rust_toolchain": "nightly"},
{"cargo_profile": "release", "rust_toolchain": "stable", "feat_set": "none"},
{"cargo_profile": "release", "bake_target": "smoke-valgrind"},
@@ -104,12 +105,29 @@ jobs:
{"cargo_profile": "release-native", "feat_set": "default"},
{"cargo_profile": "release-native", "bake_target": "smoke-valgrind"},
{"cargo_profile": "release-native", "rust_target": "aarch64-unknown-linux-gnu"},
{"cargo_profile": "release-native", "sys_target": "x86_64-v2-linux-gnu"},
{"cargo_profile": "release-native", "sys_target": "x86_64-v3-linux-gnu"},
{"cargo_profile": "release-native", "sys_target": "x86_64-v4-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "none"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "default"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-linux-gnu"},
{"rust_target": "x86_64-unknown-linux-gnu", "sys_target": "aarch64-linux-gnu"},
{"sys_target": "x86_64-linux-gnu", "machine": "ARM64"},
{"sys_target": "aarch64-linux-gnu", "machine": "X64"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v1-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v2-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v3-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v4-linux-gnu"},
{"rust_target": "x86_64-unknown-linux-gnu", "sys_target": "aarch64-v8-linux-gnu"},
{"sys_target": "aarch64-v8-linux-gnu", "machine": "X64"},
{"sys_target": "x86_64-v1-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v2-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v2-linux-gnu", "feat_set": "none"},
{"sys_target": "x86_64-v2-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v3-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v3-linux-gnu", "feat_set": "none"},
{"sys_target": "x86_64-v3-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v3-linux-gnu", "bake_target": "smoke-valgrind"},
{"sys_target": "x86_64-v4-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v4-linux-gnu", "feat_set": "none"},
{"sys_target": "x86_64-v4-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v4-linux-gnu", "bake_target": "smoke-valgrind"},
]
complement:
@@ -119,7 +137,7 @@ jobs:
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
&& contains(fromJSON(inputs.complement_feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
&& contains(fromJSON(inputs.machines), fromJSON('["X64"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
name: Complement
uses: ./.github/workflows/bake.yml
@@ -131,8 +149,8 @@ jobs:
sys_names: ${{inputs.sys_names}}
sys_versions: ${{inputs.sys_versions}}
rust_targets: ${{inputs.rust_targets}}
sys_targets: ${{inputs.sys_targets}}
machines: '["X64"]'
sys_targets: '["x86_64-v1-linux-gnu"]'
machines: ${{inputs.machines}}
runner: ${{inputs.complement_runner}}
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
@@ -145,7 +163,7 @@ jobs:
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
&& contains(fromJSON(inputs.machines), fromJSON('["X64"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
name: Matrix Compliance
needs: [complement]
@@ -163,8 +181,8 @@ jobs:
sys_name: ${{fromJSON(inputs.sys_names)}}
sys_version: ${{fromJSON(inputs.sys_versions)}}
rust_target: ${{fromJSON(inputs.rust_targets)}}
sys_target: ${{fromJSON(inputs.sys_targets)}}
machine: ${{fromJSON('["X64"]')}}
sys_target: ${{fromJSON('["x86_64-v1-linux-gnu"]')}}
machine: ${{fromJSON(inputs.machines)}}
exclude: ${{fromJSON(inputs.excludes)}}
include: ${{fromJSON(inputs.includes)}}
@@ -185,7 +203,8 @@ jobs:
run: |
docker/complement.sh
- name: Extract
- if: success() || failure() && steps.execute.outcome == 'failure'
name: Extract
id: extract
env:
name: complement_tester__${{matrix.sys_name}}__${{matrix.sys_version}}__${{matrix.sys_target}}
@@ -195,13 +214,23 @@ jobs:
cid=$(cat "$name")
docker cp "$cid:/usr/src/complement/new_results.jsonl" tests/test_results/complement/test_results.jsonl
- name: Upload
id: upload
- if: success() || failure() && steps.execute.outcome == 'failure'
name: Upload New Results
id: upload-result
uses: actions/upload-artifact@v4
with:
name: test_results-${{matrix.feat_set}}-${{matrix.sys_name}}-${{matrix.sys_target}}.jsonl
name: complement_results-${{matrix.feat_set}}-${{matrix.sys_name}}-${{matrix.sys_target}}.jsonl
path: ./tests/test_results/complement/test_results.jsonl
- if: failure() && steps.execute.outcome == 'failure'
name: Upload Failure Output
id: upload-output
uses: actions/upload-artifact@v4
with:
name: complement_output-${{matrix.feat_set}}-${{matrix.sys_name}}-${{matrix.sys_target}}.jsonl
path: ./complement.jsonl
if-no-files-found: ignore
- name: Accept
id: accept
run: |
+7
View File
@@ -95,3 +95,10 @@ rustc-ice-*
# complement test logs are huge
tests/test_results/complement/test_logs.jsonl
# cargo profiles from -Z self-profile
*.mm_profdata
# Editor crashfiles
*.save
*.swp
Generated
+24 -11
View File
@@ -1977,6 +1977,17 @@ dependencies = [
"syn",
]
[[package]]
name = "io-uring"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
dependencies = [
"bitflags 2.9.1",
"cfg-if",
"libc",
]
[[package]]
name = "ipaddress"
version = "0.1.3"
@@ -4523,16 +4534,18 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.45.1"
version = "1.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
checksum = "1140bb80481756a8cbe10541f37433b459c5aa1e727b4c020fbfebdc25bf3ec4"
dependencies = [
"backtrace",
"bytes",
"io-uring",
"libc",
"mio",
"pin-project-lite",
"signal-hook-registry",
"slab",
"socket2",
"tokio-macros",
"tracing",
@@ -4793,7 +4806,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tuwunel"
version = "1.1.0"
version = "1.3.0"
dependencies = [
"clap",
"const-str",
@@ -4822,8 +4835,9 @@ dependencies = [
[[package]]
name = "tuwunel_admin"
version = "1.1.0"
version = "1.3.0"
dependencies = [
"base64",
"clap",
"const-str",
"ctor",
@@ -4845,7 +4859,7 @@ dependencies = [
[[package]]
name = "tuwunel_api"
version = "1.1.0"
version = "1.3.0"
dependencies = [
"async-trait",
"axum",
@@ -4878,7 +4892,7 @@ dependencies = [
[[package]]
name = "tuwunel_core"
version = "1.1.0"
version = "1.3.0"
dependencies = [
"argon2",
"arrayvec",
@@ -4903,7 +4917,6 @@ dependencies = [
"ipaddress",
"itertools 0.14.0",
"jsonwebtoken",
"ldap3",
"libc",
"libloading",
"log",
@@ -4938,7 +4951,7 @@ dependencies = [
[[package]]
name = "tuwunel_database"
version = "1.1.0"
version = "1.3.0"
dependencies = [
"async-channel",
"const-str",
@@ -4957,7 +4970,7 @@ dependencies = [
[[package]]
name = "tuwunel_macros"
version = "1.1.0"
version = "1.3.0"
dependencies = [
"itertools 0.14.0",
"proc-macro2",
@@ -4967,7 +4980,7 @@ dependencies = [
[[package]]
name = "tuwunel_router"
version = "1.1.0"
version = "1.3.0"
dependencies = [
"axum",
"axum-client-ip",
@@ -5001,7 +5014,7 @@ dependencies = [
[[package]]
name = "tuwunel_service"
version = "1.1.0"
version = "1.3.0"
dependencies = [
"async-trait",
"base64",
+16 -5
View File
@@ -28,8 +28,8 @@ keywords = [
license = "Apache-2.0"
readme = "README.md"
repository = "https://github.com/matrix-construct/tuwunel"
rust-version = "1.86.0"
version = "1.1.0"
rust-version = "1.88.0"
version = "1.3.0"
[workspace.metadata.crane]
name = "tuwunel"
@@ -362,11 +362,11 @@ git = "https://github.com/matrix-construct/rust-rocksdb"
rev = "225a42519276e502205bdc845cebdb22d70ee245"
default-features = false
features = [
"bzip2",
"lz4",
"multi-threaded-cf",
"mt_static",
"lz4",
"zstd",
"bzip2",
]
[workspace.dependencies.sanitize-filename]
@@ -487,7 +487,7 @@ features = [
]
[workspace.dependencies.tokio]
version = "1.45.1"
version = "1.46.0"
default-features = false
features = [
"fs",
@@ -856,6 +856,7 @@ unused-qualifications = "warn"
## some sadness
elided_named_lifetimes = "allow" # TODO!
mismatched_lifetime_syntaxes = "allow" # TODO!
let_underscore_drop = "allow"
missing_docs = "allow"
# cfgs cannot be limited to expected cfgs or their de facto non-transitive/opt-in use-case e.g.
@@ -871,6 +872,15 @@ variant_size_differences = "allow"
# we check nightly clippy lints
unknown_lints = "allow"
# rust claims elided_named_lifetimes has been replaced by
# mismatched_lifetime_syntaxes but it actually has not.
renamed_and_removed_lints = "allow"
# nightly has a new lint that conflicts with our BoolExt trait at
# src/core/utils/bool.rs:68:48
# <https://github.com/rust-lang/rust/issues/142748>
unstable_name_collisions = "allow" # TODO
#######################################
#
# Clippy lints
@@ -910,6 +920,7 @@ enum_glob_use = { level = "allow", priority = 1 }
if_not_else = { level = "allow", priority = 1 }
if_then_some_else_none = { level = "allow", priority = 1 }
inline_always = { level = "allow", priority = 1 }
map_unwrap_or = { level = "allow", priority = 1 }
match_bool = { level = "allow", priority = 1 }
missing_docs_in_private_items = { level = "allow", priority = 1 }
missing_errors_doc = { level = "allow", priority = 1 }
+8 -13
View File
@@ -1,4 +1,4 @@
# Tuwunel <sup>🎔</sup>
# Tuwunel<sup>💕</sup>
![GitHub License](https://img.shields.io/github/license/matrix-construct/tuwunel?style=flat&color=%238A2BE2)
![GitHub Created At](https://img.shields.io/github/created-at/matrix-construct/tuwunel?style=flat&color=%238A2BE2)
@@ -47,7 +47,7 @@ ### Getting Started
This is just a summary for the impatient. See the full
[documentation](https://matrix-construct.github.io/tuwunel/).
> [!WARNING]
> [!TIP]
> Avoid using a sub-domain for your `server_name`. You can always delegate later with a [`.well-known`](https://github.com/spantaleev/matrix-docker-ansible-deploy/blob/master/docs/configuring-well-known.md)
> file, but you can never change your `server_name`.
@@ -140,31 +140,26 @@ #### Container Tracking
### Getting Help & Support
The official community will be found at [#tuwunel:tuwunel.chat](https://matrix.to/#/#tuwunel:tuwunel.chat).
If this is currently inaccessible please be patient as it's still coming online at the time of
the first release; we will have updates to this section. This is a fully moderated space to protect
the wellbeing of our users and create a non-toxic work environment for staff. If you require assistance
with anything that is not remedied by the documentation, don't hesitate to open an issue on github.
If you are opposed to using github, or if private discussion is required such as for security
disclosures, or for any other reason, I would be happy to receive your DM at
[@jason:tuwunel.me](https://matrix.to/#/@jason:tuwunel.me). This will not be bothering me as it would
be my pleasure to help you anytime. As an emergency contact you can send an email to jasonzemos@gmail.com.
be my pleasure to help you when possible. As an emergency contact you can send an email to
jasonzemos@gmail.com.
##### Tuwunel Fanclub
We also have an unofficial community-run chat which is publicly accessible at
[#tuwunel:grin.hu](https://matrix.to/#/#tuwunel:grin.hu). The members, content, or moderation
We have an unofficial community-run chat which is publicly accessible at
[#tuwunel:matrix.org](https://matrix.to/#/#tuwunel:matrix.org). The members, content, or moderation
decisions of this room are not in any way related or endorsed by this project or its sponsors,
and not all project staff will be present there. There will be at least some presence by staff to
offer assistance so long as the room remains in minimally good standing.
## Tuwunel <sup>🎔</sup>
## Tuwunel<sup>💕</sup>
Tuwunel's theme is **empathy** in communication defined by the works of
[Edith Stein](https://plato.stanford.edu/entries/stein/). Empathy is the basis for how we approach
every message, and a reminder for how we should all conduct ourselves in every conversation.
every message and our responsibility to the other in every conversation.
<!-- ANCHOR_END: body -->
+47 -25
View File
@@ -1,37 +1,59 @@
# Tuwunel 1.1.0
# Tuwunel 1.3.0
June 19, 2025
August 4, 2025
All dependencies have been fully upgraded for the first time since the
conduwuit transition. RocksDB is now synchronized to 10.2.1-tuwunel for all
builders. The Nix build itself has now been fully migrated from conduwuit;
special thanks to @wkordalski for making this happen. Thanks to @Askhalion
for opening a NixOS package request which you can [vote for here](https://github.com/NixOS/nixpkgs/issues/415469).
An [Arch package](https://aur.archlinux.org/packages/tuwunel) has also been
created in the AUR courtesy of @drrossum in addition to the transitional
package setup by @Kimiblock which we failed to acknowledge during the first
release. The RPM package now has systemd and proper installation added thanks
to a report by @alythemonk.
Preparations for [Project Hydra](https://matrix.org/blog/2025/07/security-predisclosure/) have taken place. Users should be prepared to upgrade to `v1.4.0-rc` next week. Due to the comprehensive low-level changes which have taken place, and the inability to develop them in public with your feedback, the release will undergo an extended `-rc` period. Due to the time difference and scheduling conflicts our release may be published between 12 to 36 hours after the suggested time; though well before the written disclosure of the issues.
ARMv8 builds are now supported and bundled with this release. Thanks to
@zaninime and @clement-escolano for reminding us.
Some foundational work for `v1.4.0` was backported into this release after the announcement was made mid-July to further extend Hydra. An unexpected performance improvement drastically reduced CPU usage. As a result, integration tests began to flake, uncovering latent concurrency bugs which were addressed. These fixes primarily impact the legacy and sliding-sync systems, and further fixes improve performance and compliance, though mostly in the legacy system as sliding-sync lacks adequate test coverage.
This release fixes many bugs and improves performance but at the cost of planned features further rescheduled to either `v1.4.1` or `v1.5.0`.
JSON Web Token logins are now supported. This feature was commissioned and
made public by an enterprise sponsor. The type `org.matrix.login.jwt` is now
recognized.
### New Features
- JWT login support.
- Refresh tokens (MSC2918) have been implemented. Supporting clients can now timeout their access tokens with a soft-logout after a configured `access_token_ttl`. This feature was commissioned and made public by an enterprise sponsor.
### Follow-up Features
- Typing indicators have been added to sliding-sync thanks to @tmayoff. This feature (and the whole of sliding-sync) is still experimental and the indicator may not always appear or disappear as intended, nevertheless the effort will be enhanced by foundational fixes improving sliding-sync requested soon by the project's sponsor.
### Enhancements
- @dasha_uwu maintains their streak as a serial contributor by patching the `!admin query raw` command with a base64 option allowing for low-level debugging of database records.
- @obioma has improved the documentation explaining how to use multiple configuration files with precedence.
- Upon recommendation of @grin a basic request ID has been added to the tracing logs to uniquely distinguish each request while it's interleaved among others.
- Requested by @fruzitent this and future releases are tagged by version as multi-arch docker images to be properly archived in the registry rather than simply overwriting `:latest`.
- Event processing performance has been improved by fetching and processing `prev_events` and `auth_events` concurrently. This reduces the impact of recursing large graphs while the room's mutex is locked.
- An experimental command `!admin debug resync-database` has been added for developers curious about #35
- aarch64 build and packages.
- NixOS build support. (thanks @wkordalski and @coolGi69)
- Dependency upgrades, including Axum 0.8. (thanks @dasha_uwu)
- RPM package systemd and proper installation scripts.
### Bug Fixes
- Changing passwords for pre-migration users was precluded by an error.
Special thanks to @teidesu for making a superb report about this.
- Thanks to @tmayoff room avatars are properly calculated and no longer the same for all spaces (https://github.com/matrix-construct/tuwunel/pull/102).
- Courtesy of @coolGi69 our bump to Rust 1.88 was properly updated for Nix. Apologies to the Nix community for getting this wrong the first time.
- Invite rejections have been fixed, this was due to a misinterpretation of the spec in legacy sync.
- Knock rooms might have been buggy from the ambiguous overuse of the word "count" in the codebase. Database records which expected a summation of the users in a room instead received the sequence number of the server, both are called "count."
- Room knocks failed to wakeup the sync systems; some cases of account_data changes also failed to wakeup the sync systems. These have been addressed.
- The main sequence number fundamental to the entire server's operation (the "count" or counter) has been refactored after having exceeded architectural limitations. It has been replaced by a two-phase counter ensuring read-after-write consistency, and quasi-transactions grouping multiple writes.
- Sequence issues have been addressed in both legacy and sliding sync. These systems operate using a "snapshot" approach which intentionally ignores new data received by the server after the sync request has started; the snapshot approach replaced the complex of mutexes used by Conduit. The server was not originally designed for this approach and some information "from the future" continued to leak into the snapshot's window; these leaks have been sealed.
- Protocol compliance issues in legacy sync have been addressed. Additional compliance tests for device list updates now pass. The `state` and `timeline` on incremental sync provide expected results in more (if not all) cases.
- Errors requiring M_BAD_ALIAS instead of M_UNKNOWN when sending `m.room.canonical_alias` are now conforming.
### Deprecations
- Unauthenticated media fallbacks are no longer requested by default. This can still be enabled with `request_legacy_media` if desired.
- Legacy Sliding-Sync has been removed in favor of Simplified Sliding-Sync. Clients which exclusively using Sliding-Sync have already migrated around the start of this year, so this removal should have no impact now.
+2 -2
View File
@@ -1,7 +1,7 @@
array-size-threshold = 4096
cognitive-complexity-threshold = 100 # TODO reduce me ALARA
excessive-nesting-threshold = 11 # TODO reduce me to 4 or 5
future-size-threshold = 7745 # TODO reduce me ALARA
excessive-nesting-threshold = 8
future-size-threshold = 8192
stack-size-threshold = 196608 # TODO reduce me ALARA
too-many-lines-threshold = 780 # TODO reduce me to <= 100
type-complexity-threshold = 250 # reduce me to ~200
+5 -5
View File
@@ -1,7 +1,7 @@
# syntax = docker/dockerfile:1.11-labs
FROM input AS cargo
ARG sys_target
ARG sys_triple
ARG rust_target
ARG rust_toolchain
ARG RUST_HOME
@@ -38,7 +38,7 @@ COPY --link --from=input . .
COPY --link --from=rust ${RUST_HOME} ${RUST_HOME}
COPY --link --from=deps /usr/src/tuwunel /usr/src/tuwunel
WORKDIR /usr/lib/${sys_target}
WORKDIR /usr/lib/${sys_triple}
COPY --link --from=rocksdb . .
WORKDIR /usr/src/tuwunel
@@ -59,9 +59,9 @@ ENV CARGO_TARGET_DIR="${CARGO_TARGET_DIR}"
ENV targ_dir="${CARGO_TARGET_DIR}/${cargo_target_profile}"
ENV targ_targ_dir="${CARGO_TARGET_DIR}/${rust_target}/${cargo_target_profile}"
RUN \
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared,ro \
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared,ro \
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared,ro \
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared \
--mount=type=cache,dst=${targ_dir}/deps,id=${cargo_target_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_dir}/build,id=${cargo_target_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_dir}/examples,id=${cargo_target_share}/examples,sharing=locked \
+3 -4
View File
@@ -1,7 +1,6 @@
# syntax = docker/dockerfile:1.11-labs
FROM input AS audit
ARG sys_target
ARG rust_toolchain="nightly"
ARG RUSTUP_HOME
ARG CARGO_HOME
@@ -12,9 +11,9 @@ COPY --link --from=input . .
WORKDIR /usr/src/tuwunel
RUN \
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared,ro \
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared,ro \
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared,ro \
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared \
<<EOF
set -eux
rustup run ${rust_toolchain} \
-1
View File
@@ -1,7 +1,6 @@
# syntax = docker/dockerfile:1.11-labs
FROM input AS build-deb
ARG sys_target
ARG rust_target
ARG rust_toolchain
ARG RUSTUP_HOME
+3 -4
View File
@@ -1,7 +1,6 @@
# syntax = docker/dockerfile:1.11-labs
FROM input AS fmt
ARG sys_target
ARG rust_toolchain="nightly"
ARG RUSTUP_HOME
ARG CARGO_HOME
@@ -12,9 +11,9 @@ COPY --link --from=input . .
WORKDIR /usr/src/tuwunel
RUN \
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared,ro \
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared,ro \
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared,ro \
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared \
<<EOF
set -eux
rustup run ${rust_toolchain} \
+3 -4
View File
@@ -1,7 +1,6 @@
# syntax = docker/dockerfile:1.11-labs
FROM input AS lychee
ARG sys_target
ARG rust_toolchain="nightly"
ARG RUSTUP_HOME
ARG CARGO_HOME
@@ -16,9 +15,9 @@ COPY --link --from=input . .
WORKDIR /usr/src/
RUN \
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared,ro \
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared,ro \
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared,ro \
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared \
--mount=type=cache,dst=lychee,sharing=locked \
<<EOF
set -eux
-1
View File
@@ -1,7 +1,6 @@
# syntax = docker/dockerfile:1.11-labs
FROM input AS build-rpm
ARG sys_target
ARG rust_toolchain
ARG RUSTUP_HOME
ARG CARGO_HOME
+3 -3
View File
@@ -11,9 +11,9 @@ COPY --link --from=input . .
WORKDIR /usr/src/tuwunel
RUN \
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared,ro \
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared,ro \
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared,ro \
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared \
<<EOF
set -eux
rustup run ${rust_toolchain} \
+48 -44
View File
@@ -66,49 +66,51 @@ COPY --from=source /usr/src/tuwunel/tests/test_results/complement/test_results.j
COPY <<EOF complement.toml
[global]
address = "0.0.0.0"
admin_room_notices = false
allow_check_for_updates = false
allow_device_name_federation = true
allow_guest_registration = true
allow_invalid_tls_certificates = true
allow_legacy_media = true
allow_public_room_directory_over_federation = true
allow_public_room_directory_without_auth = true
allow_registration = true
create_admin_room = false
database_path = "/database"
log = "debug,tuwunel=trace,h2=warn,hyper=warn"
port = [8008, 8448]
trusted_servers = []
only_query_trusted_key_servers = false
query_trusted_key_servers_first = false
query_trusted_key_servers_first_on_join = false
yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true
dns_attempts = 20
dns_timeout = 60
federation_idle_timeout = 300
intentionally_unknown_config_option_for_testing = true
ip_range_denylist = []
url_preview_domain_contains_allowlist = ["*"]
url_preview_domain_explicit_denylist = ["*"]
log = "debug,tuwunel=trace,h2=warn,hyper=warn"
log_colors = false
log_guest_registrations = false
log_span_events = "NONE"
log_thread_ids = true
media_compat_file_link = false
media_startup_check = true
only_query_trusted_key_servers = false
port = [8008, 8448]
prune_missing_media = true
log_colors = false
admin_room_notices = false
allow_check_for_updates = false
intentionally_unknown_config_option_for_testing = true
rocksdb_log_level = "debug"
rocksdb_max_log_files = 1
rocksdb_recovery_mode = 0
rocksdb_paranoid_file_checks = true
log_guest_registrations = false
allow_legacy_media = true
startup_netburst = true
startup_netburst_keep = -1
# valgrind makes things so slow
dns_timeout = 60
dns_attempts = 20
query_trusted_key_servers_first = false
query_trusted_key_servers_first_on_join = false
request_conn_timeout = 60
request_timeout = 120
well_known_conn_timeout = 60
well_known_timeout = 60
federation_idle_timeout = 300
sender_timeout = 300
rocksdb_log_level = "debug"
rocksdb_max_log_files = 1
rocksdb_paranoid_file_checks = true
rocksdb_recovery_mode = 0
sender_idle_timeout = 300
sender_retry_backoff_limit = 300
allow_invalid_tls_certificates = true
sender_timeout = 300
startup_netburst = true
startup_netburst_keep = -1
trusted_servers = []
url_preview_domain_contains_allowlist = ["*"]
url_preview_domain_explicit_denylist = ["*"]
well_known_conn_timeout = 60
well_known_timeout = 60
yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true
[global.tls]
certs = "/complement/certificate.crt"
@@ -150,7 +152,7 @@ ENTRYPOINT valgrind \
FROM input AS complement-base
ARG var_cache
ARG var_lib_apt
ARG complement_tags="tuwunel_blacklist"
ARG complement_tags="conduwuit_blacklist"
ARG complement_tests="./tests/..."
ARG complement_run=".*"
@@ -165,7 +167,7 @@ RUN \
EOF
WORKDIR /usr/src
ADD https://github.com/matrix-construct/complement.git complement
ADD https://github.com/matrix-construct/complement.git#81f3b61e10ec102a36921890c81b79564ebf6f40 complement
WORKDIR /usr/src/complement
ENV COMPLEMENT_BASE_IMAGE="complement-testee"
@@ -211,6 +213,7 @@ ENV COMPLEMENT_HOST_MOUNTS="/var/run/docker.sock:/var/run/docker.sock"
ENV jq_res='{Action: .Action, Test: .Test}'
ENV jq_sel='select((.Action == \"pass\" or .Action == \"fail\" or .Action == \"skip\") and .Test != null)'
ENV jq_tab='[.Action, .Test] | @tsv'
ENV jq_out='select(.Test != null) | {Test: .Test, Output: .Output}'
COPY --from=complement-config /complement/old_results.jsonl .
COPY <<EOF uwu.sh
env;
@@ -219,22 +222,23 @@ COPY <<EOF uwu.sh
COMPLEMENT_BASE_IMAGE="\${1:-$complement_base_image}"
go test
-json
-shuffle="${complement_shuffle}"
-parallel="${complement_parallel}"
-timeout="${complement_timeout}"
-count="${complement_count}"
-tags="${complement_tags}"
-skip="${complement_skip}"
-run="${complement_run}"
"${complement_tests}"
| jq --unbuffered -c "${jq_sel} | ${jq_res}"
"-shuffle=\$complement_shuffle"
"-parallel=\$complement_parallel"
"-timeout=\$complement_timeout"
"-count=\$complement_count"
"-tags=\$complement_tags"
"-skip=\$complement_skip"
"-run=\$complement_run"
"\$complement_tests"
| jq --unbuffered -c "."
| tee output.jsonl
| jq --unbuffered -c "$jq_sel | $jq_res"
| tee results.jsonl
| jq --unbuffered -r "${jq_tab}"
| jq --unbuffered -r "$jq_tab"
;
jq -s -c "sort_by(.Test)[]" < results.jsonl | uniq > new_results.jsonl;
wc -l old_results.jsonl new_results.jsonl;
diff -w -y -t --width=275 --suppress-common-lines old_results.jsonl new_results.jsonl;
jq -s -c "sort_by(.Test, .Timestamp)[] | $jq_out" < output.jsonl > full_output.jsonl;
EOF
RUN echo $(tr -d '\n' < uwu.sh) > uwu.sh
RUN echo $(tr -d '\n' < uwu.sh) > uwu.sh && chmod a+x uwu.sh
ENTRYPOINT ["/bin/bash", "/usr/src/complement/uwu.sh"]
+2 -2
View File
@@ -26,10 +26,10 @@ EOF
FROM input AS rocksdb-build
ARG rocksdb_shared=0
ARG rocksdb_portable=1
ARG rocksdb_portable="1"
ARG rocksdb_opt_level="3"
ARG rocksdb_build_type="Release"
ARG rocksdb_cxx_flags="-ftls-model=initial-exec -mno-outline-atomics"
ARG rocksdb_cxx_flags="-ftls-model=initial-exec"
ARG rocksdb_make_verbose="ON"
ARG rocksdb_make_rule_messages="OFF"
ARG rocksdb_jemalloc=1
+2 -2
View File
@@ -16,7 +16,7 @@ RUN <<EOF
var="${rustup_version}/${rust_target}"
url="https://static.rust-lang.org/rustup/archive/$var/rustup-init"
curl -S -O -s "$url"
chmod o+x rustup-init
chmod a+rx rustup-init
EOF
FROM input AS rust
@@ -54,7 +54,7 @@ RUN \
--no-update-default-toolchain \
--no-modify-path
chmod -R go+rw $CARGO_HOME $RUSTUP_HOME
chmod -R a+rw $CARGO_HOME $RUSTUP_HOME
rm rustup-init
rustup component add \
-1
View File
@@ -18,7 +18,6 @@ EOF
FROM input AS ingredients
ARG sys_target
ARG rust_target
ARG rust_toolchain
ARG RUST_HOME
+140 -49
View File
@@ -16,12 +16,6 @@ variable "repo" {
variable "docker_repo" {
default = "${repo}"
}
variable "docker_tag_preview" {
default = false
}
variable "docker_tag_latest" {
default = false
}
variable "git_ref" {
default = "${GITHUB_REF}"
@@ -36,7 +30,7 @@ variable "git_ref_name" {
cargo_feat_sets = {
none = ""
default = "brotli_compression,element_hacks,gzip_compression,io_uring,jemalloc,jemalloc_conf,media_thumbnail,release_max_log_level,systemd,url_preview,zstd_compression"
all = "blurhashing,brotli_compression,tuwunel_mods,console,default,direct_tls,element_hacks,gzip_compression,hardened_malloc,io_uring,jemalloc,jemalloc_conf,jemalloc_prof,jemalloc_stats,ldap,media_thumbnail,perf_measurements,release_max_log_level,sentry_telemetry,systemd,tokio_console,url_preview,zstd_compression"
all = "blurhashing,brotli_compression,bzip2_compression,tuwunel_mods,console,default,direct_tls,element_hacks,gzip_compression,hardened_malloc,io_uring,jemalloc,jemalloc_conf,jemalloc_prof,jemalloc_stats,ldap,lz4_compression,media_thumbnail,perf_measurements,release_max_log_level,sentry_telemetry,systemd,tokio_console,url_preview,zstd_compression"
}
variable "cargo_features_always" {
default = "direct_tls"
@@ -63,19 +57,19 @@ variable "rust_targets" {
default = "[\"x86_64-unknown-linux-gnu\"]"
}
variable "sys_targets" {
default = "[\"x86_64-linux-gnu\"]"
variable "sys_names" {
default = "[\"debian\"]"
}
variable "sys_versions" {
default = "[\"testing-slim\"]"
}
variable "sys_names" {
default = "[\"debian\"]"
variable "sys_targets" {
default = "[\"x86_64-v1-linux-gnu\"]"
}
# RocksDB options
variable "rocksdb_portable" {
default = 1
default = "1"
}
variable "rocksdb_opt_level" {
default = "3"
@@ -279,28 +273,24 @@ group "publish" {
]
}
target "github" {
target "ghcr_io" {
name = elem("github", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
"ghcr.io/${repo}:${git_ref_name}-${cargo_profile}-${feat_set}-${sys_target}",
docker_tag_preview? "ghcr.io/${repo}:preview": "",
docker_tag_latest? "ghcr.io/${repo}:latest": "",
]
output = ["type=registry,compression=zstd,mode=min,compression-level=${zstd_image_compress_level}"]
output = ["type=registry,oci-mediatypes=true,compression=zstd,compression-level=${zstd_image_compress_level},force-compression=true,mode=min"]
matrix = cargo_rust_feat_sys
inherits = [
elem("docker", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
]
}
target "dockerhub" {
target "docker_io" {
name = elem("dockerhub", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
"${docker_repo}:${git_ref_name}-${cargo_profile}-${feat_set}-${sys_target}",
docker_tag_preview? "${docker_repo}:preview": "",
docker_tag_latest? "${docker_repo}:latest": "",
"docker.io/${docker_repo}:${git_ref_name}-${cargo_profile}-${feat_set}-${sys_target}",
]
output = ["type=registry,compression=zstd,mode=min,compression-level=${zstd_image_compress_level}"]
output = ["type=registry,oci-mediatypes=true,compression=zstd,compression-level=${zstd_image_compress_level},force-compression=true,mode=min"]
matrix = cargo_rust_feat_sys
inherits = [
elem("docker", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
@@ -534,7 +524,7 @@ group "installs" {
install_labels = {
"org.opencontainers.image.authors" = "${package_authors}"
"org.opencontainers.image.created" ="${package_last_modified}"
"org.opencontainers.image.created" = "${package_last_modified}"
"org.opencontainers.image.description" = "Matrix Chat Server in Rust"
"org.opencontainers.image.documentation" = "https://github.com/matrix-construct/tuwunel/tree/main/docs/"
"org.opencontainers.image.licenses" = "Apache-2.0"
@@ -546,12 +536,26 @@ install_labels = {
"org.opencontainers.image.version" = "${package_version}"
}
install_annotations = [
"org.opencontainers.image.authors=${package_authors}",
"org.opencontainers.image.created=${package_last_modified}",
"org.opencontainers.image.description=Matrix Chat Server in Rust",
"org.opencontainers.image.documentation=https://github.com/matrix-construct/tuwunel/tree/main/docs/",
"org.opencontainers.image.licenses=Apache-2.0",
"org.opencontainers.image.revision=${package_revision}",
"org.opencontainers.image.source=https://github.com/matrix-construct/tuwunel",
"org.opencontainers.image.title=${package_name}",
"org.opencontainers.image.url=https://github.com/matrix-construct/tuwunel",
"org.opencontainers.image.vendor=matrix-construct",
"org.opencontainers.image.version=${package_version}",
]
target "oci" {
name = elem("oci", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("oci", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
output = ["type=oci,dest=tuwunel-oci.tar.zst,mode=min,compression=zstd,compression-level=${zstd_image_compress_level}"]
output = ["type=oci,dest=tuwunel-oci.tar.zst,compression=zstd,compression-level=${zstd_image_compress_level},force-compression=true,mode=min"]
matrix = cargo_rust_feat_sys
inherits = [
elem("docker", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
@@ -563,7 +567,7 @@ target "docker" {
tags = [
elem_tag("docker", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
output = ["type=docker,compression=gzip,mode=min,compression-level=${gz_image_compress_level}"]
output = ["type=docker,compression=zstd,compression-level=${zstd_image_compress_level},force-compression=true,mode=min"]
matrix = cargo_rust_feat_sys
inherits = [
elem("static", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
@@ -643,7 +647,8 @@ target "install" {
elem_tag("install", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
labels = install_labels
output = ["type=docker,compression=zstd,mode=min,compression-level=${zstd_image_compress_level}"]
annotations = install_annotations
output = ["type=docker,compression=zstd,mode=max,compression-level=${zstd_image_compress_level}"]
cache_to = ["type=local,compression=zstd,mode=max,compression-level=${cache_compress_level}"]
dockerfile = "${docker_dir}/Dockerfile.install"
target = "install"
@@ -1195,13 +1200,17 @@ target "deps-base" {
join(" ", static_rustflags),
join(" ", static_nightly_rustflags),
join(" ", native_rustflags),
"-C link-arg=-L/usr/lib/gcc/${sys_target}/14", #FIXME
"-C link-arg=-L/usr/lib/gcc/${sys_target_triple(sys_target)}/14", #FIXME
contains(split(",", cargo_feat_sets[feat_set]), "bzip2_compression")?
"-C link-arg=-l:libbz2.a": "",
contains(split(",", cargo_feat_sets[feat_set]), "lz4_compression")?
"-C link-arg=-l:liblz4.a": "",
contains(split(",", cargo_feat_sets[feat_set]), "zstd_compression")?
"-C link-arg=-l:libzstd.a": "",
contains(split(",", cargo_feat_sets[feat_set]), "io_uring")?
"-C link-arg=-l:liburing.a": "",
join(" ", static_libs),
sys_target == "aarch64-linux-gnu"?
sys_target_triple(sys_target) == "aarch64-linux-gnu"?
"-C link-arg=-l:libgcc.a": "",
]):
@@ -1211,13 +1220,19 @@ target "deps-base" {
join(" ", nightly_rustflags),
join(" ", static_rustflags),
join(" ", static_nightly_rustflags),
"-C link-arg=-L/usr/lib/gcc/${sys_target}/14", #FIXME
sys_target_triple(sys_target) == "x86_64-linux-gnu"?
"-C target-cpu=${sys_target_isa(sys_target)}": "",
"-C link-arg=-L/usr/lib/gcc/${sys_target_triple(sys_target)}/14", #FIXME
contains(split(",", cargo_feat_sets[feat_set]), "bzip2_compression")?
"-C link-arg=-l:libbz2.a": "",
contains(split(",", cargo_feat_sets[feat_set]), "lz4_compression")?
"-C link-arg=-l:liblz4.a": "",
contains(split(",", cargo_feat_sets[feat_set]), "zstd_compression")?
"-C link-arg=-l:libzstd.a": "",
contains(split(",", cargo_feat_sets[feat_set]), "io_uring")?
"-C link-arg=-l:liburing.a": "",
join(" ", static_libs),
sys_target == "aarch64-linux-gnu"?
sys_target_triple(sys_target) == "aarch64-linux-gnu"?
"-C link-arg=-l:libgcc.a": "",
]):
@@ -1225,13 +1240,19 @@ target "deps-base" {
join(" ", [
join(" ", rustflags),
join(" ", static_rustflags),
"-C link-arg=-L/usr/lib/gcc/${sys_target}/14", #FIXME
sys_target_triple(sys_target) == "x86_64-linux-gnu"?
"-C target-cpu=${sys_target_isa(sys_target)}": "",
"-C link-arg=-L/usr/lib/gcc/${sys_target_triple(sys_target)}/14", #FIXME
contains(split(",", cargo_feat_sets[feat_set]), "bzip2_compression")?
"-C link-arg=-l:libbz2.a": "",
contains(split(",", cargo_feat_sets[feat_set]), "lz4_compression")?
"-C link-arg=-l:liblz4.a": "",
contains(split(",", cargo_feat_sets[feat_set]), "zstd_compression")?
"-C link-arg=-l:libzstd.a": "",
contains(split(",", cargo_feat_sets[feat_set]), "io_uring")?
"-C link-arg=-l:liburing.a": "",
join(" ", static_libs),
sys_target == "aarch64-linux-gnu"?
sys_target_triple(sys_target) == "aarch64-linux-gnu"?
"-C link-arg=-l:libgcc.a": "",
]):
@@ -1239,13 +1260,19 @@ target "deps-base" {
join(" ", [
join(" ", rustflags),
join(" ", static_rustflags),
"-C link-arg=-L/usr/lib/gcc/${sys_target}/14", #FIXME
sys_target_triple(sys_target) == "x86_64-linux-gnu"?
"-C target-cpu=${sys_target_isa(sys_target)}": "",
"-C link-arg=-L/usr/lib/gcc/${sys_target_triple(sys_target)}/14", #FIXME
contains(split(",", cargo_feat_sets[feat_set]), "bzip2_compression")?
"-C link-arg=-l:libbz2.a": "",
contains(split(",", cargo_feat_sets[feat_set]), "lz4_compression")?
"-C link-arg=-l:liblz4.a": "",
contains(split(",", cargo_feat_sets[feat_set]), "zstd_compression")?
"-C link-arg=-l:libzstd.a": "",
contains(split(",", cargo_feat_sets[feat_set]), "io_uring")?
"-C link-arg=-l:liburing.a": "",
join(" ", static_libs),
sys_target == "aarch64-linux-gnu"?
sys_target_triple(sys_target) == "aarch64-linux-gnu"?
"-C link-arg=-l:libgcc.a": "",
]):
@@ -1254,6 +1281,12 @@ target "deps-base" {
join(" ", rustflags),
join(" ", nightly_rustflags),
join(" ", dynamic_rustflags),
sys_target_triple(sys_target) == "x86_64-linux-gnu"?
"-C target-cpu=${sys_target_isa(sys_target)}": "",
contains(split(",", cargo_feat_sets[feat_set]), "bzip2_compression")?
"-C link-arg=-lbz2": "",
contains(split(",", cargo_feat_sets[feat_set]), "lz4_compression")?
"-C link-arg=-llz4": "",
contains(split(",", cargo_feat_sets[feat_set]), "zstd_compression")?
"-C link-arg=-lzstd": "",
contains(split(",", cargo_feat_sets[feat_set]), "io_uring")?
@@ -1303,13 +1336,32 @@ target "rocksdb-build" {
input = elem("target:kitchen", [feat_set, sys_name, sys_version, sys_target])
}
args = {
rocksdb_bz2 = contains(split(",", cargo_feat_sets[feat_set]), "bzip2_compression")? 1: 0
rocksdb_lz4 = contains(split(",", cargo_feat_sets[feat_set]), "lz4_compression")? 1: 0
rocksdb_zstd = contains(split(",", cargo_feat_sets[feat_set]), "zstd_compression")? 1: 0
rocksdb_jemalloc = contains(split(",", cargo_feat_sets[feat_set]), "jemalloc")? 1: 0
rocksdb_iouring = contains(split(",", cargo_feat_sets[feat_set]), "io_uring")? 1: 0
rocksdb_portable = cargo_profile == "release_max_perf"? 0: rocksdb_portable
rocksdb_build_type = rocksdb_build_type
rocksdb_opt_level = rocksdb_opt_level
rocksdb_shared = 0
rocksdb_opt_level = rocksdb_opt_level
rocksdb_build_type = rocksdb_build_type
rocksdb_cxx_flags = (
cargo_profile == "release-native" && sys_target_triple(sys_target) == "aarch64-linux-gnu"?
"-ftls-model=local-exec -mno-outline-atomics":
cargo_profile == "release-native"?
"-ftls-model=local-exec":
sys_target_triple(sys_target) == "aarch64-linux-gnu"?
"-ftls-model=initial-exec -mno-outline-atomics":
sys_target_triple(sys_target) == "x86_64-linux-gnu" && sys_target != "x86_64-v1-linux-gnu"?
"-ftls-model=initial-exec -mpclmul":
"-ftls-model=initial-exec"
)
rocksdb_portable = (
cargo_profile == "release-native"?
"0":
sys_target_triple(sys_target) == "x86_64-linux-gnu"?
"${sys_target_isa(sys_target)}":
rocksdb_portable
)
}
}
@@ -1405,8 +1457,8 @@ target "ingredients" {
"--all-features": "--no-default-features"
)
RUST_BACKTRACE = "full"
ROCKSDB_LIB_DIR="/usr/lib/${sys_target}"
JEMALLOC_OVERRIDE="/usr/lib/${sys_target}/libjemalloc.a"
ROCKSDB_LIB_DIR="/usr/lib/${sys_target_triple(sys_target)}"
JEMALLOC_OVERRIDE="/usr/lib/${sys_target_triple(sys_target)}/libjemalloc.a"
ZSTD_SYS_USE_PKG_CONFIG = (
contains(split(",", cargo_feat_sets[feat_set]), "zstd_compression")? 1: 0
)
@@ -1487,8 +1539,8 @@ target "rust" {
cargo_installs = join(" ", cargo_installs)
CARGO_TERM_VERBOSE = CARGO_TERM_VERBOSE
RUSTUP_HOME = "/opt/rust/rustup/${sys_name}/${sys_target}"
CARGO_HOME = "/opt/rust/cargo/${sys_name}/${sys_target}"
RUSTUP_HOME = "/opt/rust/rustup/${sys_name}/${sys_target_triple(sys_target)}"
CARGO_HOME = "/opt/rust/cargo/${sys_name}/${sys_target_triple(sys_target)}"
}
}
@@ -1536,6 +1588,7 @@ kitchen_packages = [
"cmake",
"curl",
"git",
"gzip",
"libc6-dev",
"libssl-dev",
"make",
@@ -1559,10 +1612,12 @@ target "kitchen" {
}
args = {
packages = join(" ", [
contains(split(",", cargo_feat_sets[feat_set]), "io_uring")? "liburing-dev": "",
contains(split(",", cargo_feat_sets[feat_set]), "zstd_compression")? "libzstd-dev": "",
contains(split(",", cargo_feat_sets[feat_set]), "jemalloc")? "libjemalloc-dev": "",
contains(split(",", cargo_feat_sets[feat_set]), "bzip2_compression")? "libbz2-dev": "",
contains(split(",", cargo_feat_sets[feat_set]), "hardened_malloc")? "g++": "",
contains(split(",", cargo_feat_sets[feat_set]), "io_uring")? "liburing-dev": "",
contains(split(",", cargo_feat_sets[feat_set]), "jemalloc")? "libjemalloc-dev": "",
contains(split(",", cargo_feat_sets[feat_set]), "lz4_compression")? "liblz4-dev": "",
contains(split(",", cargo_feat_sets[feat_set]), "zstd_compression")? "libzstd-dev": "",
])
}
}
@@ -1662,18 +1717,18 @@ target "runtime" {
}
args = {
packages = join(" ", [
contains(split(",", cargo_feat_sets[feat_set]), "bzip2_compression")? "bzip2": "",
contains(split(",", cargo_feat_sets[feat_set]), "io_uring")? "liburing2": "",
contains(split(",", cargo_feat_sets[feat_set]), "zstd_compression")? "libzstd1": "",
contains(split(",", cargo_feat_sets[feat_set]), "jemalloc")? "libjemalloc2": "",
contains(split(",", cargo_feat_sets[feat_set]), "lz4_compression")? "liblz4-1": "",
contains(split(",", cargo_feat_sets[feat_set]), "zstd_compression")? "libzstd1": "",
])
}
}
base_pkgs = [
"adduser",
"bzip2",
"ca-certificates",
"gzip",
]
target "base" {
@@ -1692,8 +1747,8 @@ target "base" {
}
args = {
DEBIAN_FRONTEND="noninteractive"
var_lib_apt = "/var/lib/apt/${sys_name}/${sys_version}/${sys_target}"
var_cache = "/var/cache/${sys_name}/${sys_version}/${sys_target}"
var_lib_apt = "/var/lib/apt/${sys_name}/${sys_version}/${sys_target_triple(sys_target)}"
var_cache = "/var/cache/${sys_name}/${sys_version}/${sys_target_triple(sys_target)}"
packages = join(" ", base_pkgs)
}
}
@@ -1713,12 +1768,21 @@ target "system" {
cache_to = ["type=local,compression=zstd,mode=max,compression-level=${cache_compress_level}"]
cache_from = ["type=local"]
dockerfile = "${docker_dir}/Dockerfile.system"
matrix = sys
context = "."
matrix = sys
platforms = (
sys_target_triple(sys_target) == "x86_64-linux-gnu"?
["linux/amd64/${sys_target_ver(sys_target)}"]:
sys_target_triple(sys_target) == "aarch64-linux-gnu"?
["linux/arm64"]:
["local"]
)
args = {
sys_name = sys_name
sys_version = sys_version
sys_target = sys_target
sys_triple = sys_target_triple(sys_target)
sys_isa = sys_target_isa(sys_target)
}
}
@@ -1727,6 +1791,33 @@ target "system" {
# Utils
#
function "sys_target_isa" {
params = [sys_target]
result = (
sys_target_ver(sys_target) != "v1"?
join("-",
[
replace(split("-", sys_target)[0], "_", "-"),
sys_target_ver(sys_target)
]):
replace(split("-", sys_target)[0], "_", "-")
)
}
function "sys_target_triple" {
params = [sys_target]
result = join("-", [
split("-", sys_target)[0],
split("-", sys_target)[2],
split("-", sys_target)[3],
])
}
function "sys_target_ver" {
params = [sys_target]
result = split("-", sys_target)[1]
}
function "elem_tag" {
params = [prefix, matrix, tag]
result = join(":", [elem(prefix, matrix), tag])
+2 -11
View File
@@ -14,8 +14,8 @@ default_feat_sets='["all"]'
default_rust_toolchains='["nightly"]'
default_rust_targets='["x86_64-unknown-linux-gnu"]'
default_sys_names='["debian"]'
default_sys_targets='["x86_64-linux-gnu"]'
default_sys_versions='["testing-slim"]'
default_sys_targets='["x86_64-v1-linux-gnu"]'
if test ! -z "$cargo_profile"; then
env_cargo_profiles="[\"${cargo_profile}\"]"
@@ -63,16 +63,6 @@ rocksdb_opt_level=3
rocksdb_portable=1
git_checkout="HEAD"
use_chef="true"
complement_count=1
complement_skip="TestPartialStateJoin.*"
complement_skip="${complement_skip}|TestRoomDeleteAlias/Pa.*/Can_delete_canonical_alias"
complement_skip="${complement_skip}|TestUnbanViaInvite.*"
complement_skip="${complement_skip}|TestRoomDeleteAlias/Pa.*/Regular_users_can_add_and_delete_aliases_when.*"
complement_skip="${complement_skip}|TestToDeviceMessagesOverFederation/stopped_server"
complement_skip="${complement_skip}|TestLogin/parallel/POST_/login_as_non-existing_user_is_rejected"
complement_skip="${complement_skip}|TestRoomState/Parallel/GET_/publicRooms_lists_newly-created_room"
complement_skip="${complement_skip}|TestThreadReceiptsInSyncMSC4102"
complement_run=".*"
set +a
###############################################################################
@@ -83,6 +73,7 @@ if test "$CI" = "true"; then
fi
args=""
args="$args --provenance=false"
args="$args --builder ${builder_name}"
#args="$args --set *.platform=${sys_platform}"
+48 -11
View File
@@ -12,9 +12,26 @@ default_feat_set="all"
default_rust_toolchain="nightly"
default_rust_target="x86_64-unknown-linux-gnu"
default_sys_name="debian"
default_sys_target="x86_64-linux-gnu"
default_sys_target="x86_64-v1-linux-gnu"
default_sys_version="testing-slim"
default_complement_verbose=0
default_complement_count=1
default_complement_parallel=1
default_complement_shuffle=0
default_complement_timeout="1h"
default_complement_run=".*"
run="${1:-$default_complement_run}"
skip=""
skip="${skip}TestToDeviceMessagesOverFederation/stopped_server"
skip="${skip}|TestToDeviceMessagesOverFederation/interrupted_connectivity"
skip="${skip}|TestRoomCreate/Parallel/POST_/createRoom_makes_a_room_with_a_topic_and_writes_rich_topic_representation"
skip="${skip}|TestRoomCreate/Parallel/POST_/createRoom_makes_a_room_with_a_topic_via_initial_state_overwritten_by_topic"
skip="${skip}|TestLogin/parallel/POST_/"
skip="${skip}|TestUnbanViaInvite"
skip="${skip}|TestRoomState/Parallel/GET_/publicRooms_lists_newly-created_room"
set -a
cargo_profile="${cargo_profile:-$default_cargo_profile}"
feat_set="${feat_set:-$default_feat_set}"
@@ -30,12 +47,21 @@ set +a
###############################################################################
envs=""
envs="$envs -e complement_verbose=${complement_verbose:-$default_complement_verbose}"
envs="$envs -e complement_count=${complement_count:-$default_complement_count}"
envs="$envs -e complement_parallel=${complement_parallel:-$default_complement_parallel}"
envs="$envs -e complement_shuffle=${complement_shuffle:-$default_complement_shuffle}"
envs="$envs -e complement_timeout=${complement_timeout:-$default_complement_timeout}"
envs="$envs -e complement_skip=${complement_skip:-$skip}"
envs="$envs -e complement_run=${1:-$default_complement_run}"
set -x
tester_image="complement-tester--${sys_name}--${sys_version}--${sys_target}"
testee_image="complement-testee--${cargo_profile}--${rust_toolchain}--${rust_target}--${feat_set}--${sys_name}--${sys_version}--${sys_target}"
name="complement_tester__${sys_name}__${sys_version}__${sys_target}"
sock="/var/run/docker.sock"
arg="--name $name -v $sock:$sock --network=host $tester_image ${testee_image}"
arg="--name $name -v $sock:$sock --network=host $envs $tester_image ${testee_image}"
set +x
if test "$CI_VERBOSE_ENV" = "true"; then
@@ -45,21 +71,32 @@ fi
docker rm -f "$name" 2>/dev/null
trap 'set +x; date; echo -e "\033[1;41;37mERROR\033[0m"' ERR
arg="-d $arg"
cid=$(docker run $arg)
if test "$CI" = "true"; then
arg="-d $arg"
cid=$(docker run $arg)
else
arg="--rm -a stdout -a stderr $arg"
docker run $arg
exit $?
echo -n "$cid" > "$name"
fi
echo -n "$cid" > "$name"
output_src="$cid:/usr/src/complement/full_output.jsonl"
output_dst="complement.jsonl"
extract_output() {
docker cp "$output_src" "$output_dst"
}
trap 'docker container stop $cid; set +x; date; echo -e "\033[1;41;37mERROR\033[0m"' INT
result_src="$cid:/usr/src/complement/new_results.jsonl"
result_dst="tests/test_results/complement/test_results.jsonl"
extract_results() {
docker cp "$result_src" "$result_dst"
}
trap 'extract_output; set +x; date; echo -e "\033[1;41;37mERROR\033[0m"' ERR
trap 'docker container stop $cid; extract_output' INT
docker logs -f "$cid"
docker wait "$cid" 2>/dev/null
extract_results
git diff -U0 --color --shortstat "$result_dst" | (grep "$run" || true)
git diff --quiet --exit-code "$result_dst"
echo -e "\033[1;42;30mACCEPT\033[0m"
+26 -6
View File
@@ -7,14 +7,20 @@ ## Basics
Tuwunel uses a config file for the majority of the settings, but also supports
setting individual config options via commandline.
Please refer to the [example config
file](./configuration/examples.md#example-configuration) for all of those
settings.
Please refer to the [example config file](./configuration/examples.md#example-configuration) for all of those settings.
The config file to use can be specified on the commandline when running
Tuwunel by specifying the `-c`, `--config` flag. Alternatively, you can use
the environment variable `TUWUNEL_CONFIG` to specify the config file to used.
Conduit's environment variables are supported for backwards compatibility.
Tuwunel by specifying the `-c`, `--config` flag. It is also possible to specify
more than one config file.
Alternatively, you can use the environment variable `TUWUNEL_CONFIG` to specify
the config file to used. Conduit's environment variables are supported for
backwards compatibility.
> [!IMPORTANT]
> It is bad practice to uncomment default options without changing them. Many
> defaults are updated by developers as features evolve and can be essential
> to expected server function.
## Option commandline flag
@@ -31,6 +37,20 @@ ## Option commandline flag
- `--option log=\"debug\"` works ✅
- `--option server_name='"example.com'"` works ✅
## Relevance of configuration settings
There is a specific sequence for reading and overwriting the settings. The
latest setting takes precedence and defines the configuration.
1. Set in `CONDUIT_CONFIG`.
2. Set in `CONDUWUIT_CONFIG`.
3. Set in `TUWUNEL_CONFIG`.
4. Set in the first config file on the command line (e.g. `-c config_file_1.toml`).
5. Set in the second config file on the command line (e.g. `-c config_file_2.toml`).
6. Set in any additional config file on the command line (e.g. `-c config_file_n.toml`).
7. Set within the options (again, the latest option in the list overrides).
## Execute commandline flag
Tuwunel supports running admin commands on startup using the commandline
+1 -1
View File
@@ -26,7 +26,7 @@
file = ./rust-toolchain.toml;
# See also `rust-toolchain.toml`
sha256 = "sha256-X/4ZBHO3iW0fOenQ3foEvscgAPJYl2abspaBThDOukI=";
sha256 = "sha256-Qxt8XAuaUR2OMdKbN4u8dBJOhSHxS+uS06Wl9+flVEk=";
};
mkScope = pkgs: pkgs.lib.makeScope pkgs.newScope (self: {
+1 -1
View File
@@ -9,7 +9,7 @@
# If you're having trouble making the relevant changes, bug a maintainer.
[toolchain]
channel = "1.86.0"
channel = "1.88.0"
profile = "minimal"
components = [
# For rust-analyzer
+11
View File
@@ -22,6 +22,11 @@ brotli_compression = [
"tuwunel-core/brotli_compression",
"tuwunel-service/brotli_compression",
]
bzip2_compression = [
"tuwunel-api/bzip2_compression",
"tuwunel-database/bzip2_compression",
"tuwunel-service/bzip2_compression",
]
gzip_compression = [
"tuwunel-api/gzip_compression",
"tuwunel-core/gzip_compression",
@@ -56,6 +61,11 @@ jemalloc_stats = [
"tuwunel-database/jemalloc_stats",
"tuwunel-service/jemalloc_stats",
]
lz4_compression = [
"tuwunel-api/lz4_compression",
"tuwunel-database/lz4_compression",
"tuwunel-service/lz4_compression",
]
release_max_log_level = [
"tuwunel-api/release_max_log_level",
"tuwunel-core/release_max_log_level",
@@ -74,6 +84,7 @@ zstd_compression = [
]
[dependencies]
base64.workspace = true
clap.workspace = true
tuwunel-api.workspace = true
tuwunel-core.workspace = true
+15 -2
View File
@@ -638,7 +638,7 @@ pub(super) async fn force_set_room_state_from_server(
self.services
.rooms
.outlier
.timeline
.add_pdu_outlier(&event_id, &value);
if let Some(state_key) = &pdu.state_key {
@@ -668,7 +668,7 @@ pub(super) async fn force_set_room_state_from_server(
self.services
.rooms
.outlier
.timeline
.add_pdu_outlier(&event_id, &value);
}
@@ -1035,3 +1035,16 @@ struct Claim {
.map(async |token| self.write_str(&token).await)?
.await
}
#[admin_command]
pub(super) async fn resync_database(&self) -> Result {
if !self.services.db.is_secondary() {
return Err!("Not a secondary instance.");
}
self.services
.db
.db
.update()
.map_err(|e| err!("Failed to update from primary: {e:?}"))
}
+3
View File
@@ -256,6 +256,9 @@ pub(super) enum DebugCommand {
audience: Option<String>,
},
/// - Synchronize database with primary (secondary only)
ResyncDatabase,
/// - Developer test stubs
#[command(subcommand)]
#[allow(non_snake_case)]
+1 -21
View File
@@ -1,5 +1,3 @@
use std::fmt::Write;
use futures::StreamExt;
use ruma::{OwnedRoomId, OwnedServerName, OwnedUserId};
use tuwunel_core::{Err, Result};
@@ -26,25 +24,7 @@ pub(super) async fn enable_room(&self, room_id: OwnedRoomId) -> Result {
#[admin_command]
pub(super) async fn incoming_federation(&self) -> Result {
let msg = {
let map = self
.services
.rooms
.event_handler
.federation_handletime
.read()
.expect("locked");
let mut msg = format!("Handling {} incoming pdus:\n", map.len());
for (r, (e, i)) in map.iter() {
let elapsed = i.elapsed();
writeln!(msg, "{} {}: {}m{}s", r, e, elapsed.as_secs() / 60, elapsed.as_secs() % 60)?;
}
msg
};
self.write_str(&msg).await
Err!("This command is temporarily disabled")
}
#[admin_command]
+1 -1
View File
@@ -32,7 +32,7 @@ pub(super) async fn process(subcommand: GlobalsCommand, context: &Context<'_>) -
},
| GlobalsCommand::CurrentCount => {
let timer = tokio::time::Instant::now();
let results = services.globals.db.current_count();
let results = services.globals.current_count();
let query_time = timer.elapsed();
write!(context, "Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```")
+5 -2
View File
@@ -19,6 +19,9 @@ pub(crate) enum PresenceCommand {
PresenceSince {
/// UNIX timestamp since (u64)
since: u64,
/// Upper-bound of since
to: Option<u64>,
},
}
@@ -34,11 +37,11 @@ pub(super) async fn process(subcommand: PresenceCommand, context: &Context<'_>)
write!(context, "Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```")
},
| PresenceCommand::PresenceSince { since } => {
| PresenceCommand::PresenceSince { since, to } => {
let timer = tokio::time::Instant::now();
let results: Vec<(_, _, _)> = services
.presence
.presence_since(since)
.presence_since(since, to)
.map(|(user_id, count, bytes)| (user_id.to_owned(), count, bytes.to_vec()))
.collect()
.await;
+13 -2
View File
@@ -1,5 +1,6 @@
use std::{borrow::Cow, collections::BTreeMap, ops::Deref, sync::Arc};
use base64::prelude::*;
use clap::Subcommand;
use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
use tokio::time::Instant;
@@ -30,6 +31,10 @@ pub(crate) enum RawCommand {
/// Key
key: String,
/// Encode as base64
#[arg(long, short)]
base64: bool,
},
/// - Raw database delete (for string keys)
@@ -421,13 +426,19 @@ pub(super) async fn raw_del(&self, map: String, key: String) -> Result {
}
#[admin_command]
pub(super) async fn raw_get(&self, map: String, key: String) -> Result {
pub(super) async fn raw_get(&self, map: String, key: String, base64: bool) -> Result {
let map = self.services.db.get(&map)?;
let timer = Instant::now();
let handle = map.get(&key).await?;
let query_time = timer.elapsed();
let result = String::from_utf8_lossy(&handle);
let result = if base64 {
BASE64_STANDARD.encode(&handle)
} else {
String::from_utf8_lossy(&handle).to_string()
};
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:?}\n```"))
.await
}
+2 -2
View File
@@ -19,7 +19,7 @@ pub(super) async fn list_rooms(
.rooms
.metadata
.iter_ids()
.filter_map(|room_id| async move {
.filter_map(async |room_id| {
(!exclude_disabled
|| !self
.services
@@ -29,7 +29,7 @@ pub(super) async fn list_rooms(
.await)
.then_some(room_id)
})
.filter_map(|room_id| async move {
.filter_map(async |room_id| {
(!exclude_banned
|| !self
.services
+6 -2
View File
@@ -1,7 +1,7 @@
use clap::Subcommand;
use futures::StreamExt;
use ruma::OwnedRoomId;
use tuwunel_core::{Err, Result};
use tuwunel_core::Result;
use crate::{Context, PAGE_SIZE, get_room_info};
@@ -57,7 +57,11 @@ pub(super) async fn process(command: RoomDirectoryCommand, context: &Context<'_>
.collect();
if rooms.is_empty() {
return Err!("No more rooms.");
context
.write_str("No rooms are published.")
.await?;
return Ok(());
}
let body = rooms
+1 -1
View File
@@ -47,7 +47,7 @@ async fn list_joined_members(&self, room_id: OwnedRoomId, local_only: bool) -> R
.unwrap_or(true)
})
.map(ToOwned::to_owned)
.filter_map(|user_id| async move {
.filter_map(async |user_id| {
Some((
self.services
.users
+2 -2
View File
@@ -179,7 +179,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
.alias
.local_aliases_for_room(&room_id)
.map(ToOwned::to_owned)
.for_each(|local_alias| async move {
.for_each(async |local_alias| {
self.services
.rooms
.alias
@@ -363,7 +363,7 @@ async fn ban_list_of_rooms(&self) -> Result {
.alias
.local_aliases_for_room(&room_id)
.map(ToOwned::to_owned)
.for_each(|local_alias| async move {
.for_each(async |local_alias| {
self.services
.rooms
.alias
+8 -2
View File
@@ -236,7 +236,10 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
.collect()
.await;
full_user_deactivate(self.services, &user_id, &all_joined_rooms).await?;
full_user_deactivate(self.services, &user_id, &all_joined_rooms)
.boxed()
.await?;
update_displayname(self.services, &user_id, None, &all_joined_rooms).await;
update_avatar_url(self.services, &user_id, None, None, &all_joined_rooms).await;
leave_all_rooms(self.services, &user_id).await;
@@ -358,7 +361,10 @@ pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) ->
.collect()
.await;
full_user_deactivate(self.services, &user_id, &all_joined_rooms).await?;
full_user_deactivate(self.services, &user_id, &all_joined_rooms)
.boxed()
.await?;
update_displayname(self.services, &user_id, None, &all_joined_rooms).await;
update_avatar_url(self.services, &user_id, None, None, &all_joined_rooms)
.await;
+9 -3
View File
@@ -22,6 +22,9 @@ brotli_compression = [
"tuwunel-service/brotli_compression",
"reqwest/brotli",
]
bzip2_compression = [
"tuwunel-service/bzip2_compression",
]
element_hacks = [
"tuwunel-service/element_hacks",
]
@@ -52,6 +55,9 @@ jemalloc_stats = [
ldap = [
"tuwunel-service/ldap",
]
lz4_compression = [
"tuwunel-service/lz4_compression",
]
release_max_log_level = [
"tuwunel-core/release_max_log_level",
"tuwunel-service/release_max_log_level",
@@ -73,10 +79,8 @@ axum-extra.workspace = true
axum.workspace = true
base64.workspace = true
bytes.workspace = true
ctor.workspace = true
tuwunel-core.workspace = true
tuwunel-service.workspace = true
const-str.workspace = true
ctor.workspace = true
futures.workspace = true
hmac.workspace = true
http.workspace = true
@@ -94,6 +98,8 @@ serde.workspace = true
sha1.workspace = true
tokio.workspace = true
tracing.workspace = true
tuwunel-core.workspace = true
tuwunel-service.workspace = true
[lints]
workspace = true
+5 -589
View File
@@ -1,29 +1,23 @@
use std::fmt::Write;
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use futures::{FutureExt, StreamExt};
use register::RegistrationKind;
use ruma::{
OwnedRoomId, UserId,
api::client::{
account::{
ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity,
deactivate, get_3pids, get_username_availability,
register::{self, LoginType},
ThirdPartyIdRemovalStatus, change_password, deactivate, get_3pids,
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
whoami,
},
uiaa::{AuthFlow, AuthType, UiaaInfo},
},
events::{
GlobalAccountDataEventType, StateEventType,
StateEventType,
room::power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
push,
};
use tuwunel_core::{
Err, Error, Result, debug_info, err, error, info, is_equal_to,
Err, Error, Result, err, info,
matrix::{Event, pdu::PduBuilder},
utils,
utils::{ReadyExt, stream::BroadbandExt},
@@ -31,570 +25,9 @@
};
use tuwunel_service::Services;
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
use super::SESSION_ID_LENGTH;
use crate::Ruma;
const RANDOM_USER_ID_LENGTH: usize = 10;
/// # `GET /_matrix/client/v3/register/available`
///
/// Checks if a username is valid and available on this server.
///
/// Conditions for returning true:
/// - The user id is not historical
/// - The server name of the user id matches this server
/// - No user or appservice on this server already claimed this username
///
/// Note: This will not reserve the username, so the username might become
/// invalid when trying to register
#[tracing::instrument(skip_all, fields(%client), name = "register_available")]
pub(crate) async fn get_register_available_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_username_availability::v3::Request>,
) -> Result<get_username_availability::v3::Response> {
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
let is_matrix_appservice_irc = body
.appservice_info
.as_ref()
.is_some_and(|appservice| {
appservice.registration.id == "irc"
|| appservice
.registration
.id
.contains("matrix-appservice-irc")
|| appservice
.registration
.id
.contains("matrix_appservice_irc")
});
if services
.globals
.forbidden_usernames()
.is_match(&body.username)
{
return Err!(Request(Forbidden("Username is forbidden")));
}
// don't force the username lowercase if it's from matrix-appservice-irc
let body_username = if is_matrix_appservice_irc {
body.username.clone()
} else {
body.username.to_lowercase()
};
// Validate user id
let user_id =
match UserId::parse_with_server_name(&body_username, services.globals.server_name()) {
| Ok(user_id) => {
if let Err(e) = user_id.validate_strict() {
// unless the username is from the broken matrix appservice IRC bridge, we
// should follow synapse's behaviour on not allowing things like spaces
// and UTF-8 characters in usernames
if !is_matrix_appservice_irc {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} contains disallowed characters or spaces: \
{e}"
))));
}
}
user_id
},
| Err(e) => {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} is not valid: {e}"
))));
},
};
// Check if username is creative enough
if services.users.exists(&user_id).await {
return Err!(Request(UserInUse("User ID is not available.")));
}
if let Some(ref info) = body.appservice_info {
if !info.is_user_match(&user_id) {
return Err!(Request(Exclusive("Username is not in an appservice namespace.")));
}
}
if services
.appservice
.is_exclusive_user_id(&user_id)
.await
{
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
}
Ok(get_username_availability::v3::Response { available: true })
}
/// # `POST /_matrix/client/v3/register`
///
/// Register an account on this homeserver.
///
/// You can use [`GET
/// /_matrix/client/v3/register/available`](fn.get_register_available_route.
/// html) to check if the user id is valid and available.
///
/// - Only works if registration is enabled
/// - If type is guest: ignores all parameters except
/// initial_device_display_name
/// - If sender is not appservice: Requires UIAA (but we only use a dummy stage)
/// - If type is not guest and no username is given: Always fails after UIAA
/// check
/// - Creates a new account and populates it with default account data
/// - If `inhibit_login` is false: Creates a device and returns device id and
/// access_token
#[allow(clippy::doc_markdown)]
#[tracing::instrument(skip_all, fields(%client), name = "register")]
pub(crate) async fn register_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<register::v3::Request>,
) -> Result<register::v3::Response> {
let is_guest = body.kind == RegistrationKind::Guest;
let emergency_mode_enabled = services.config.emergency_password.is_some();
if !services.config.allow_registration && body.appservice_info.is_none() {
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
| (Some(username), Some(device_display_name)) => {
info!(
%is_guest,
user = %username,
device_name = %device_display_name,
"Rejecting registration attempt as registration is disabled"
);
},
| (Some(username), _) => {
info!(
%is_guest,
user = %username,
"Rejecting registration attempt as registration is disabled"
);
},
| (_, Some(device_display_name)) => {
info!(
%is_guest,
device_name = %device_display_name,
"Rejecting registration attempt as registration is disabled"
);
},
| (None, _) => {
info!(
%is_guest,
"Rejecting registration attempt as registration is disabled"
);
},
}
return Err!(Request(Forbidden("Registration has been disabled.")));
}
if is_guest
&& (!services.config.allow_guest_registration
|| (services.config.allow_registration
&& services.globals.registration_token.is_some()))
{
info!(
"Guest registration disabled / registration enabled with token configured, \
rejecting guest registration attempt, initial device name: \"{}\"",
body.initial_device_display_name
.as_deref()
.unwrap_or("")
);
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
}
// forbid guests from registering if there is not a real admin user yet. give
// generic user error.
if is_guest && services.users.count().await < 2 {
warn!(
"Guest account attempted to register before a real admin user has been registered, \
rejecting registration. Guest's initial device name: \"{}\"",
body.initial_device_display_name
.as_deref()
.unwrap_or("")
);
return Err!(Request(Forbidden("Registration is temporarily disabled.")));
}
let user_id = match (body.username.as_ref(), is_guest) {
| (Some(username), false) => {
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
let is_matrix_appservice_irc =
body.appservice_info
.as_ref()
.is_some_and(|appservice| {
appservice.registration.id == "irc"
|| appservice
.registration
.id
.contains("matrix-appservice-irc")
|| appservice
.registration
.id
.contains("matrix_appservice_irc")
});
if services
.globals
.forbidden_usernames()
.is_match(username)
&& !emergency_mode_enabled
{
return Err!(Request(Forbidden("Username is forbidden")));
}
// don't force the username lowercase if it's from matrix-appservice-irc
let body_username = if is_matrix_appservice_irc {
username.clone()
} else {
username.to_lowercase()
};
let proposed_user_id = match UserId::parse_with_server_name(
&body_username,
services.globals.server_name(),
) {
| Ok(user_id) => {
if let Err(e) = user_id.validate_strict() {
// unless the username is from the broken matrix appservice IRC bridge, or
// we are in emergency mode, we should follow synapse's behaviour on
// not allowing things like spaces and UTF-8 characters in usernames
if !is_matrix_appservice_irc && !emergency_mode_enabled {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} contains disallowed characters or \
spaces: {e}"
))));
}
}
user_id
},
| Err(e) => {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} is not valid: {e}"
))));
},
};
if services.users.exists(&proposed_user_id).await {
return Err!(Request(UserInUse("User ID is not available.")));
}
proposed_user_id
},
| _ => loop {
let proposed_user_id = UserId::parse_with_server_name(
utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
services.globals.server_name(),
)
.unwrap();
if !services.users.exists(&proposed_user_id).await {
break proposed_user_id;
}
},
};
if body.body.login_type == Some(LoginType::ApplicationService) {
match body.appservice_info {
| Some(ref info) =>
if !info.is_user_match(&user_id) && !emergency_mode_enabled {
return Err!(Request(Exclusive(
"Username is not in an appservice namespace."
)));
},
| _ => {
return Err!(Request(MissingToken("Missing appservice token.")));
},
}
} else if services
.appservice
.is_exclusive_user_id(&user_id)
.await && !emergency_mode_enabled
{
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
}
// UIAA
let mut uiaainfo;
let skip_auth = if services.globals.registration_token.is_some() {
// Registration token required
uiaainfo = UiaaInfo {
flows: vec![AuthFlow {
stages: vec![AuthType::RegistrationToken],
}],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
};
body.appservice_info.is_some()
} else {
// No registration token necessary, but clients must still go through the flow
uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
};
body.appservice_info.is_some() || is_guest
};
if !skip_auth {
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(
&UserId::parse_with_server_name("", services.globals.server_name())
.unwrap(),
"".into(),
auth,
&uiaainfo,
)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
},
| _ => match body.json_body {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services.uiaa.create(
&UserId::parse_with_server_name("", services.globals.server_name())
.unwrap(),
"".into(),
&uiaainfo,
json,
);
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err!(Request(NotJson("JSON body is not valid")));
},
},
}
}
let password = if is_guest { None } else { body.password.as_deref() };
// Create user
services
.users
.create(&user_id, password, None)
.await?;
// Default to pretty displayname
let mut displayname = user_id.localpart().to_owned();
// If `new_user_displayname_suffix` is set, registration will push whatever
// content is set to the user's display name with a space before it
if !services
.globals
.new_user_displayname_suffix()
.is_empty()
&& body.appservice_info.is_none()
{
write!(displayname, " {}", services.server.config.new_user_displayname_suffix)?;
}
services
.users
.set_displayname(&user_id, Some(displayname.clone()));
// Initial account data
services
.account_data
.update(
None,
&user_id,
GlobalAccountDataEventType::PushRules
.to_string()
.into(),
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
content: ruma::events::push_rules::PushRulesEventContent {
global: push::Ruleset::server_default(&user_id),
},
})?,
)
.await?;
if (!is_guest && body.inhibit_login)
|| body
.appservice_info
.as_ref()
.is_some_and(|appservice| appservice.registration.device_management)
{
return Ok(register::v3::Response {
access_token: None,
user_id,
device_id: None,
refresh_token: None,
expires_in: None,
});
}
// Generate new device id if the user didn't specify one
let device_id = if is_guest { None } else { body.device_id.clone() }
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
// Generate new token for the device
let token = utils::random_string(TOKEN_LENGTH);
// Create device for this account
services
.users
.create_device(
&user_id,
&device_id,
&token,
body.initial_device_display_name.clone(),
Some(client.to_string()),
)
.await?;
debug_info!(%user_id, %device_id, "User account was created");
let device_display_name = body
.initial_device_display_name
.as_deref()
.unwrap_or("");
// log in conduit admin channel if a non-guest user registered
if body.appservice_info.is_none() && !is_guest {
if !device_display_name.is_empty() {
let notice = format!(
"New user \"{user_id}\" registered on this server from IP {client} and device \
display name \"{device_display_name}\""
);
info!("{notice}");
if services.server.config.admin_room_notices {
services.admin.notice(&notice).await;
}
} else {
let notice = format!("New user \"{user_id}\" registered on this server.");
info!("{notice}");
if services.server.config.admin_room_notices {
services.admin.notice(&notice).await;
}
}
}
// log in conduit admin channel if a guest registered
if body.appservice_info.is_none() && is_guest && services.config.log_guest_registrations {
debug_info!("New guest user \"{user_id}\" registered on this server.");
if !device_display_name.is_empty() {
if services.server.config.admin_room_notices {
services
.admin
.notice(&format!(
"Guest user \"{user_id}\" with device display name \
\"{device_display_name}\" registered on this server from IP {client}"
))
.await;
}
} else {
#[allow(clippy::collapsible_else_if)]
if services.server.config.admin_room_notices {
services
.admin
.notice(&format!(
"Guest user \"{user_id}\" with no device display name registered on \
this server from IP {client}",
))
.await;
}
}
}
// If this is the first real user, grant them admin privileges except for guest
// users
// Note: the server user is generated first
if !is_guest {
if let Ok(admin_room) = services.admin.get_admin_room().await {
if services
.rooms
.state_cache
.room_joined_count(&admin_room)
.await
.is_ok_and(is_equal_to!(1))
{
services.admin.make_user_admin(&user_id).await?;
warn!("Granting {user_id} admin privileges as the first user");
}
}
}
if body.appservice_info.is_none()
&& !services.server.config.auto_join_rooms.is_empty()
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
{
for room in &services.server.config.auto_join_rooms {
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
error!(
"Failed to resolve room alias to room ID when attempting to auto join \
{room}, skipping"
);
continue;
};
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), &room_id)
.await
{
warn!(
"Skipping room {room} to automatically join as we have never joined before."
);
continue;
}
if let Some(room_server_name) = room.server_name() {
match join_room_by_id_helper(
&services,
&user_id,
&room_id,
Some("Automatically joining this room upon registration".to_owned()),
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
None,
&body.appservice_info,
)
.boxed()
.await
{
| Err(e) => {
// don't return this error so we don't fail registrations
error!(
"Failed to automatically join room {room} for user {user_id}: {e}"
);
},
| _ => {
info!("Automatically joined room {room} for user {user_id}");
},
}
}
}
}
Ok(register::v3::Response {
access_token: Some(token),
user_id,
device_id: Some(device_id),
refresh_token: None,
expires_in: None,
})
}
/// # `POST /_matrix/client/r0/account/password`
///
/// Changes the password of this account.
@@ -861,23 +294,6 @@ pub(crate) async fn request_3pid_management_token_via_msisdn_route(
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
}
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
///
/// Checks if the provided registration token is valid at the time of checking
///
/// Currently does not have any ratelimiting, and this isn't very practical as
/// there is only one registration token allowed.
pub(crate) async fn check_registration_token_validity(
State(services): State<crate::State>,
body: Ruma<check_registration_token_validity::v1::Request>,
) -> Result<check_registration_token_validity::v1::Response> {
let Some(reg_token) = services.globals.registration_token.clone() else {
return Err!(Request(Forbidden("Server does not allow token registration")));
};
Ok(check_registration_token_validity::v1::Response { valid: reg_token == body.token })
}
/// Runs through all the deactivation steps:
///
/// - Mark as deactivated
@@ -889,7 +305,7 @@ pub async fn full_user_deactivate(
services: &Services,
user_id: &UserId,
all_joined_rooms: &[OwnedRoomId],
) -> Result<()> {
) -> Result {
services
.users
.deactivate_account(user_id)
+1 -1
View File
@@ -132,7 +132,7 @@ pub(crate) async fn get_context_route(
let state_at = events_after
.last()
.map(ref_at!(1))
.map_or(body.event_id.as_ref(), |pdu| pdu.event_id.as_ref());
.map_or_else(|| body.event_id.as_ref(), |pdu| pdu.event_id.as_ref());
let state_ids = services
.rooms
+2 -1
View File
@@ -100,7 +100,8 @@ pub(crate) async fn update_device_route(
.create_device(
sender_user,
&device_id,
&appservice.registration.as_token,
(&appservice.registration.as_token, None),
None,
None,
Some(client.to_string()),
)
+60 -32
View File
@@ -33,7 +33,7 @@
TryFutureExtExt,
math::Expected,
result::FlatOk,
stream::{ReadyExt, WidebandExt},
stream::{IterStream, ReadyExt, WidebandExt},
},
};
use tuwunel_service::Services;
@@ -51,19 +51,7 @@ pub(crate) async fn get_public_rooms_filtered_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_public_rooms_filtered::v3::Request>,
) -> Result<get_public_rooms_filtered::v3::Response> {
if let Some(server) = &body.server {
if services
.config
.forbidden_remote_room_directory_server_names
.is_match(server.host())
|| services
.config
.forbidden_remote_server_names
.is_match(server.host())
{
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
}
}
check_banned(&services, body.server.as_deref())?;
let response = get_public_rooms_filtered_helper(
&services,
@@ -92,19 +80,7 @@ pub(crate) async fn get_public_rooms_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_public_rooms::v3::Request>,
) -> Result<get_public_rooms::v3::Response> {
if let Some(server) = &body.server {
if services
.config
.forbidden_remote_room_directory_server_names
.is_match(server.host())
|| services
.config
.forbidden_remote_server_names
.is_match(server.host())
{
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
}
}
check_banned(&services, body.server.as_deref())?;
let response = get_public_rooms_filtered_helper(
&services,
@@ -308,32 +284,62 @@ pub(crate) async fn get_public_rooms_filtered_helper(
}
}
let search_term = filter
.generic_search_term
.as_deref()
.map(str::to_lowercase);
let search_room_id = filter
.generic_search_term
.as_deref()
.filter(|_| services.config.allow_public_room_search_by_id)
.filter(|s| s.starts_with('!'))
.filter(|s| s.len() > 5); // require some characters to limit scope.
let meta_public_rooms = search_room_id
.filter(|_| services.config.allow_unlisted_room_search_by_id)
.map(|prefix| services.rooms.metadata.public_ids_prefix(prefix))
.into_iter()
.stream()
.flatten();
let mut all_rooms: Vec<PublicRoomsChunk> = services
.rooms
.directory
.public_rooms()
.map(ToOwned::to_owned)
.chain(meta_public_rooms)
.wide_then(|room_id| public_rooms_chunk(services, room_id))
.ready_filter_map(|chunk| {
if !filter.room_types.is_empty() && !filter.room_types.contains(&RoomTypeFilter::from(chunk.room_type.clone())) {
if !filter.room_types.is_empty()
&& !filter
.room_types
.contains(&RoomTypeFilter::from(chunk.room_type.clone()))
{
return None;
}
if let Some(query) = filter.generic_search_term.as_ref().map(|q| q.to_lowercase()) {
if let Some(query) = search_room_id {
if chunk.room_id.as_str().contains(query) {
return Some(chunk);
}
}
if let Some(query) = search_term.as_deref() {
if let Some(name) = &chunk.name {
if name.as_str().to_lowercase().contains(&query) {
if name.as_str().to_lowercase().contains(query) {
return Some(chunk);
}
}
if let Some(topic) = &chunk.topic {
if topic.to_lowercase().contains(&query) {
if topic.to_lowercase().contains(query) {
return Some(chunk);
}
}
if let Some(canonical_alias) = &chunk.canonical_alias {
if canonical_alias.as_str().to_lowercase().contains(&query) {
if canonical_alias.as_str().to_lowercase().contains(query) {
return Some(chunk);
}
}
@@ -490,3 +496,25 @@ async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> Public
world_readable,
}
}
fn check_banned(services: &Services, server: Option<&ServerName>) -> Result {
let Some(server) = server else {
return Ok(());
};
let forbidden_remote_directory = services
.config
.forbidden_remote_room_directory_server_names
.is_match(server.host());
let forbidden_remote_server = services
.config
.forbidden_remote_server_names
.is_match(server.host());
if forbidden_remote_directory || forbidden_remote_server {
Err!(Request(Forbidden("Server is banned on this homeserver.")))
} else {
Ok(())
}
}
+3 -3
View File
@@ -526,7 +526,7 @@ pub(crate) async fn get_keys_helper<F>(
let mut futures: FuturesUnordered<_> = get_over_federation
.into_iter()
.map(|(server, vec)| async move {
.map(async |(server, vec)| {
let mut device_keys_input_fed = BTreeMap::new();
for (user_id, keys) in vec {
device_keys_input_fed.insert(user_id.to_owned(), keys.clone());
@@ -597,7 +597,7 @@ fn add_unsigned_device_display_name(
keys: &mut Raw<ruma::encryption::DeviceKeys>,
metadata: ruma::api::client::device::Device,
include_display_names: bool,
) -> serde_json::Result<()> {
) -> Result {
if let Some(display_name) = metadata.display_name {
let mut object = keys.deserialize_as::<serde_json::Map<String, serde_json::Value>>()?;
@@ -656,7 +656,7 @@ pub(crate) async fn claim_keys_helper(
let mut futures: FuturesUnordered<_> = get_over_federation
.into_iter()
.map(|(server, vec)| async move {
.map(async |(server, vec)| {
let mut one_time_keys_input_fed = BTreeMap::new();
for (user_id, keys) in vec {
one_time_keys_input_fed.insert(user_id.clone(), keys.clone());
+10 -9
View File
@@ -2,7 +2,7 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use futures::{FutureExt, StreamExt};
use futures::{FutureExt, StreamExt, pin_mut};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId,
RoomVersionId, UserId,
@@ -550,7 +550,7 @@ async fn join_room_by_id_helper_remote(
.validate_and_add_event_id_no_fetch(pdu, &room_version_id)
})
.ready_filter_map(Result::ok)
.fold(HashMap::new(), |mut state, (event_id, value)| async move {
.fold(HashMap::new(), async |mut state, (event_id, value)| {
let pdu = match PduEvent::from_id_val(&event_id, value.clone()) {
| Ok(pdu) => pdu,
| Err(e) => {
@@ -561,8 +561,9 @@ async fn join_room_by_id_helper_remote(
services
.rooms
.outlier
.timeline
.add_pdu_outlier(&event_id, &value);
if let Some(state_key) = &pdu.state_key {
let shortstatekey = services
.rooms
@@ -595,7 +596,7 @@ async fn join_room_by_id_helper_remote(
.ready_for_each(|(event_id, value)| {
services
.rooms
.outlier
.timeline
.add_pdu_outlier(&event_id, &value);
})
.await;
@@ -747,7 +748,7 @@ async fn join_room_by_id_helper_local(
})
.await
{
services
let users = services
.rooms
.state_cache
.local_users_in_room(room_id)
@@ -759,10 +760,10 @@ async fn join_room_by_id_helper_local(
&state_lock,
)
})
.boxed()
.next()
.await
.map(ToOwned::to_owned)
.map(ToOwned::to_owned);
pin_mut!(users);
users.next().await
} else {
None
}
+2 -1
View File
@@ -541,8 +541,9 @@ async fn knock_room_helper_remote(
services
.rooms
.outlier
.timeline
.add_pdu_outlier(&event_id, &event);
state_map.insert(shortstatekey, event_id.clone());
}
+1 -5
View File
@@ -230,11 +230,7 @@ pub async fn leave_room(
Ok(())
}
async fn remote_leave_room(
services: &Services,
user_id: &UserId,
room_id: &RoomId,
) -> Result<()> {
async fn remote_leave_room(services: &Services, user_id: &UserId, room_id: &RoomId) -> Result {
let mut make_leave_response_and_server =
Err!(BadServerResponse("No remote server available to assist in leaving {room_id}."));
+1 -1
View File
@@ -85,7 +85,7 @@ pub(crate) async fn joined_members_route(
.state_cache
.room_members(&body.room_id)
.map(ToOwned::to_owned)
.broad_then(|user_id| async move {
.broad_then(async |user_id| {
let (display_name, avatar_url) = join(
services.users.displayname(&user_id).ok(),
services.users.avatar_url(&user_id).ok(),
+7 -9
View File
@@ -100,7 +100,6 @@ pub(crate) async fn get_message_events_route(
.rooms
.timeline
.backfill_if_required(room_id, from)
.boxed()
.await
.log_err()
.ok();
@@ -151,9 +150,7 @@ pub(crate) async fn get_message_events_route(
.map(IterStream::stream)
.into_stream()
.flatten()
.broad_filter_map(|user_id| async move {
get_member_event(&services, room_id, &user_id).await
})
.broad_filter_map(async |user_id| get_member_event(&services, room_id, &user_id).await)
.collect()
.await;
@@ -195,10 +192,11 @@ pub(crate) async fn lazy_loading_witness<'a, I>(
.max()
.unwrap_or_else(PduCount::max);
let receipts = services
.rooms
.read_receipt
.readreceipts_since(lazy_loading_context.room_id, oldest.into_unsigned());
let receipts = services.rooms.read_receipt.readreceipts_since(
lazy_loading_context.room_id,
oldest.into_unsigned(),
Some(newest.into_unsigned()),
);
pin_mut!(receipts);
let witness: Witness = events
@@ -256,7 +254,7 @@ pub(crate) async fn is_ignored_pdu<Pdu>(
user_id: &UserId,
) -> bool
where
Pdu: Event + Send + Sync,
Pdu: Event,
{
// exclude Synapse's dummy events from bloating up response bodies. clients
// don't need to see this.
+3 -1
View File
@@ -19,6 +19,7 @@
pub(super) mod push;
pub(super) mod read_marker;
pub(super) mod redact;
pub(super) mod register;
pub(super) mod relations;
pub(super) mod report;
pub(super) mod room;
@@ -63,6 +64,7 @@
pub(super) use push::*;
pub(super) use read_marker::*;
pub(super) use redact::*;
pub(super) use register::*;
pub(super) use relations::*;
pub(super) use report::*;
pub(super) use room::*;
@@ -87,7 +89,7 @@
const DEVICE_ID_LENGTH: usize = 10;
/// generated user access token length
const TOKEN_LENGTH: usize = 32;
const TOKEN_LENGTH: usize = tuwunel_service::users::device::TOKEN_LENGTH;
/// generated user session ID length
const SESSION_ID_LENGTH: usize = tuwunel_service::uiaa::SESSION_ID_LENGTH;
+2 -2
View File
@@ -350,7 +350,7 @@ pub async fn update_displayname(
let all_joined_rooms: Vec<_> = all_joined_rooms
.iter()
.try_stream()
.and_then(|room_id: &OwnedRoomId| async move {
.and_then(async |room_id: &OwnedRoomId| {
let pdu = PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
displayname: displayname.clone(),
membership: MembershipState::Join,
@@ -403,7 +403,7 @@ pub async fn update_avatar_url(
let all_joined_rooms: Vec<_> = all_joined_rooms
.iter()
.try_stream()
.and_then(|room_id: &OwnedRoomId| async move {
.and_then(async |room_id: &OwnedRoomId| {
let pdu = PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
avatar_url: avatar_url.clone(),
blurhash: blurhash.clone(),
+606
View File
@@ -0,0 +1,606 @@
use std::fmt::Write;
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use futures::FutureExt;
use register::RegistrationKind;
use ruma::{
UserId,
api::client::{
account::{
check_registration_token_validity, get_username_availability,
register::{self, LoginType},
},
uiaa::{AuthFlow, AuthType, UiaaInfo},
},
events::GlobalAccountDataEventType,
push,
};
use tuwunel_core::{Err, Error, Result, debug_info, error, info, is_equal_to, utils, warn};
use tuwunel_service::users::device::generate_refresh_token;
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, join_room_by_id_helper};
use crate::Ruma;
const RANDOM_USER_ID_LENGTH: usize = 10;
/// # `GET /_matrix/client/v3/register/available`
///
/// Checks if a username is valid and available on this server.
///
/// Conditions for returning true:
/// - The user id is not historical
/// - The server name of the user id matches this server
/// - No user or appservice on this server already claimed this username
///
/// Note: This will not reserve the username, so the username might become
/// invalid when trying to register
#[tracing::instrument(skip_all, fields(%client), name = "register_available")]
pub(crate) async fn get_register_available_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_username_availability::v3::Request>,
) -> Result<get_username_availability::v3::Response> {
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
let is_matrix_appservice_irc = body
.appservice_info
.as_ref()
.is_some_and(|appservice| {
appservice.registration.id == "irc"
|| appservice
.registration
.id
.contains("matrix-appservice-irc")
|| appservice
.registration
.id
.contains("matrix_appservice_irc")
});
if services
.globals
.forbidden_usernames()
.is_match(&body.username)
{
return Err!(Request(Forbidden("Username is forbidden")));
}
// don't force the username lowercase if it's from matrix-appservice-irc
let body_username = if is_matrix_appservice_irc {
body.username.clone()
} else {
body.username.to_lowercase()
};
// Validate user id
let user_id =
match UserId::parse_with_server_name(&body_username, services.globals.server_name()) {
| Ok(user_id) => {
if let Err(e) = user_id.validate_strict() {
// unless the username is from the broken matrix appservice IRC bridge, we
// should follow synapse's behaviour on not allowing things like spaces
// and UTF-8 characters in usernames
if !is_matrix_appservice_irc {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} contains disallowed characters or spaces: \
{e}"
))));
}
}
user_id
},
| Err(e) => {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} is not valid: {e}"
))));
},
};
// Check if username is creative enough
if services.users.exists(&user_id).await {
return Err!(Request(UserInUse("User ID is not available.")));
}
if let Some(ref info) = body.appservice_info {
if !info.is_user_match(&user_id) {
return Err!(Request(Exclusive("Username is not in an appservice namespace.")));
}
}
if services
.appservice
.is_exclusive_user_id(&user_id)
.await
{
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
}
Ok(get_username_availability::v3::Response { available: true })
}
/// # `POST /_matrix/client/v3/register`
///
/// Register an account on this homeserver.
///
/// You can use [`GET
/// /_matrix/client/v3/register/available`](fn.get_register_available_route.
/// html) to check if the user id is valid and available.
///
/// - Only works if registration is enabled
/// - If type is guest: ignores all parameters except
/// initial_device_display_name
/// - If sender is not appservice: Requires UIAA (but we only use a dummy stage)
/// - If type is not guest and no username is given: Always fails after UIAA
/// check
/// - Creates a new account and populates it with default account data
/// - If `inhibit_login` is false: Creates a device and returns device id and
/// access_token
#[allow(clippy::doc_markdown)]
#[tracing::instrument(skip_all, fields(%client), name = "register")]
pub(crate) async fn register_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<register::v3::Request>,
) -> Result<register::v3::Response> {
let is_guest = body.kind == RegistrationKind::Guest;
let emergency_mode_enabled = services.config.emergency_password.is_some();
if !services.config.allow_registration && body.appservice_info.is_none() {
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
| (Some(username), Some(device_display_name)) => {
info!(
%is_guest,
user = %username,
device_name = %device_display_name,
"Rejecting registration attempt as registration is disabled"
);
},
| (Some(username), _) => {
info!(
%is_guest,
user = %username,
"Rejecting registration attempt as registration is disabled"
);
},
| (_, Some(device_display_name)) => {
info!(
%is_guest,
device_name = %device_display_name,
"Rejecting registration attempt as registration is disabled"
);
},
| (None, _) => {
info!(
%is_guest,
"Rejecting registration attempt as registration is disabled"
);
},
}
return Err!(Request(Forbidden("Registration has been disabled.")));
}
if is_guest
&& (!services.config.allow_guest_registration
|| (services.config.allow_registration
&& services.globals.registration_token.is_some()))
{
info!(
"Guest registration disabled / registration enabled with token configured, \
rejecting guest registration attempt, initial device name: \"{}\"",
body.initial_device_display_name
.as_deref()
.unwrap_or("")
);
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
}
// forbid guests from registering if there is not a real admin user yet. give
// generic user error.
if is_guest && services.users.count().await < 2 {
warn!(
"Guest account attempted to register before a real admin user has been registered, \
rejecting registration. Guest's initial device name: \"{}\"",
body.initial_device_display_name
.as_deref()
.unwrap_or("")
);
return Err!(Request(Forbidden("Registration is temporarily disabled.")));
}
let user_id = match (body.username.as_ref(), is_guest) {
| (Some(username), false) => {
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
let is_matrix_appservice_irc =
body.appservice_info
.as_ref()
.is_some_and(|appservice| {
appservice.registration.id == "irc"
|| appservice
.registration
.id
.contains("matrix-appservice-irc")
|| appservice
.registration
.id
.contains("matrix_appservice_irc")
});
if services
.globals
.forbidden_usernames()
.is_match(username)
&& !emergency_mode_enabled
{
return Err!(Request(Forbidden("Username is forbidden")));
}
// don't force the username lowercase if it's from matrix-appservice-irc
let body_username = if is_matrix_appservice_irc {
username.clone()
} else {
username.to_lowercase()
};
let proposed_user_id = match UserId::parse_with_server_name(
&body_username,
services.globals.server_name(),
) {
| Ok(user_id) => {
if let Err(e) = user_id.validate_strict() {
// unless the username is from the broken matrix appservice IRC bridge, or
// we are in emergency mode, we should follow synapse's behaviour on
// not allowing things like spaces and UTF-8 characters in usernames
if !is_matrix_appservice_irc && !emergency_mode_enabled {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} contains disallowed characters or \
spaces: {e}"
))));
}
}
user_id
},
| Err(e) => {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} is not valid: {e}"
))));
},
};
if services.users.exists(&proposed_user_id).await {
return Err!(Request(UserInUse("User ID is not available.")));
}
proposed_user_id
},
| _ => loop {
let proposed_user_id = UserId::parse_with_server_name(
utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
services.globals.server_name(),
)
.unwrap();
if !services.users.exists(&proposed_user_id).await {
break proposed_user_id;
}
},
};
if body.body.login_type == Some(LoginType::ApplicationService) {
match body.appservice_info {
| Some(ref info) =>
if !info.is_user_match(&user_id) && !emergency_mode_enabled {
return Err!(Request(Exclusive(
"Username is not in an appservice namespace."
)));
},
| _ => {
return Err!(Request(MissingToken("Missing appservice token.")));
},
}
} else if services
.appservice
.is_exclusive_user_id(&user_id)
.await && !emergency_mode_enabled
{
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
}
// UIAA
let mut uiaainfo;
let skip_auth = if services.globals.registration_token.is_some() {
// Registration token required
uiaainfo = UiaaInfo {
flows: vec![AuthFlow {
stages: vec![AuthType::RegistrationToken],
}],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
};
body.appservice_info.is_some()
} else {
// No registration token necessary, but clients must still go through the flow
uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
};
body.appservice_info.is_some() || is_guest
};
if !skip_auth {
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(
&UserId::parse_with_server_name("", services.globals.server_name())
.unwrap(),
"".into(),
auth,
&uiaainfo,
)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
},
| _ => match body.json_body {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services.uiaa.create(
&UserId::parse_with_server_name("", services.globals.server_name())
.unwrap(),
"".into(),
&uiaainfo,
json,
);
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err!(Request(NotJson("JSON body is not valid")));
},
},
}
}
let password = if is_guest { None } else { body.password.as_deref() };
// Create user
services
.users
.create(&user_id, password, None)
.await?;
// Default to pretty displayname
let mut displayname = user_id.localpart().to_owned();
// If `new_user_displayname_suffix` is set, registration will push whatever
// content is set to the user's display name with a space before it
if !services
.globals
.new_user_displayname_suffix()
.is_empty()
&& body.appservice_info.is_none()
{
write!(displayname, " {}", services.server.config.new_user_displayname_suffix)?;
}
services
.users
.set_displayname(&user_id, Some(displayname.clone()));
// Initial account data
services
.account_data
.update(
None,
&user_id,
GlobalAccountDataEventType::PushRules
.to_string()
.into(),
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
content: ruma::events::push_rules::PushRulesEventContent {
global: push::Ruleset::server_default(&user_id),
},
})?,
)
.await?;
if (!is_guest && body.inhibit_login)
|| body
.appservice_info
.as_ref()
.is_some_and(|appservice| appservice.registration.device_management)
{
return Ok(register::v3::Response {
access_token: None,
user_id,
device_id: None,
refresh_token: None,
expires_in: None,
});
}
// Generate new device id if the user didn't specify one
let device_id = if is_guest { None } else { body.device_id.clone() }
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
// Generate new token for the device
let (access_token, expires_in) = services
.users
.generate_access_token(body.body.refresh_token);
// Generate a new refresh_token if requested by client
let refresh_token = expires_in.is_some().then(generate_refresh_token);
// Create device for this account
services
.users
.create_device(
&user_id,
&device_id,
(&access_token, expires_in),
refresh_token.as_deref(),
body.initial_device_display_name.clone(),
Some(client.to_string()),
)
.await?;
debug_info!(%user_id, %device_id, "User account was created");
let device_display_name = body
.initial_device_display_name
.as_deref()
.unwrap_or("");
// log in conduit admin channel if a non-guest user registered
if body.appservice_info.is_none() && !is_guest {
if !device_display_name.is_empty() {
let notice = format!(
"New user \"{user_id}\" registered on this server from IP {client} and device \
display name \"{device_display_name}\""
);
info!("{notice}");
if services.server.config.admin_room_notices {
services.admin.notice(&notice).await;
}
} else {
let notice = format!("New user \"{user_id}\" registered on this server.");
info!("{notice}");
if services.server.config.admin_room_notices {
services.admin.notice(&notice).await;
}
}
}
// log in conduit admin channel if a guest registered
if body.appservice_info.is_none() && is_guest && services.config.log_guest_registrations {
debug_info!("New guest user \"{user_id}\" registered on this server.");
if !device_display_name.is_empty() {
if services.server.config.admin_room_notices {
services
.admin
.notice(&format!(
"Guest user \"{user_id}\" with device display name \
\"{device_display_name}\" registered on this server from IP {client}"
))
.await;
}
} else {
#[allow(clippy::collapsible_else_if)]
if services.server.config.admin_room_notices {
services
.admin
.notice(&format!(
"Guest user \"{user_id}\" with no device display name registered on \
this server from IP {client}",
))
.await;
}
}
}
// If this is the first real user, grant them admin privileges except for guest
// users
// Note: the server user is generated first
if !is_guest
&& services.config.grant_admin_to_first_user
&& let Ok(admin_room) = services.admin.get_admin_room().await
&& services
.rooms
.state_cache
.room_joined_count(&admin_room)
.await
.is_ok_and(is_equal_to!(1))
{
services.admin.make_user_admin(&user_id).await?;
warn!("Granting {user_id} admin privileges as the first user");
}
if body.appservice_info.is_none()
&& !services.server.config.auto_join_rooms.is_empty()
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
{
for room in &services.server.config.auto_join_rooms {
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
error!(
"Failed to resolve room alias to room ID when attempting to auto join \
{room}, skipping"
);
continue;
};
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), &room_id)
.await
{
warn!(
"Skipping room {room} to automatically join as we have never joined before."
);
continue;
}
if let Some(room_server_name) = room.server_name() {
match join_room_by_id_helper(
&services,
&user_id,
&room_id,
Some("Automatically joining this room upon registration".to_owned()),
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
None,
&body.appservice_info,
)
.boxed()
.await
{
| Err(e) => {
// don't return this error so we don't fail registrations
error!(
"Failed to automatically join room {room} for user {user_id}: {e}"
);
},
| _ => {
info!("Automatically joined room {room} for user {user_id}");
},
}
}
}
}
Ok(register::v3::Response {
user_id,
device_id: Some(device_id),
access_token: Some(access_token),
refresh_token,
expires_in,
})
}
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
///
/// Checks if the provided registration token is valid at the time of checking
///
/// Currently does not have any ratelimiting, and this isn't very practical as
/// there is only one registration token allowed.
pub(crate) async fn check_registration_token_validity(
State(services): State<crate::State>,
body: Ruma<check_registration_token_validity::v1::Request>,
) -> Result<check_registration_token_validity::v1::Response> {
let Some(reg_token) = services.globals.registration_token.clone() else {
return Err!(Request(Forbidden("Server does not allow token registration")));
};
Ok(check_registration_token_validity::v1::Response { valid: reg_token == body.token })
}
+1 -1
View File
@@ -175,7 +175,7 @@ async fn paginate_relations_with_filter(
})
}
async fn visibility_filter<Pdu: Event + Send + Sync>(
async fn visibility_filter<Pdu: Event>(
services: &Services,
sender_user: &UserId,
item: (PduCount, Pdu),
+1 -1
View File
@@ -149,7 +149,7 @@ async fn is_event_report_valid(
reason: Option<&String>,
score: Option<ruma::Int>,
pdu: &PduEvent,
) -> Result<()> {
) -> Result {
debug_info!(
"Checking if report from user {sender_user} for event {event_id} in room {room_id} is \
valid"
+23 -27
View File
@@ -111,6 +111,7 @@ pub(crate) async fn create_room_route(
.short
.get_or_create_shortroomid(&room_id)
.await;
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
let alias: Option<OwnedRoomAliasId> = match body.room_alias_name.as_ref() {
@@ -187,6 +188,10 @@ pub(crate) async fn create_room_route(
},
};
// Increment and hold the counter; the room will sync atomically to clients
// which is preferable.
let next_count = services.globals.next_count();
// 1. The room create event
services
.rooms
@@ -425,8 +430,10 @@ pub(crate) async fn create_room_route(
.await?;
}
// 8. Events implied by invite (and TODO: invite_3pid)
drop(next_count);
drop(state_lock);
// 8. Events implied by invite (and TODO: invite_3pid)
for user_id in &body.invite {
if services
.users
@@ -487,43 +494,32 @@ fn default_power_levels_content(
visibility: &room::Visibility,
users: BTreeMap<OwnedUserId, Int>,
) -> Result<serde_json::Value> {
use serde_json::to_value;
let mut power_levels_content =
serde_json::to_value(RoomPowerLevelsEventContent { users, ..Default::default() })
.expect("event is valid, we just created it");
to_value(RoomPowerLevelsEventContent { users, ..Default::default() })?;
// secure proper defaults of sensitive/dangerous permissions that moderators
// (power level 50) should not have easy access to
power_levels_content["events"]["m.room.power_levels"] =
serde_json::to_value(100).expect("100 is valid Value");
power_levels_content["events"]["m.room.server_acl"] =
serde_json::to_value(100).expect("100 is valid Value");
power_levels_content["events"]["m.room.tombstone"] =
serde_json::to_value(100).expect("100 is valid Value");
power_levels_content["events"]["m.room.encryption"] =
serde_json::to_value(100).expect("100 is valid Value");
power_levels_content["events"]["m.room.history_visibility"] =
serde_json::to_value(100).expect("100 is valid Value");
power_levels_content["events"]["m.room.power_levels"] = to_value(100)?;
power_levels_content["events"]["m.room.server_acl"] = to_value(100)?;
power_levels_content["events"]["m.room.tombstone"] = to_value(100)?;
power_levels_content["events"]["m.room.encryption"] = to_value(100)?;
power_levels_content["events"]["m.room.history_visibility"] = to_value(100)?;
// always allow users to respond (not post new) to polls. this is primarily
// useful in read-only announcement rooms that post a public poll.
power_levels_content["events"]["org.matrix.msc3381.poll.response"] =
serde_json::to_value(0).expect("0 is valid Value");
power_levels_content["events"]["m.poll.response"] =
serde_json::to_value(0).expect("0 is valid Value");
power_levels_content["events"]["org.matrix.msc3381.poll.response"] = to_value(0)?;
power_levels_content["events"]["m.poll.response"] = to_value(0)?;
// synapse does this too. clients do not expose these permissions. it prevents
// default users from calling public rooms, for obvious reasons.
if *visibility == room::Visibility::Public {
power_levels_content["events"]["m.call.invite"] =
serde_json::to_value(50).expect("50 is valid Value");
power_levels_content["events"]["m.call"] =
serde_json::to_value(50).expect("50 is valid Value");
power_levels_content["events"]["m.call.member"] =
serde_json::to_value(50).expect("50 is valid Value");
power_levels_content["events"]["org.matrix.msc3401.call"] =
serde_json::to_value(50).expect("50 is valid Value");
power_levels_content["events"]["org.matrix.msc3401.call.member"] =
serde_json::to_value(50).expect("50 is valid Value");
power_levels_content["events"]["m.call.invite"] = to_value(50)?;
power_levels_content["events"]["m.call"] = to_value(50)?;
power_levels_content["events"]["m.call.member"] = to_value(50)?;
power_levels_content["events"]["org.matrix.msc3401.call"] = to_value(50)?;
power_levels_content["events"]["org.matrix.msc3401.call.member"] = to_value(50)?;
}
if let Some(power_level_content_override) = power_level_content_override {
+11 -20
View File
@@ -17,7 +17,7 @@
};
use serde_json::{json, value::to_raw_value};
use tuwunel_core::{
Error, Result, err, info,
Err, Error, Result, err,
matrix::{Event, StateKey, pdu::PduBuilder},
};
@@ -54,10 +54,8 @@ pub(crate) async fn upgrade_room_route(
TRANSFERABLE_STATE_EVENTS.is_sorted(),
"TRANSFERABLE_STATE_EVENTS is not sorted"
);
let sender_user = body
.sender_user
.as_ref()
.expect("user is authenticated");
let sender_user = body.sender_user();
if !services
.server
@@ -134,8 +132,7 @@ pub(crate) async fn upgrade_room_route(
create_event_content.insert(
"creator".into(),
json!(&sender_user).try_into().map_err(|e| {
info!("Error forming creation event: {e}");
Error::BadRequest(ErrorKind::BadJson, "Error forming creation event")
err!(Request(BadJson(error!("Error forming creation event: {e}"))))
})?,
);
},
@@ -150,24 +147,20 @@ pub(crate) async fn upgrade_room_route(
"room_version".into(),
json!(&body.new_version)
.try_into()
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?,
.map_err(|_| err!(Request(BadJson("Error forming creation event"))))?,
);
create_event_content.insert(
"predecessor".into(),
json!(predecessor)
.try_into()
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?,
.map_err(|_| err!(Request(BadJson("Error forming creation event"))))?,
);
// Validate creation event content
if serde_json::from_str::<CanonicalJsonObject>(
to_raw_value(&create_event_content)
.expect("Error forming creation event")
.get(),
)
.is_err()
if serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&create_event_content)?.get())
.is_err()
{
return Err(Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"));
return Err!(Request(BadJson("Error forming creation event")));
}
services
@@ -176,8 +169,7 @@ pub(crate) async fn upgrade_room_route(
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_event_content)
.expect("event is valid, we just created it"),
content: to_raw_value(&create_event_content)?,
unsigned: None,
state_key: Some(StateKey::new()),
redacts: None,
@@ -205,8 +197,7 @@ pub(crate) async fn upgrade_room_route(
blurhash: services.users.blurhash(sender_user).await.ok(),
reason: None,
join_authorized_via_users_server: None,
})
.expect("event is valid, we just created it"),
})?,
unsigned: None,
state_key: Some(sender_user.as_str().into()),
redacts: None,
+3 -3
View File
@@ -95,13 +95,13 @@ async fn category_room_events(
});
let results: Vec<_> = rooms
.filter_map(|room_id| async move {
.filter_map(async |room_id| {
check_room_visible(services, sender_user, &room_id, criteria)
.await
.is_ok()
.then_some(room_id)
})
.filter_map(|room_id| async move {
.filter_map(async |room_id| {
let query = RoomQuery {
room_id: &room_id,
user_id: Some(sender_user),
@@ -135,7 +135,7 @@ async fn category_room_events(
.iter()
.stream()
.ready_filter(|_| criteria.include_state.is_some_and(is_true!()))
.filter_map(|(room_id, ..)| async move {
.filter_map(async |(room_id, ..)| {
procure_room_state(services, room_id)
.map_ok(|state| (room_id.clone(), state))
.await
+20 -5
View File
@@ -3,6 +3,7 @@
mod ldap;
mod logout;
mod password;
mod refresh;
mod token;
use axum::extract::State;
@@ -21,10 +22,12 @@
},
};
use tuwunel_core::{Err, Result, info, utils, utils::stream::ReadyExt};
use tuwunel_service::users::device::generate_refresh_token;
use self::{ldap::ldap_login, password::password_login};
pub(crate) use self::{
logout::{logout_all_route, logout_route},
refresh::refresh_token_route,
token::login_token_route,
};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
@@ -87,7 +90,12 @@ pub(crate) async fn login_route(
};
// Generate a new token for the device
let access_token = utils::random_string(TOKEN_LENGTH);
let (access_token, expires_in) = services
.users
.generate_access_token(body.body.refresh_token);
// Generate a new refresh_token if requested by client
let refresh_token = expires_in.is_some().then(generate_refresh_token);
// Generate new device id if the user didn't specify one
let device_id = body
@@ -108,7 +116,8 @@ pub(crate) async fn login_route(
.create_device(
&user_id,
&device_id,
&access_token,
(&access_token, expires_in),
refresh_token.as_deref(),
body.initial_device_display_name.clone(),
Some(client.to_string()),
)
@@ -116,7 +125,13 @@ pub(crate) async fn login_route(
} else {
services
.users
.set_access_token(&user_id, &device_id, &access_token)
.set_access_token(
&user_id,
&device_id,
&access_token,
expires_in,
refresh_token.as_deref(),
)
.await?;
}
@@ -141,7 +156,7 @@ pub(crate) async fn login_route(
device_id,
home_server,
well_known,
expires_in: None,
refresh_token: None,
expires_in,
refresh_token,
})
}
+1
View File
@@ -86,6 +86,7 @@ pub(super) async fn password_login(
.password_hash(lowercased_user_id)
.map_ok(|hash| (hash, lowercased_user_id))
})
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))
.await?;
if hash.is_empty() {
+54
View File
@@ -0,0 +1,54 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use ruma::api::client::session::refresh_token::v3::{Request, Response};
use tuwunel_core::{Err, Result, debug_info, err};
use tuwunel_service::users::device::generate_refresh_token;
use crate::Ruma;
/// # `POST /_matrix/client/v3/refresh`
///
/// Refresh an access token.
///
/// <https://spec.matrix.org/v1.15/client-server-api/#post_matrixclientv3refresh>
#[tracing::instrument(skip_all, fields(%client), name = "refresh_token")]
pub(crate) async fn refresh_token_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<Request>,
) -> Result<Response> {
let refresh_token_claim = body.body.refresh_token;
if !refresh_token_claim.starts_with("refresh_") {
return Err!(Request(Forbidden("Refresh token is malformed.")));
}
let (user_id, device_id, ..) = services
.users
.find_from_token(&refresh_token_claim)
.await
.map_err(|e| err!(Request(Forbidden("Refresh token is unrecognized: {e}"))))?;
// New tokens
let refresh_token = Some(generate_refresh_token());
let (access_token, expires_in_ms) = services.users.generate_access_token(true);
services
.users
.set_access_token(
&user_id,
&device_id,
&access_token,
expires_in_ms,
refresh_token.as_deref(),
)
.await?;
debug_info!(?user_id, ?device_id, ?expires_in_ms, "refreshed their access_token",);
Ok(Response {
access_token,
refresh_token,
expires_in_ms,
})
}
+1 -1
View File
@@ -166,7 +166,7 @@ async fn get_client_hierarchy<'a, ShortRoomIds>(
let next_batch: OptionFuture<_> = queue
.pop_front()
.map(|(room, _)| async move {
.map(async |(room, _)| {
parents.insert(room);
let next_short_room_ids: Vec<_> = parents
+14 -23
View File
@@ -1,5 +1,5 @@
use axum::extract::State;
use futures::{FutureExt, TryStreamExt};
use futures::{FutureExt, TryFutureExt, TryStreamExt};
use ruma::{
OwnedEventId, RoomId, UserId,
api::client::state::{get_state_events, get_state_events_for_key, send_state_event},
@@ -17,7 +17,7 @@
};
use serde_json::json;
use tuwunel_core::{
Err, Result, err,
Err, Result, err, is_false,
matrix::{Event, pdu::PduBuilder},
utils::BoolExt,
};
@@ -337,7 +337,7 @@ async fn allowed_to_send_state_event(
.resolve_alias(&alias, None)
.await
.map_err(|e| {
err!(Request(Unknown("Failed resolving alias \"{alias}\": {e}")))
err!(Request(BadAlias("Failed resolving alias \"{alias}\": {e}")))
})?;
if alias_room_id != room_id {
@@ -356,7 +356,7 @@ async fn allowed_to_send_state_event(
},
| StateEventType::RoomMember => match json.deserialize_as::<RoomMemberEventContent>() {
| Ok(membership_content) => {
let Ok(state_key) = UserId::parse(state_key) else {
let Ok(_state_key) = UserId::parse(state_key) else {
return Err!(Request(BadJson(
"Membership event has invalid or non-existent state key"
)));
@@ -371,17 +371,6 @@ async fn allowed_to_send_state_event(
)));
}
if services
.rooms
.state_cache
.is_joined(state_key, room_id)
.await
{
return Err!(Request(InvalidParam(
"{state_key} is already joined, an authorising user is not required."
)));
}
if !services.globals.user_is_local(&authorising_user) {
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} does not belong to this \
@@ -389,17 +378,19 @@ async fn allowed_to_send_state_event(
)));
}
if !services
services
.rooms
.state_cache
.is_joined(&authorising_user, room_id)
.await
{
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} is not in the room, they \
cannot authorise the join."
)));
}
.map(is_false!())
.map(BoolExt::into_result)
.map_err(|()| {
err!(Request(InvalidParam(
"Authorising user {authorising_user} is not in the room. They \
cannot authorise the join."
)))
})
.await?;
}
},
| Err(e) => {
+1 -1
View File
@@ -75,7 +75,7 @@ async fn share_encrypted_room(
.get_shared_rooms(sender_user, user_id)
.ready_filter(|&room_id| Some(room_id) != ignore_room)
.map(ToOwned::to_owned)
.broad_any(|other_room_id| async move {
.broad_any(async |other_room_id| {
services
.rooms
.state_accessor
+392 -346
View File
File diff suppressed because it is too large Load Diff
+184 -121
View File
@@ -1,5 +1,5 @@
use std::{
cmp::{self, Ordering},
cmp::Ordering,
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
ops::Deref,
time::Duration,
@@ -8,7 +8,7 @@
use axum::extract::State;
use futures::{
FutureExt, Stream, StreamExt, TryFutureExt,
future::{OptionFuture, join3, try_join4},
future::{OptionFuture, join3, try_join5},
pin_mut,
};
use ruma::{
@@ -18,6 +18,7 @@
events::{
AnyRawAccountDataEvent, AnySyncEphemeralRoomEvent, StateEventType, TimelineEventType,
room::member::{MembershipState, RoomMemberEventContent},
typing::TypingEventContent,
},
serde::Raw,
uint,
@@ -27,7 +28,7 @@
matrix::{Event, TypeStateKey, pdu::PduCount},
trace,
utils::{
BoolExt, FutureBoolExt, IterStream, ReadyExt, TryFutureExtExt,
BoolExt, FutureBoolExt, IterStream, ReadyExt, TryFutureExtExt, TryReadyExt,
future::ReadyEqExt,
math::{ruma_from_usize, usize_from_ruma},
},
@@ -60,31 +61,16 @@ pub(crate) async fn sync_events_v5_route(
body: Ruma<sync_events::v5::Request>,
) -> Result<sync_events::v5::Response> {
debug_assert!(DEFAULT_BUMP_TYPES.is_sorted(), "DEFAULT_BUMP_TYPES is not sorted");
let sender_user = body
.sender_user
.as_ref()
.expect("user is authenticated");
let sender_device = body
.sender_device
.as_ref()
.expect("user is authenticated");
let mut body = body.body;
// Setup watchers, so if there's no response, we can wait for them
let watcher = services.sync.watch(sender_user, sender_device);
let next_batch = services.globals.next_count()?;
let conn_id = body.conn_id.clone();
let sender_user = body.sender_user();
let sender_device = body.sender_device();
let snake_key = into_snake_key(sender_user, sender_device, body.conn_id.clone());
let globalsince = body
.pos
.as_ref()
.and_then(|string| string.parse().ok())
.unwrap_or(0);
let snake_key = into_snake_key(sender_user, sender_device, conn_id);
if globalsince != 0 && !services.sync.snake_connection_cached(&snake_key) {
return Err!(Request(UnknownPos(
"Connection data unknown to server; restarting sync stream."
@@ -98,10 +84,16 @@ pub(crate) async fn sync_events_v5_route(
.forget_snake_sync_connection(&snake_key);
}
// Setup watchers, so if there's no response, we can wait for them
let watcher = services.sync.watch(sender_user, sender_device);
let next_batch = services.globals.wait_pending().await?;
// Get sticky parameters from cache
let mut cached = body.body.clone();
let known_rooms = services
.sync
.update_snake_sync_request_with_cache(&snake_key, &mut body);
.update_snake_sync_request_with_cache(&snake_key, &mut cached);
let all_joined_rooms = services
.rooms
@@ -135,33 +127,35 @@ pub(crate) async fn sync_events_v5_route(
.chain(all_invited_rooms.clone())
.chain(all_knocked_rooms.clone());
let pos = next_batch.clone().to_string();
let pos = next_batch.to_string();
let mut todo_rooms: TodoRooms = BTreeMap::new();
let sync_info: SyncInfo<'_> = (sender_user, sender_device, globalsince, &body);
let sync_info: SyncInfo<'_> = (sender_user, sender_device, globalsince, &cached);
let account_data = collect_account_data(services, sync_info).map(Ok);
let account_data = collect_account_data(services, sync_info, next_batch).map(Ok);
let e2ee = collect_e2ee(services, sync_info, all_joined_rooms.clone());
let e2ee = collect_e2ee(services, sync_info, next_batch, all_joined_rooms.clone());
let to_device = collect_to_device(services, sync_info, next_batch).map(Ok);
let receipts = collect_receipts(services).map(Ok);
let receipts = collect_receipts(services, sync_info, next_batch).map(Ok);
let (account_data, e2ee, to_device, receipts) =
try_join4(account_data, e2ee, to_device, receipts).await?;
let typing = collect_typing_events(services, sync_info, next_batch, all_joined_rooms.clone());
let (account_data, e2ee, to_device, receipts, typing) =
try_join5(account_data, e2ee, to_device, receipts, typing).await?;
let extensions = sync_events::v5::response::Extensions {
account_data,
e2ee,
to_device,
receipts,
typing: sync_events::v5::response::Typing::default(),
typing,
};
let mut response = sync_events::v5::Response {
txn_id: body.txn_id.clone(),
txn_id: cached.txn_id.clone(),
pos,
lists: BTreeMap::new(),
rooms: BTreeMap::new(),
@@ -173,7 +167,7 @@ pub(crate) async fn sync_events_v5_route(
sync_info,
all_invited_rooms.clone(),
all_joined_rooms.clone(),
all_rooms,
all_rooms.clone(),
&mut todo_rooms,
&known_rooms,
&mut response,
@@ -189,7 +183,7 @@ pub(crate) async fn sync_events_v5_route(
all_invited_rooms.clone(),
&todo_rooms,
&mut response,
&body,
&cached,
)
.await?;
@@ -206,11 +200,18 @@ pub(crate) async fn sync_events_v5_route(
.to_device
.clone()
.is_none_or(|to| to.events.is_empty())
&& response.extensions.typing.is_empty()
{
// Hang a few seconds so requests are not spammed
// Stop hanging if new info arrives
let default = Duration::from_secs(30);
let duration = cmp::min(body.timeout.unwrap_or(default), default);
let timeout_default = services.config.client_sync_timeout_default;
let timeout_min = services.config.client_sync_timeout_min;
let timeout_max = services.config.client_sync_timeout_max;
let duration = body
.timeout
.unwrap_or_else(|| Duration::from_millis(timeout_default))
.clamp(Duration::from_millis(timeout_min), Duration::from_millis(timeout_max));
_ = tokio::time::timeout(duration, watcher).await;
}
@@ -338,7 +339,6 @@ async fn handle_lists<'a, Rooms, AllRooms>(
.collect();
new_known_rooms.extend(new_rooms);
//new_known_rooms.extend(room_ids..cloned());
for room_id in room_ids {
let todo_room = todo_rooms.entry(room_id.to_owned()).or_insert((
BTreeSet::new(),
@@ -469,8 +469,8 @@ async fn process_rooms<'a, Rooms>(
let mut receipts: Vec<Raw<AnySyncEphemeralRoomEvent>> = services
.rooms
.read_receipt
.readreceipts_since(room_id, *roomsince)
.filter_map(|(read_user, _ts, v)| async move {
.readreceipts_since(room_id, *roomsince, Some(next_batch))
.filter_map(async |(read_user, _ts, v)| {
services
.users
.user_is_ignored(read_user, sender_user)
@@ -494,15 +494,15 @@ async fn process_rooms<'a, Rooms>(
.insert(room_id.clone(), pack_receipts(Box::new(receipts.into_iter())));
}
if roomsince != &0
if *roomsince != 0
&& timeline_pdus.is_empty()
&& receipt_size == 0
&& response
.extensions
.account_data
.rooms
.get(room_id)
.is_none_or(Vec::is_empty)
&& receipt_size == 0
{
continue;
}
@@ -519,7 +519,7 @@ async fn process_rooms<'a, Rooms>(
}))
})?
.or_else(|| {
if roomsince != &0 {
if *roomsince != 0 {
Some(roomsince.to_string())
} else {
None
@@ -548,7 +548,7 @@ async fn process_rooms<'a, Rooms>(
let required_state = required_state_request
.iter()
.stream()
.filter_map(|state| async move {
.filter_map(async |state| {
services
.rooms
.state_accessor
@@ -560,29 +560,40 @@ async fn process_rooms<'a, Rooms>(
.collect()
.await;
// Heroes
let heroes: Vec<_> = services
let room_name = services
.rooms
.state_cache
.room_members(room_id)
.ready_filter(|member| *member != sender_user)
.filter_map(|user_id| {
services
.rooms
.state_accessor
.get_member(room_id, user_id)
.map_ok(|memberevent| sync_events::v5::response::Hero {
user_id: user_id.into(),
name: memberevent.displayname,
avatar: memberevent.avatar_url,
})
.ok()
})
.take(5)
.collect()
.await;
.state_accessor
.get_name(room_id)
.await
.ok();
let name = match heroes.len().cmp(&(1_usize)) {
// Heroes
let heroes: Vec<_> = if room_name.is_none() {
services
.rooms
.state_cache
.room_members(room_id)
.ready_filter(|member| *member != sender_user)
.filter_map(|user_id| {
services
.rooms
.state_accessor
.get_member(room_id, user_id)
.map_ok(|memberevent| sync_events::v5::response::Hero {
user_id: user_id.into(),
name: memberevent.displayname,
avatar: memberevent.avatar_url,
})
.ok()
})
.take(5)
.collect()
.await
} else {
vec![]
};
let hero_name = match heroes.len().cmp(&(1_usize)) {
| Ordering::Greater => {
let firsts = heroes[1..]
.iter()
@@ -616,27 +627,24 @@ async fn process_rooms<'a, Rooms>(
None
};
let room_avatar = match services
.rooms
.state_accessor
.get_avatar(room_id)
.await
{
| ruma::JsOption::Some(avatar) => ruma::JsOption::from_option(avatar.url),
| ruma::JsOption::Null => ruma::JsOption::Null,
| ruma::JsOption::Undefined => ruma::JsOption::Undefined,
};
rooms.insert(room_id.clone(), sync_events::v5::response::Room {
name: services
.rooms
.state_accessor
.get_name(room_id)
.await
.ok()
.or(name),
avatar: match heroes_avatar {
| Some(heroes_avatar) => ruma::JsOption::Some(heroes_avatar),
| _ => match services
.rooms
.state_accessor
.get_avatar(room_id)
.await
{
| ruma::JsOption::Some(avatar) => ruma::JsOption::from_option(avatar.url),
| ruma::JsOption::Null => ruma::JsOption::Null,
| ruma::JsOption::Undefined => ruma::JsOption::Undefined,
},
avatar: if room_name.is_some() {
room_avatar
} else {
ruma::JsOption::from_option(heroes_avatar)
},
name: room_name.or(hero_name),
initial: Some(roomsince == &0),
is_dm: None,
invite_state,
@@ -691,9 +699,11 @@ async fn process_rooms<'a, Rooms>(
}
Ok(rooms)
}
async fn collect_account_data(
services: &Services,
(sender_user, _, globalsince, body): (&UserId, &DeviceId, u64, &sync_events::v5::Request),
next_batch: u64,
) -> sync_events::v5::response::AccountData {
let mut account_data = sync_events::v5::response::AccountData {
global: Vec::new(),
@@ -711,7 +721,7 @@ async fn collect_account_data(
account_data.global = services
.account_data
.changes_since(None, sender_user, globalsince, None)
.changes_since(None, sender_user, globalsince, Some(next_batch))
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Global))
.collect()
.await;
@@ -722,7 +732,7 @@ async fn collect_account_data(
room.clone(),
services
.account_data
.changes_since(Some(room), sender_user, globalsince, None)
.changes_since(Some(room), sender_user, globalsince, Some(next_batch))
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Room))
.collect()
.await,
@@ -741,6 +751,7 @@ async fn collect_e2ee<'a, Rooms>(
u64,
&sync_events::v5::Request,
),
next_batch: u64,
all_joined_rooms: Rooms,
) -> Result<sync_events::v5::response::E2EE>
where
@@ -756,7 +767,7 @@ async fn collect_e2ee<'a, Rooms>(
device_list_changes.extend(
services
.users
.keys_changed(sender_user, globalsince, None)
.keys_changed(sender_user, globalsince, Some(next_batch))
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
@@ -832,44 +843,51 @@ async fn collect_e2ee<'a, Rooms>(
.await;
for (key, id) in current_state_ids {
if since_state_ids.get(&key) != Some(&id) {
let Ok(pdu) = services.rooms.timeline.get_pdu(&id).await else {
error!("Pdu in state not found: {id}");
continue;
};
if pdu.kind == TimelineEventType::RoomMember {
if let Some(Ok(user_id)) = pdu.state_key.as_deref().map(UserId::parse)
{
if user_id == sender_user {
continue;
}
if since_state_ids.get(&key) == Some(&id) {
continue;
}
let content: RoomMemberEventContent = pdu.get_content()?;
match content.membership {
| MembershipState::Join => {
// A new user joined an encrypted room
if !share_encrypted_room(
services,
sender_user,
user_id,
Some(room_id),
)
.await
{
device_list_changes.insert(user_id.to_owned());
}
},
| MembershipState::Leave => {
// Write down users that have left encrypted rooms we
// are in
left_encrypted_users.insert(user_id.to_owned());
},
| _ => {},
}
let Ok(pdu) = services.rooms.timeline.get_pdu(&id).await else {
error!("Pdu in state not found: {id}");
continue;
};
if pdu.kind != TimelineEventType::RoomMember {
continue;
}
let Some(Ok(user_id)) = pdu.state_key.as_deref().map(UserId::parse) else {
continue;
};
if user_id == sender_user {
continue;
}
let content: RoomMemberEventContent = pdu.get_content()?;
match content.membership {
| MembershipState::Join => {
// A new user joined an encrypted room
if !share_encrypted_room(
services,
sender_user,
user_id,
Some(room_id),
)
.await
{
device_list_changes.insert(user_id.to_owned());
}
}
},
| MembershipState::Leave => {
// Write down users that have left encrypted rooms we
// are in
left_encrypted_users.insert(user_id.to_owned());
},
| _ => {},
}
}
if joined_since_last_sync || new_encrypted_room {
// If the user is in a new encrypted room, give them all joined users
device_list_changes.extend(
@@ -895,7 +913,7 @@ async fn collect_e2ee<'a, Rooms>(
device_list_changes.extend(
services
.users
.room_keys_changed(room_id, globalsince, None)
.room_keys_changed(room_id, globalsince, Some(next_batch))
.map(|(user_id, _)| user_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
@@ -953,7 +971,52 @@ async fn collect_to_device(
})
}
async fn collect_receipts(_services: &Services) -> sync_events::v5::response::Receipts {
async fn collect_typing_events<'a, Rooms>(
services: &Services,
(sender_user, _, _, body): SyncInfo<'_>,
_next_batch: u64,
rooms: Rooms,
) -> Result<sync_events::v5::response::Typing>
where
Rooms: Iterator<Item = &'a RoomId> + Send + 'a,
{
use sync_events::v5::response::Typing;
if !body.extensions.typing.enabled.unwrap_or(false) {
return Ok(Typing::default());
}
rooms
.stream()
.filter_map(async |room_id| {
services
.rooms
.typing
.typing_users_for_user(room_id, sender_user)
.inspect_err(|e| warn!(%room_id, "Failed to get typing events for room: {e}"))
.await
.ok()
.filter(|users| !users.is_empty())
.map(|users| Ok((room_id, users)))
})
.ready_try_fold_default(|mut response: Typing, (room_id, users)| {
response.rooms.insert(
room_id.to_owned(),
Raw::new(&ruma::events::typing::SyncTypingEvent {
content: TypingEventContent::new(users),
})?,
);
Ok(response)
})
.await
}
async fn collect_receipts(
_services: &Services,
(_sender_user, _, _globalsince, _body): SyncInfo<'_>,
_next_batch: u64,
) -> sync_events::v5::response::Receipts {
sync_events::v5::response::Receipts { rooms: BTreeMap::new() }
// TODO: get explicitly requested read receipts
}
+6 -7
View File
@@ -1,5 +1,5 @@
use axum::extract::State;
use futures::StreamExt;
use futures::{StreamExt, TryStreamExt};
use ruma::{api::client::threads::get_threads, uint};
use tuwunel_core::{
Result, at,
@@ -35,18 +35,17 @@ pub(crate) async fn get_threads_route(
.rooms
.threads
.threads_until(body.sender_user(), &body.room_id, from, &body.include)
.await?
.take(limit)
.filter_map(|(count, pdu)| async move {
services
.try_filter_map(async |(count, pdu)| {
Ok(services
.rooms
.state_accessor
.user_can_see_event(body.sender_user(), &body.room_id, &pdu.event_id)
.await
.then_some((count, pdu))
.then_some((count, pdu)))
})
.collect()
.await;
.try_collect()
.await?;
Ok(get_threads::v1::Response {
next_batch: threads
+1 -2
View File
@@ -41,7 +41,6 @@ pub(crate) async fn send_event_to_device_route(
map.insert(target_device_id_maybe.clone(), event.clone());
let mut messages = BTreeMap::new();
messages.insert(target_user_id.clone(), map);
let count = services.globals.next_count()?;
let mut buf = EduBuf::new();
serde_json::to_writer(
@@ -49,7 +48,7 @@ pub(crate) async fn send_event_to_device_route(
&federation::transactions::edu::Edu::DirectToDevice(DirectDeviceContent {
sender: sender_user.to_owned(),
ev_type: body.event_type.clone(),
message_id: count.to_string().into(),
message_id: services.globals.next_count().to_string().into(),
messages,
}),
)
+5 -1
View File
@@ -6,7 +6,10 @@
};
use tuwunel_core::{
Result,
utils::{future::BoolExt, stream::BroadbandExt},
utils::{
future::BoolExt,
stream::{BroadbandExt, ReadyExt},
},
};
use crate::Ruma;
@@ -34,6 +37,7 @@ pub(crate) async fn search_users_route(
let mut users = services
.users
.stream()
.ready_filter(|&user_id| user_id != sender_user)
.map(ToOwned::to_owned)
.broad_filter_map(async |user_id| {
let display_name = services.users.displayname(&user_id).await.ok();
+1 -1
View File
@@ -1,4 +1,4 @@
#![type_length_limit = "16384"] //TODO: reduce me
#![type_length_limit = "65536"] //TODO: reduce me
#![allow(clippy::toplevel_ref_arg)]
pub mod client;
+1
View File
@@ -35,6 +35,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::get_login_types_route)
.ruma_route(&client::login_route)
.ruma_route(&client::login_token_route)
.ruma_route(&client::refresh_token_route)
.ruma_route(&client::whoami_route)
.ruma_route(&client::logout_route)
.ruma_route(&client::logout_all_route)
+6 -7
View File
@@ -1,4 +1,4 @@
use std::{mem, ops::Deref};
use std::{fmt::Debug, mem, ops::Deref};
use axum::{body::Body, extract::FromRequest};
use bytes::{BufMut, Bytes, BytesMut};
@@ -13,6 +13,7 @@
use crate::State;
/// Extractor for Ruma request structs
#[derive(Debug)]
pub(crate) struct Args<T> {
/// Request struct body
pub(crate) body: T,
@@ -38,10 +39,7 @@ pub(crate) struct Args<T> {
pub(crate) json_body: Option<CanonicalJsonValue>,
}
impl<T> Args<T>
where
T: IncomingRequest + Send + Sync + 'static,
{
impl<T> Args<T> {
#[inline]
pub(crate) fn sender(&self) -> (&UserId, &DeviceId) {
(self.sender_user(), self.sender_device())
@@ -71,7 +69,7 @@ pub(crate) fn origin(&self) -> &ServerName {
impl<T> Deref for Args<T>
where
T: IncomingRequest + Send + Sync + 'static,
T: Sync,
{
type Target = T;
@@ -80,10 +78,11 @@ fn deref(&self) -> &Self::Target { &self.body }
impl<T> FromRequest<State, Body> for Args<T>
where
T: IncomingRequest + Send + Sync + 'static,
T: IncomingRequest + Debug + Send + Sync + 'static,
{
type Rejection = Error;
#[tracing::instrument(name = "ar", level = "debug", skip(services), ret, err)]
async fn from_request(
request: hyper::Request<Body>,
services: &State,
+153 -138
View File
@@ -1,9 +1,19 @@
use std::{fmt::Debug, time::SystemTime};
use axum::RequestPartsExt;
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
typed_header::TypedHeaderRejectionReason,
};
use futures::{
TryFutureExt,
future::{
Either::{Left, Right},
select_ok,
},
pin_mut,
};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
api::{
@@ -19,7 +29,9 @@
federation::{authentication::XMatrix, openid::get_openid_userinfo},
},
};
use tuwunel_core::{Err, Error, Result, debug_error, err, warn};
use tuwunel_core::{
Err, Error, Result, debug_error, err, is_less_than, utils::result::LogDebugErr, warn,
};
use tuwunel_service::{
Services,
appservice::RegistrationInfo,
@@ -30,24 +42,38 @@
enum Token {
Appservice(Box<RegistrationInfo>),
User((OwnedUserId, OwnedDeviceId)),
User((OwnedUserId, OwnedDeviceId, Option<SystemTime>)),
Expired((OwnedUserId, OwnedDeviceId)),
Invalid,
None,
}
#[derive(Debug, Default)]
pub(super) struct Auth {
pub(super) origin: Option<OwnedServerName>,
pub(super) sender_user: Option<OwnedUserId>,
pub(super) sender_device: Option<OwnedDeviceId>,
pub(super) appservice_info: Option<RegistrationInfo>,
pub(super) _expires_at: Option<SystemTime>,
}
#[tracing::instrument(
level = "trace",
skip(services, request, json_body),
ret,
err
)]
pub(super) async fn auth(
services: &Services,
request: &mut Request,
json_body: Option<&CanonicalJsonValue>,
metadata: &Metadata,
) -> Result<Auth> {
use AuthScheme::{AccessToken, AccessTokenOptional, AppserviceToken, ServerSignatures};
use Error::BadRequest;
use ErrorKind::UnknownToken;
use Token::{Appservice, Expired, Invalid, User};
let bearer: Option<TypedHeader<Authorization<Bearer>>> =
request.parts.extract().await.unwrap_or(None);
@@ -56,146 +82,138 @@ pub(super) async fn auth(
| None => request.query.access_token.as_deref(),
};
let token = if let Some(token) = token {
match services
.appservice
.find_from_access_token(token)
.await
{
| Some(reg_info) => Token::Appservice(Box::new(reg_info)),
| _ => match services.users.find_from_access_token(token).await {
| Ok((user_id, device_id)) => Token::User((user_id, device_id)),
| _ => Token::Invalid,
},
}
} else {
Token::None
let token = match find_token(services, token).await? {
| User((user_id, device_id, expires_at))
if expires_at.is_some_and(is_less_than!(SystemTime::now())) =>
Expired((user_id, device_id)),
| token => token,
};
if metadata.authentication == AuthScheme::None {
match metadata {
| &get_public_rooms::v3::Request::METADATA => {
if !services
.server
.config
.allow_public_room_directory_without_auth
{
match token {
| Token::Appservice(_) | Token::User(_) => {
// we should have validated the token above
// already
},
| Token::None | Token::Invalid => {
return Err!(Request(MissingToken(
"Missing or invalid access token."
)));
},
}
}
},
| &get_profile::v3::Request::METADATA
| &get_profile_key::unstable::Request::METADATA
| &get_display_name::v3::Request::METADATA
| &get_avatar_url::v3::Request::METADATA
| &get_timezone_key::unstable::Request::METADATA => {
if services
.server
.config
.require_auth_for_profile_requests
{
match token {
| Token::Appservice(_) | Token::User(_) => {
// we should have validated the token above
// already
},
| Token::None | Token::Invalid => {
return Err!(Request(MissingToken(
"Missing or invalid access token."
)));
},
}
}
},
| _ => {},
}
check_auth_still_required(services, metadata, &token)?;
}
match (metadata.authentication, token) {
| (AuthScheme::AccessToken, Token::Appservice(info)) =>
Ok(auth_appservice(services, request, info).await?),
| (
AuthScheme::None | AuthScheme::AccessTokenOptional | AuthScheme::AppserviceToken,
Token::Appservice(info),
) => Ok(Auth {
origin: None,
sender_user: None,
sender_device: None,
appservice_info: Some(*info),
}),
| (AuthScheme::AccessToken, Token::None) => match metadata {
| &get_turn_server_info::v3::Request::METADATA => {
if services.server.config.turn_allow_guests {
Ok(Auth {
origin: None,
sender_user: None,
sender_device: None,
appservice_info: None,
})
} else {
Err!(Request(MissingToken("Missing access token.")))
}
},
| _ => Err!(Request(MissingToken("Missing access token."))),
},
| (
AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None,
Token::User((user_id, device_id)),
) => Ok(Auth {
origin: None,
sender_user: Some(user_id),
sender_device: Some(device_id),
appservice_info: None,
}),
| (AuthScheme::ServerSignatures, Token::None) =>
Ok(auth_server(services, request, json_body).await?),
| (
AuthScheme::None | AuthScheme::AppserviceToken | AuthScheme::AccessTokenOptional,
Token::None,
) => Ok(Auth {
sender_user: None,
sender_device: None,
origin: None,
appservice_info: None,
}),
| (AuthScheme::ServerSignatures, Token::Appservice(_) | Token::User(_)) =>
Err!(Request(Unauthorized("Only server signatures should be used on this endpoint."))),
| (AuthScheme::AppserviceToken, Token::User(_)) => Err!(Request(Unauthorized(
"Only appservice access tokens should be used on this endpoint."
))),
| (AuthScheme::None, Token::Invalid) => {
| (AuthScheme::None, Invalid)
if request.query.access_token.is_some()
&& metadata == &get_openid_userinfo::v1::Request::METADATA =>
{
// OpenID federation endpoint uses a query param with the same name, drop this
// once query params for user auth are removed from the spec. This is
// required to make integration manager work.
if request.query.access_token.is_some()
&& metadata == &get_openid_userinfo::v1::Request::METADATA
{
Ok(Auth {
origin: None,
sender_user: None,
sender_device: None,
appservice_info: None,
})
} else {
Err(Error::BadRequest(
ErrorKind::UnknownToken { soft_logout: false },
"Unknown access token.",
))
}
Ok(Auth::default())
},
| (_, Token::Invalid) => Err(Error::BadRequest(
ErrorKind::UnknownToken { soft_logout: false },
"Unknown access token.",
)),
| (_, Invalid) =>
Err(BadRequest(UnknownToken { soft_logout: false }, "Unknown access token.")),
| (_, Expired((user_id, device_id))) => {
services
.users
.remove_access_token(&user_id, &device_id)
.await
.log_debug_err()
.ok();
Err(BadRequest(UnknownToken { soft_logout: true }, "Expired access token."))
},
| (AppserviceToken, User(_)) =>
Err!(Request(Unauthorized("Appservice tokens must be used on this endpoint."))),
| (ServerSignatures, Appservice(_) | User(_)) =>
Err!(Request(Unauthorized("Server signatures must be used on this endpoint."))),
| (ServerSignatures, Token::None) => Ok(auth_server(services, request, json_body).await?),
| (AccessToken, Appservice(info)) => Ok(auth_appservice(services, request, info).await?),
| (AccessToken, Token::None) => match metadata {
| &get_turn_server_info::v3::Request::METADATA
if services.server.config.turn_allow_guests =>
Ok(Auth::default()),
| _ => Err!(Request(MissingToken("Missing access token."))),
},
| (AccessToken | AccessTokenOptional | AuthScheme::None, User(user)) => Ok(Auth {
sender_user: Some(user.0),
sender_device: Some(user.1),
_expires_at: user.2,
..Auth::default()
}),
//TODO: add AppserviceTokenOptional
| (AccessTokenOptional | AppserviceToken | AuthScheme::None, Appservice(info)) =>
Ok(Auth {
appservice_info: Some(*info),
..Auth::default()
}),
//TODO: add AppserviceTokenOptional
| (AccessTokenOptional | AppserviceToken | AuthScheme::None, Token::None) =>
Ok(Auth::default()),
}
}
fn check_auth_still_required(services: &Services, metadata: &Metadata, token: &Token) -> Result {
debug_assert_eq!(
metadata.authentication,
AuthScheme::None,
"Expected endpoint to be unauthenticated"
);
match metadata {
| &get_profile::v3::Request::METADATA
| &get_profile_key::unstable::Request::METADATA
| &get_display_name::v3::Request::METADATA
| &get_avatar_url::v3::Request::METADATA
| &get_timezone_key::unstable::Request::METADATA
if services
.server
.config
.require_auth_for_profile_requests =>
match token {
| Token::Appservice(_) | Token::User(_) => Ok(()),
| Token::None | Token::Expired(_) | Token::Invalid =>
Err!(Request(MissingToken("Missing or invalid access token."))),
},
| &get_public_rooms::v3::Request::METADATA
if !services
.server
.config
.allow_public_room_directory_without_auth =>
match token {
| Token::Appservice(_) | Token::User(_) => Ok(()),
| Token::None | Token::Expired(_) | Token::Invalid =>
Err!(Request(MissingToken("Missing or invalid access token."))),
},
| _ => Ok(()),
}
}
async fn find_token(services: &Services, token: Option<&str>) -> Result<Token> {
let Some(token) = token else {
return Ok(Token::None);
};
let user_token = services
.users
.find_from_token(token)
.map_ok(Token::User);
let appservice_token = services
.appservice
.find_from_access_token(token)
.map_ok(Box::new)
.map_ok(Token::Appservice);
pin_mut!(user_token, appservice_token);
match select_ok([Left(user_token), Right(appservice_token)]).await {
| Err(e) if !e.is_not_found() => Err(e),
| Ok((token, _)) => Ok(token),
| _ => Ok(Token::Invalid),
}
}
@@ -225,10 +243,9 @@ async fn auth_appservice(
}
Ok(Auth {
origin: None,
sender_user: Some(user_id),
sender_device: None,
appservice_info: Some(*info),
..Auth::default()
})
}
@@ -303,13 +320,11 @@ async fn auth_server(
Ok(Auth {
origin: origin.to_owned().into(),
sender_user: None,
sender_device: None,
appservice_info: None,
..Auth::default()
})
}
fn auth_server_checks(services: &Services, x_matrix: &XMatrix) -> Result<()> {
fn auth_server_checks(services: &Services, x_matrix: &XMatrix) -> Result {
if !services.server.config.allow_federation {
return Err!(Config("allow_federation", "Federation is disabled."));
}
+3 -1
View File
@@ -1,3 +1,5 @@
use std::fmt::Debug;
use axum::{
Router,
extract::FromRequestParts,
@@ -38,7 +40,7 @@ impl<Err, Req, Fut, Fun, $($tx,)*> RumaHandler<($($tx,)* Ruma<Req>,)> for Fun
where
Fun: Fn($($tx,)* Ruma<Req>,) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Req::OutgoingResponse, Err>> + Send,
Req: IncomingRequest + Send + Sync + 'static,
Req: IncomingRequest + Debug + Send + Sync + 'static,
Err: IntoResponse + Send,
<Req as IncomingRequest>::OutgoingResponse: Send,
$( $tx: FromRequestParts<State> + Send + Sync + 'static, )*
+2 -2
View File
@@ -64,7 +64,7 @@ pub(crate) async fn get_backfill_route(
.timeline
.pdus_rev(None, &body.room_id, Some(from.saturating_add(1)))
.try_take(limit)
.try_filter_map(|(_, pdu)| async move {
.try_filter_map(async |(_, pdu)| {
Ok(services
.rooms
.state_accessor
@@ -72,7 +72,7 @@ pub(crate) async fn get_backfill_route(
.await
.then_some(pdu))
})
.try_filter_map(|pdu| async move {
.try_filter_map(async |pdu| {
Ok(services
.rooms
.timeline
+1 -1
View File
@@ -49,7 +49,7 @@ pub(crate) async fn get_event_authorization_route(
.auth_chain
.event_ids_iter(room_id, once(body.event_id.borrow()))
.ready_filter_map(Result::ok)
.filter_map(|id| async move {
.filter_map(async |id| {
services
.rooms
.timeline
+1 -1
View File
@@ -45,7 +45,7 @@ pub(crate) async fn get_hierarchy_route(
let (children, inaccessible_children) =
get_parent_children_via(&room, suggested_only)
.stream()
.broad_filter_map(|(child, _via)| async move {
.broad_filter_map(async |(child, _via)| {
match services
.rooms
.spaces
+7 -7
View File
@@ -1,5 +1,5 @@
use axum::extract::State;
use futures::StreamExt;
use futures::{StreamExt, pin_mut};
use ruma::{
CanonicalJsonObject, OwnedUserId, RoomId, RoomVersionId, UserId,
api::{client::error::ErrorKind, federation::membership::prepare_join_event},
@@ -105,7 +105,7 @@ pub(crate) async fn create_join_event_template_route(
)
.await?
{
let Some(auth_user) = services
let users = services
.rooms
.state_cache
.local_users_in_room(&body.room_id)
@@ -117,15 +117,15 @@ pub(crate) async fn create_join_event_template_route(
&state_lock,
)
})
.boxed()
.next()
.await
.map(ToOwned::to_owned)
else {
.map(ToOwned::to_owned);
pin_mut!(users);
let Some(auth_user) = users.next().await else {
return Err!(Request(UnableToGrantJoin(
"No user on this server is able to assist in joining."
)));
};
Some(auth_user)
} else {
None
+14 -14
View File
@@ -158,10 +158,11 @@ async fn handle(
let results: ResolvedMap = pdus
.into_iter()
.try_stream()
.broad_and_then(|(room_id, pdus): (_, Vec<_>)| {
handle_room(services, client, origin, started, room_id, pdus.into_iter())
.map_ok(Vec::into_iter)
.broad_and_then(async |(room_id, pdus): (_, Vec<_>)| {
handle_room(services, client, origin, &started, room_id, pdus.into_iter())
.map_ok(ResolvedMap::into_iter)
.map_ok(IterStream::try_stream)
.await
})
.try_flatten()
.try_collect()
@@ -180,28 +181,27 @@ async fn handle_room(
services: &Services,
_client: &IpAddr,
origin: &ServerName,
txn_start_time: Instant,
room_id: OwnedRoomId,
txn_start_time: &Instant,
ref room_id: OwnedRoomId,
pdus: impl Iterator<Item = Pdu> + Send,
) -> Result<Vec<(OwnedEventId, Result)>> {
) -> Result<ResolvedMap> {
let _room_lock = services
.rooms
.event_handler
.mutex_federation
.lock(&room_id)
.lock(room_id)
.await;
let room_id = &room_id;
pdus.try_stream()
.and_then(|(_, event_id, value)| async move {
.and_then(async |(room_id, event_id, value)| {
services.server.check_running()?;
let pdu_start_time = Instant::now();
let result = services
.rooms
.event_handler
.handle_incoming_pdu(origin, room_id, &event_id, value, true)
.await
.map(|_| ());
.handle_incoming_pdu(origin, &room_id, &event_id, value, true)
.map_ok(|_| ())
.await;
debug!(
pdu_elapsed = ?pdu_start_time.elapsed(),
@@ -329,7 +329,7 @@ async fn handle_edu_receipt_room(
.read
.into_iter()
.stream()
.for_each_concurrent(automatic_width(), |(user_id, user_updates)| async move {
.for_each_concurrent(automatic_width(), async |(user_id, user_updates)| {
handle_edu_receipt_room_user(services, origin, room_id, &user_id, user_updates).await;
})
.await;
@@ -368,7 +368,7 @@ async fn handle_edu_receipt_room_user(
.event_ids
.into_iter()
.stream()
.for_each_concurrent(automatic_width(), |event_id| async move {
.for_each_concurrent(automatic_width(), async |event_id| {
let user_data = [(user_id.to_owned(), data.clone())];
let receipts = [(ReceiptType::Read, BTreeMap::from(user_data))];
let content = [(event_id.clone(), BTreeMap::from(receipts))];
+1 -1
View File
@@ -242,7 +242,7 @@ async fn create_join_event(
.rooms
.auth_chain
.event_ids_iter(room_id, starting_events)
.broad_and_then(|event_id| async move {
.broad_and_then(async |event_id| {
services
.rooms
.timeline
+1 -1
View File
@@ -56,7 +56,7 @@ pub(crate) async fn get_room_state_route(
.rooms
.auth_chain
.event_ids_iter(&body.room_id, once(body.event_id.borrow()))
.and_then(|id| async move { services.rooms.timeline.get_pdu_json(&id).await })
.and_then(async |id| services.rooms.timeline.get_pdu_json(&id).await)
.and_then(|pdu| {
services
.sending
+1 -1
View File
@@ -40,7 +40,7 @@ pub(crate) async fn get_devices_route(
devices: services
.users
.all_devices_metadata(user_id)
.filter_map(|metadata| async move {
.filter_map(async |metadata| {
let device_id = metadata.device_id.clone();
let device_id_clone = device_id.clone();
let device_id_string = device_id.as_str().to_owned();
-1
View File
@@ -79,7 +79,6 @@ http.workspace = true
ipaddress.workspace = true
itertools.workspace = true
jsonwebtoken.workspace = true
ldap3.workspace = true
libc.workspace = true
libloading.workspace = true
libloading.optional = true
+5 -1
View File
@@ -68,6 +68,10 @@ pub fn check(config: &Config) -> Result {
return Err!(Config("port", "No ports were specified to listen on"));
}
if !config.listening {
warn!("Configuration item `listening` is set to `false`. Cannot hear anyone.");
}
if config.unix_socket_path.is_none() {
config.get_bind_addrs().iter().for_each(|addr| {
use std::path::Path;
@@ -317,7 +321,7 @@ fn warn_unknown_key(config: &Config) {
/// Checks the presence of the `address` and `unix_socket_path` keys in the
/// raw_config, exiting the process if both keys were detected.
pub(super) fn is_dual_listening(raw_config: &Figment) -> Result<()> {
pub(super) fn is_dual_listening(raw_config: &Figment) -> Result {
let contains_address = raw_config.contains("address");
let contains_unix_socket = raw_config.contains("unix_socket_path");
if contains_address && contains_unix_socket {
+232 -21
View File
@@ -25,7 +25,11 @@
use self::proxy::ProxyConfig;
pub use self::{check::check, manager::Manager};
use crate::{Result, err, error::Error, utils::sys};
use crate::{
Result, err,
error::Error,
utils::{string::EMPTY, sys},
};
/// All the config options for tuwunel.
#[allow(clippy::struct_excessive_bools)]
@@ -52,7 +56,8 @@
### For more information, see:
### https://tuwunel.chat/configuration.html
"#,
ignore = "catchall well_known tls blurhashing allow_invalid_tls_certificates ldap jwt"
ignore = "catchall well_known tls blurhashing allow_invalid_tls_certificates ldap jwt \
appservice"
)]
pub struct Config {
/// The server_name is the pretty name of this server. It is used as a
@@ -73,6 +78,24 @@ pub struct Config {
/// example: "girlboss.ceo"
pub server_name: OwnedServerName,
/// This is the only directory where tuwunel will save its data, including
/// media. Note: this was previously "/var/lib/matrix-conduit".
///
/// YOU NEED TO EDIT THIS.
///
/// example: "/var/lib/tuwunel"
pub database_path: PathBuf,
/// Text which will be added to the end of the user's displayname upon
/// registration with a space before the text. In Conduit, this was the
/// lightning bolt emoji.
///
/// To disable, set this to "" (an empty string).
///
/// default: "💕"
#[serde(default = "default_new_user_displayname_suffix")]
pub new_user_displayname_suffix: String,
#[allow(clippy::doc_link_with_quotes)]
/// The default address (IPv4 or IPv6) tuwunel will listen on.
///
@@ -122,14 +145,6 @@ pub struct Config {
#[serde(default = "default_unix_socket_perms")]
pub unix_socket_perms: u32,
/// This is the only directory where tuwunel will save its data, including
/// media. Note: this was previously "/var/lib/matrix-conduit".
///
/// YOU NEED TO EDIT THIS.
///
/// example: "/var/lib/tuwunel"
pub database_path: PathBuf,
/// tuwunel supports online database backups using RocksDB's Backup engine
/// API. To use this, set a database backup path that tuwunel can write
/// to.
@@ -147,16 +162,6 @@ pub struct Config {
#[serde(default = "default_database_backups_to_keep")]
pub database_backups_to_keep: i16,
/// Text which will be added to the end of the user's displayname upon
/// registration with a space before the text. In Conduit, this was the
/// lightning bolt emoji.
///
/// To disable, set this to "" (an empty string).
///
/// default: "🎔"
#[serde(default = "default_new_user_displayname_suffix")]
pub new_user_displayname_suffix: String,
/// Set this to any float value to multiply tuwunel's in-memory LRU caches
/// with such as "auth_chain_cache_capacity".
///
@@ -247,6 +252,27 @@ pub struct Config {
#[serde(default = "default_roomid_spacehierarchy_cache_capacity")]
pub roomid_spacehierarchy_cache_capacity: u32,
/// Minimum timeout a client can request for long-polling sync. Requests
/// will be clamped up to this value if smaller.
///
/// default: 5000
#[serde(default = "default_client_sync_timeout_min")]
pub client_sync_timeout_min: u64,
/// Default timeout for long-polling sync if a client does not request
/// another in their query-string.
///
/// default: 30000
#[serde(default = "default_client_sync_timeout_default")]
pub client_sync_timeout_default: u64,
/// Maximum timeout a client can request for long-polling sync. Requests
/// will be clamped down to this value if larger.
///
/// default: 90000
#[serde(default = "default_client_sync_timeout_max")]
pub client_sync_timeout_max: u64,
/// Maximum entries stored in DNS memory-cache. The size of an entry may
/// vary so please take care if raising this value excessively. Only
/// decrease this when using an external DNS cache. Please note that
@@ -572,6 +598,26 @@ pub struct Config {
#[serde(default)]
pub allow_public_room_directory_without_auth: bool,
/// Allows room directory searches to match on partial room_id's when the
/// search term starts with '!'.
///
/// default: true
#[serde(default = "true_fn")]
pub allow_public_room_search_by_id: bool,
/// Set this to false to limit results of rooms when searching by ID to
/// those that would be found by an alias or other query; specifically
/// those listed in the public rooms directory. By default this is set to
/// true allowing any joinable room to match. This satisfies the Principle
/// of Least Expectation when pasting a room_id into a search box with
/// intent to join; many rooms simply opt-out of public listings. Therefor
/// to prevent this feature from abuse, knowledge of several characters of
/// the room_id is required before any results are returned.
///
/// default: true
#[serde(default = "true_fn")]
pub allow_unlisted_room_search_by_id: bool,
/// Allow guests/unauthenticated users to access TURN credentials.
///
/// This is the equivalent of Synapse's `turn_allow_guests` config option.
@@ -814,6 +860,16 @@ pub struct Config {
#[serde(default = "default_login_token_ttl")]
pub login_token_ttl: u64,
/// Access token TTL in seconds.
///
/// For clients that support refresh-tokens, the access-token provided on
/// login will be invalidated after this amount of time and the client will
/// be soft-logged-out until refreshing it.
///
/// default: 604800
#[serde(default = "default_access_token_ttl")]
pub access_token_ttl: u64,
/// Static TURN username to provide the client if not using a shared secret
/// ("turn_secret"), It is recommended to use a shared secret over static
/// credentials.
@@ -1313,6 +1369,12 @@ pub struct Config {
#[serde(default)]
pub allow_legacy_media: bool,
/// Fallback to requesting legacy unauthenticated media from remote servers.
/// Unauthenticated media was removed in ~2024Q3; enabling this adds
/// considerable federation requests which are unlikely to succeed.
#[serde(default)]
pub request_legacy_media: bool,
#[serde(default = "true_fn")]
pub freeze_legacy_media: bool,
@@ -1621,6 +1683,23 @@ pub struct Config {
#[serde(default = "default_admin_room_tag")]
pub admin_room_tag: String,
/// Whether to grant the first user to register admin privileges by joining
/// them to the admin room. Note that technically the next user to register
/// when the admin room is empty (or only contains the server-user) is
/// granted, and only when the admin room is enabled.
///
/// default: true
#[serde(default = "true_fn")]
pub grant_admin_to_first_user: bool,
/// Whether the admin room is created on first startup. Users should not set
/// this to false. Developers can set this to false during integration tests
/// to reduce activity and output.
///
/// default: true
#[serde(default = "true_fn")]
pub create_admin_room: bool,
/// Sentry.io crash/panic reporting, performance monitoring/metrics, etc.
/// This is NOT enabled by default. tuwunel's default Sentry reporting
/// endpoint domain is `o4509498990067712.ingest.us.sentry.io`.
@@ -1818,6 +1897,10 @@ pub struct Config {
#[serde(default)]
pub jwt: JwtConfig,
// external structure; separate section
#[serde(default)]
pub appservice: BTreeMap<String, AppService>,
#[serde(flatten)]
#[allow(clippy::zero_sized_map_values)]
// this is a catchall, the map shouldn't be zero at runtime
@@ -2088,6 +2171,126 @@ pub struct JwtConfig {
pub validate_signature: bool,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[config_example_generator(
filename = "tuwunel-example.toml",
section = "global.appservice.<ID>",
ignore = "id users aliases rooms"
)]
pub struct AppService {
#[serde(default)]
pub id: String,
/// The URL for the application service.
///
/// Optionally set to `null` if no traffic is required.
pub url: Option<String>,
/// A unique token for application services to use to authenticate requests
/// to Homeservers.
///
/// default:
/// display: sensitive
pub as_token: String,
/// A unique token for Homeservers to use to authenticate requests to
/// application services.
///
/// default:
/// display: sensitive
pub hs_token: String,
/// The localpart of the user associated with the application service.
pub sender_localpart: Option<String>,
/// Events which are sent from certain users.
#[serde(default)]
pub users: Vec<AppServiceNamespace>,
/// Events which are sent in rooms with certain room aliases.
#[serde(default)]
pub aliases: Vec<AppServiceNamespace>,
/// Events which are sent in rooms with certain room IDs.
#[serde(default)]
pub rooms: Vec<AppServiceNamespace>,
/// Whether requests from masqueraded users are rate-limited.
///
/// The sender is excluded.
#[serde(default)]
pub rate_limited: bool,
/// The external protocols which the application service provides (e.g.
/// IRC).
///
/// default: []
#[serde(default)]
pub protocols: Vec<String>,
/// Whether the application service wants to receive ephemeral data.
///
/// default: false
#[serde(default)]
pub receive_ephemeral: bool,
/// Whether the application service wants to do device management, as part
/// of MSC4190.
///
/// default: false
#[serde(default)]
pub device_management: bool,
}
impl From<AppService> for ruma::api::appservice::Registration {
fn from(conf: AppService) -> Self {
use ruma::api::appservice::Namespaces;
Self {
id: conf.id,
url: conf.url,
as_token: conf.as_token,
hs_token: conf.hs_token,
receive_ephemeral: conf.receive_ephemeral,
device_management: conf.device_management,
protocols: conf.protocols.into(),
rate_limited: conf.rate_limited.into(),
sender_localpart: conf
.sender_localpart
.unwrap_or_else(|| EMPTY.into()),
namespaces: Namespaces {
users: conf.users.into_iter().map(Into::into).collect(),
aliases: conf.aliases.into_iter().map(Into::into).collect(),
rooms: conf.rooms.into_iter().map(Into::into).collect(),
},
}
}
}
#[derive(Clone, Debug, Default, Deserialize)]
#[config_example_generator(
filename = "tuwunel-example.toml",
section = "[global.appservice.<ID>.<users|rooms|aliases>]"
)]
pub struct AppServiceNamespace {
/// Whether this application service has exclusive access to events within
/// this namespace.
#[serde(default)]
pub exclusive: bool,
/// A regular expression defining which values this namespace includes.
pub regex: String,
}
impl From<AppServiceNamespace> for ruma::api::appservice::Namespace {
fn from(conf: AppServiceNamespace) -> Self {
Self {
exclusive: conf.exclusive,
regex: conf.regex,
}
}
}
#[derive(Deserialize, Clone, Debug)]
#[serde(transparent)]
struct ListeningPort {
@@ -2407,7 +2610,7 @@ fn default_url_preview_max_spider_size() -> usize {
256_000 // 256KB
}
fn default_new_user_displayname_suffix() -> String { "🎔".to_owned() }
fn default_new_user_displayname_suffix() -> String { "💕".to_owned() }
fn default_sentry_endpoint() -> Option<Url> {
let url = "https://8994b1762a6a95af9502a7900edabc4c@o4509498990067712.ingest.us.sentry.io/4509498993213440"
@@ -2493,3 +2696,11 @@ fn default_ldap_name_attribute() -> String { String::from("givenName") }
fn default_jwt_algorithm() -> String { "HS256".to_owned() }
fn default_jwt_format() -> String { "HMAC".to_owned() }
fn default_client_sync_timeout_min() -> u64 { 5000 }
fn default_client_sync_timeout_default() -> u64 { 30000 }
fn default_client_sync_timeout_max() -> u64 { 90000 }
fn default_access_token_ttl() -> u64 { 604_800 }
+2
View File
@@ -29,6 +29,8 @@ pub enum Error {
#[error(transparent)]
Std(#[from] Box<dyn std::error::Error + Send>),
#[error(transparent)]
SystemTime(#[from] std::time::SystemTimeError),
#[error(transparent)]
ThreadAccessError(#[from] std::thread::AccessError),
#[error(transparent)]
TryFromInt(#[from] std::num::TryFromIntError),
+1 -1
View File
@@ -78,7 +78,7 @@ fn init_features() -> Result<Vec<String>> {
Ok(features)
}
fn append_features(features: &mut Vec<String>, manifest: &str) -> Result<()> {
fn append_features(features: &mut Vec<String>, manifest: &str) -> Result {
let manifest = Manifest::from_str(manifest)?;
features.extend(manifest.features.keys().cloned());
+2 -2
View File
@@ -22,7 +22,7 @@ pub fn fmt_markdown<S>(out: Arc<Mutex<S>>) -> Box<Closure>
pub fn fmt<F, S>(fun: F, out: Arc<Mutex<S>>) -> Box<Closure>
where
F: Fn(&mut S, &Level, &str, &str) -> Result<()> + Send + Sync + Copy + 'static,
F: Fn(&mut S, &Level, &str, &str) -> Result + Send + Sync + Copy + 'static,
S: std::fmt::Write + Send + 'static,
{
Box::new(move |data| call(fun, &mut *out.lock().expect("locked"), &data))
@@ -30,7 +30,7 @@ pub fn fmt<F, S>(fun: F, out: Arc<Mutex<S>>) -> Box<Closure>
fn call<F, S>(fun: F, out: &mut S, data: &Data<'_>)
where
F: Fn(&mut S, &Level, &str, &str) -> Result<()>,
F: Fn(&mut S, &Level, &str, &str) -> Result,
S: std::fmt::Write,
{
fun(out, &data.level(), data.span_name(), data.message()).expect("log line appended");
+3 -1
View File
@@ -73,7 +73,9 @@ impl ConsoleFormat {
#[must_use]
pub fn new(config: &Config) -> Self {
Self {
_compact: fmt::format().compact(),
_compact: fmt::format()
.compact()
.with_ansi(config.log_colors),
full: Format::<Full>::default()
.with_thread_ids(config.log_thread_ids)
+4 -4
View File
@@ -3,7 +3,7 @@
use super::{Level, color};
use crate::Result;
pub fn html<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result<()>
pub fn html<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result
where
S: Write + ?Sized,
{
@@ -18,7 +18,7 @@ pub fn html<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result<()>
Ok(())
}
pub fn markdown<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result<()>
pub fn markdown<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result
where
S: Write + ?Sized,
{
@@ -28,7 +28,7 @@ pub fn markdown<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result<
Ok(())
}
pub fn markdown_table<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result<()>
pub fn markdown_table<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result
where
S: Write + ?Sized,
{
@@ -38,7 +38,7 @@ pub fn markdown_table<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> R
Ok(())
}
pub fn markdown_table_head<S>(out: &mut S) -> Result<()>
pub fn markdown_table_head<S>(out: &mut S) -> Result
where
S: Write + ?Sized,
{
+1 -1
View File
@@ -49,7 +49,7 @@ pub fn add(&self, name: &str, handle: Handle) {
.insert(name.into(), handle);
}
pub fn reload(&self, new_value: &EnvFilter, names: Option<&[&str]>) -> Result<()> {
pub fn reload(&self, new_value: &EnvFilter, names: Option<&[&str]>) -> Result {
self.handles
.lock()
.expect("locked")
+1 -1
View File
@@ -21,7 +21,7 @@
use crate::{Result, utils};
/// Abstraction of a PDU so users can have their own PDU types.
pub trait Event: Clone + Debug {
pub trait Event: Clone + Debug + Send + Sync {
/// Serialize into a Ruma JSON format, consuming.
#[inline]
fn into_format<T>(self) -> T
+10 -9
View File
@@ -1,5 +1,6 @@
mod builder;
mod count;
mod hashes;
mod id;
mod raw_id;
mod redact;
@@ -20,6 +21,7 @@
Count as PduCount, Id as PduId, Pdu as PduEvent, RawId as RawPduId,
builder::{Builder, Builder as PduBuilder},
count::Count,
hashes::EventHashes as EventHash,
id::{ShortId, *},
raw_id::*,
};
@@ -67,13 +69,6 @@ pub struct Pdu {
pub signatures: Option<Box<RawJsonValue>>,
}
/// Content hashes of a PDU.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct EventHash {
/// The SHA-256 hash.
pub sha256: String,
}
impl Pdu {
pub fn from_id_val(event_id: &EventId, mut json: CanonicalJsonObject) -> Result<Self> {
let event_id = CanonicalJsonValue::String(event_id.into());
@@ -84,7 +79,10 @@ pub fn from_id_val(event_id: &EventId, mut json: CanonicalJsonObject) -> Result<
}
}
impl Event for Pdu {
impl Event for Pdu
where
Self: Send + Sync + 'static,
{
#[inline]
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_ {
self.auth_events.iter().map(AsRef::as_ref)
@@ -137,7 +135,10 @@ fn into_pdu(self) -> Pdu { self }
fn is_owned(&self) -> bool { true }
}
impl Event for &Pdu {
impl Event for &Pdu
where
Self: Send,
{
#[inline]
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_ {
self.auth_events.iter().map(AsRef::as_ref)
+12
View File
@@ -0,0 +1,12 @@
use serde::{Deserialize, Serialize};
use crate::arrayvec::ArrayString;
/// Content hashes of a PDU.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct EventHashes {
/// The SHA-256 hash.
pub sha256: ArrayString<SHA256_LEN>,
}
const SHA256_LEN: usize = 43;
+10 -1
View File
@@ -1,8 +1,10 @@
use std::fmt;
use arrayvec::ArrayVec;
use super::{Count, Id, ShortEventId, ShortId, ShortRoomId};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub enum RawId {
Normal(RawIdNormal),
Backfilled(RawIdBackfilled),
@@ -61,6 +63,13 @@ pub fn as_bytes(&self) -> &[u8] {
}
}
impl fmt::Debug for RawId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let id: Id = (*self).into();
write!(f, "{id:?}")
}
}
impl AsRef<[u8]> for RawId {
#[inline]
fn as_ref(&self) -> &[u8] { self.as_bytes() }
+1 -1
View File
@@ -418,7 +418,7 @@ fn to_pdu_event<S>(
auth_events,
prev_events,
depth: uint!(0),
hashes: EventHash { sha256: String::new() },
hashes: EventHash::default(),
signatures: None,
}
}
+6 -5
View File
@@ -571,10 +571,11 @@ struct GetThirdPartyInvite {
(target_membership == MembershipState::Join).then_some(&power_levels.users_default)
});
let mut join_rules = JoinRule::Invite;
if let Some(jr) = &join_rules_event {
join_rules = from_json_str::<RoomJoinRulesEventContent>(jr.content().get())?.join_rule;
}
let join_rules = if let Some(jr) = &join_rules_event {
from_json_str::<RoomJoinRulesEventContent>(jr.content().get())?.join_rule
} else {
JoinRule::Invite
};
let power_levels_event_id = power_levels_event.as_ref().map(Event::event_id);
let sender_membership_event_id = sender_membership_event
@@ -649,7 +650,7 @@ struct GetThirdPartyInvite {
warn!(?target_user_membership_event_id, "Banned user can't join");
false
} else if (join_rules == JoinRule::Invite
|| room_version.allow_knocking && join_rules == JoinRule::Knock)
|| room_version.allow_knocking && (join_rules == JoinRule::Knock || matches!(join_rules, JoinRule::KnockRestricted(_))))
// If the join_rule is invite then allow if membership state is invite or join
&& (target_user_current_membership == MembershipState::Join
|| target_user_current_membership == MembershipState::Invite)
+2 -2
View File
@@ -426,7 +426,7 @@ pub(crate) fn to_init_pdu_event(
auth_events: vec![],
prev_events: vec![],
depth: uint!(0),
hashes: EventHash { sha256: "".to_owned() },
hashes: EventHash::default(),
signatures: None,
}
}
@@ -474,7 +474,7 @@ pub(crate) fn to_pdu_event<S>(
auth_events,
prev_events,
depth: uint!(0),
hashes: EventHash { sha256: "".to_owned() },
hashes: EventHash::default(),
signatures: None,
}
}
+5 -3
View File
@@ -1,4 +1,4 @@
use std::sync::atomic::AtomicU32;
use std::sync::atomic::{AtomicU32, AtomicU64};
use tokio::runtime;
use tokio_metrics::TaskMonitor;
@@ -19,8 +19,9 @@ pub struct Metrics {
runtime_intervals: std::sync::Mutex<Option<RuntimeIntervals>>,
// TODO: move stats
pub requests_count: AtomicU64,
pub requests_handle_finished: AtomicU64,
pub requests_handle_active: AtomicU32,
pub requests_handle_finished: AtomicU32,
pub requests_panic: AtomicU32,
}
@@ -48,8 +49,9 @@ pub fn new(runtime: Option<runtime::Handle>) -> Self {
#[cfg(tokio_unstable)]
runtime_intervals: std::sync::Mutex::new(runtime_intervals),
requests_count: AtomicU64::new(0),
requests_handle_finished: AtomicU64::new(0),
requests_handle_active: AtomicU32::new(0),
requests_handle_finished: AtomicU32::new(0),
requests_panic: AtomicU32::new(0),
}
}

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